diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html index 7461ac475b..87ea02be93 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html @@ -29,7 +29,6 @@ limitations under the License. - diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index d634f14339..9b63f759e5 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js @@ -27,6 +27,34 @@ */ Defs.patchRange; + /** + * @typedef {{ + * url: string, + * fetchOptions: (Object|null|undefined), + * anonymizedUrl: (string|undefined), + * }} + */ + Defs.FetchRequest; + + /** + * Object to describe a request for passing into _fetchJSON or _fetchRawJSON. + * - url is the URL for the request (excluding get params) + * - errFn is a function to invoke when the request fails. + * - cancelCondition is a function that, if provided and returns true, will + * cancel the response after it resolves. + * - params is a key-value hash to specify get params for the request URL. + * @typedef {{ + * url: string, + * errFn: (function(?Response, string=)|null|undefined), + * cancelCondition: (function()|null|undefined), + * params: (Object|null|undefined), + * fetchOptions: (Object|null|undefined), + * anonymizedUrl: (string|undefined), + * reportUrlAsIs: (boolean|undefined), + * }} + */ + Defs.FetchJSONRequest; + /** * @typedef {{ * changeNum: (string|number), @@ -93,6 +121,7 @@ const MAX_PROJECT_RESULTS = 25; const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900; const PARENT_PATCH_NUM = 'PARENT'; + const FAILED_TO_FETCH_ERROR = 'Failed to fetch'; const Requests = { SEND_DIFF_DRAFT: 'sendDiffDraft', @@ -106,6 +135,60 @@ const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL + '/revisions/*'; + /** + * Wrapper around Map for caching server responses. Site-based so that + * changes to CANONICAL_PATH will result in a different cache going into + * effect. + */ + class SiteBasedCache { + constructor() { + // Container of per-canonical-path caches. + this._data = new Map(); + if (window.INITIAL_DATA != undefined) { + // Put all data shipped with index.html into the cache. This makes it + // so that we spare more round trips to the server when the app loads + // initially. + Object + .entries(window.INITIAL_DATA) + .forEach(e => this._cache().set(e[0], e[1])); + } + } + + // Returns the cache for the current canonical path. + _cache() { + if (!this._data.has(window.CANONICAL_PATH)) { + this._data.set(window.CANONICAL_PATH, new Map()); + } + return this._data.get(window.CANONICAL_PATH); + } + + has(key) { + return this._cache().has(key); + } + + get(key) { + return this._cache().get(key); + } + + set(key, value) { + this._cache().set(key, value); + } + + delete(key) { + this._cache().delete(key); + } + + invalidatePrefix(prefix) { + const newMap = new Map(); + for (const [key, value] of this._cache().entries()) { + if (!key.startsWith(prefix)) { + newMap.set(key, value); + } + } + this._data.set(window.CANONICAL_PATH, newMap); + } + } + Polymer({ is: 'gr-rest-api-interface', _legacyUndefinedCheck: true, @@ -152,7 +235,7 @@ }, _sharedFetchPromises: { type: Object, - value: new FetchPromisesCache(), // Shared across instances. + value: {}, // Intentional to share the object across instances. }, _pendingRequests: { type: Object, @@ -176,14 +259,133 @@ }, JSON_PREFIX, - ready() { - this._restApiHelper = new GrRestApiHelper(this._cache, this._auth, - this._sharedFetchPromises, this._credentialCheck, this); + + /** + * Wraps calls to the underlying authenticated fetch function (_auth.fetch) + * with timing and logging. + * @param {Defs.FetchRequest} req + */ + _fetch(req) { + const start = Date.now(); + const xhr = this._auth.fetch(req.url, req.fetchOptions); + + // Log the call after it completes. + xhr.then(res => this._logCall(req, start, res.status)); + + // Return the XHR directly (without the log). + return xhr; }, - _fetchSharedCacheURL(req) { - // Cache is shared across instances - return this._restApiHelper.fetchCacheURL(req); + /** + * Log information about a REST call. Because the elapsed time is determined + * by this method, it should be called immediately after the request + * finishes. + * @param {Defs.FetchRequest} req + * @param {number} startTime the time that the request was started. + * @param {number} status the HTTP status of the response. The status value + * is used here rather than the response object so there is no way this + * method can read the body stream. + */ + _logCall(req, startTime, status) { + const method = (req.fetchOptions && req.fetchOptions.method) ? + req.fetchOptions.method : 'GET'; + const endTime = Date.now(); + const elapsed = (endTime - startTime); + const startAt = new Date(startTime); + const endAt = new Date(endTime); + console.log([ + 'HTTP', + status, + method, + elapsed + 'ms', + req.anonymizedUrl || req.url, + `(${startAt.toISOString()}, ${endAt.toISOString()})`, + ].join(' ')); + if (req.anonymizedUrl) { + this.fire('rpc-log', + {status, method, elapsed, anonymizedUrl: req.anonymizedUrl}); + } + }, + + /** + * Fetch JSON from url provided. + * Returns a Promise that resolves to a native Response. + * Doesn't do error checking. Supports cancel condition. Performs auth. + * Validates auth expiry errors. + * @param {Defs.FetchJSONRequest} req + */ + _fetchRawJSON(req) { + const urlWithParams = this._urlWithParams(req.url, req.params); + const fetchReq = { + url: urlWithParams, + fetchOptions: req.fetchOptions, + anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl, + }; + return this._fetch(fetchReq).then(res => { + if (req.cancelCondition && req.cancelCondition()) { + res.body.cancel(); + return; + } + return res; + }).catch(err => { + const isLoggedIn = !!this._cache.get('/accounts/self/detail'); + if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) { + this.checkCredentials(); + } else { + if (req.errFn) { + req.errFn.call(undefined, null, err); + } else { + this.fire('network-error', {error: err}); + } + } + throw err; + }); + }, + + /** + * Fetch JSON from url provided. + * Returns a Promise that resolves to a parsed response. + * Same as {@link _fetchRawJSON}, plus error handling. + * @param {Defs.FetchJSONRequest} req + */ + _fetchJSON(req) { + req = this._addAcceptJsonHeader(req); + return this._fetchRawJSON(req).then(response => { + if (!response) { + return; + } + if (!response.ok) { + if (req.errFn) { + req.errFn.call(null, response); + return; + } + this.fire('server-error', {request: req, response}); + return; + } + return response && this.getResponseObject(response); + }); + }, + + /** + * @param {string} url + * @param {?Object|string=} opt_params URL params, key-value hash. + * @return {string} + */ + _urlWithParams(url, opt_params) { + if (!opt_params) { return this.getBaseUrl() + url; } + + const params = []; + for (const p in opt_params) { + if (!opt_params.hasOwnProperty(p)) { continue; } + if (opt_params[p] == null) { + params.push(encodeURIComponent(p)); + continue; + } + for (const value of [].concat(opt_params[p])) { + params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`); + } + } + return this.getBaseUrl() + url + '?' + params.join('&'); }, /** @@ -191,7 +393,45 @@ * @return {?} */ getResponseObject(response) { - return this._restApiHelper.getResponseObject(response); + return this._readResponsePayload(response) + .then(payload => payload.parsed); + }, + + /** + * @param {!Object} response + * @return {!Object} + */ + _readResponsePayload(response) { + return response.text().then(text => { + let result; + try { + result = this._parsePrefixedJSON(text); + } catch (_) { + result = null; + } + return {parsed: result, raw: text}; + }); + }, + + /** + * @param {string} source + * @return {?} + */ + _parsePrefixedJSON(source) { + return JSON.parse(source.substring(JSON_PREFIX.length)); + }, + + /** + * @param {Defs.FetchJSONRequest} req + * @return {Defs.FetchJSONRequest} + */ + _addAcceptJsonHeader(req) { + if (!req.fetchOptions) req.fetchOptions = {}; + if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers(); + if (!req.fetchOptions.headers.has('Accept')) { + req.fetchOptions.headers.append('Accept', 'application/json'); + } + return req; }, getConfig(noCache) { @@ -202,7 +442,7 @@ }); } - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/config/server/info', reportUrlAsIs: true, }); @@ -252,7 +492,7 @@ // supports it. const url = `/projects/${encodeURIComponent(repo)}/config`; this._cache.delete(url); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url, body: config, @@ -266,7 +506,7 @@ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. const encodeName = encodeURIComponent(repo); - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: `/projects/${encodeName}/gc`, body: '', @@ -284,7 +524,7 @@ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. const encodeName = encodeURIComponent(config.name); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/projects/${encodeName}`, body: config, @@ -300,7 +540,7 @@ createGroup(config, opt_errFn) { if (!config.name) { return ''; } const encodeName = encodeURIComponent(config.name); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeName}`, body: config, @@ -310,7 +550,7 @@ }, getGroupConfig(group, opt_errFn) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/groups/${encodeURIComponent(group)}/detail`, errFn: opt_errFn, anonymizedUrl: '/groups/*/detail', @@ -328,7 +568,7 @@ // supports it. const encodeName = encodeURIComponent(repo); const encodeRef = encodeURIComponent(ref); - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: `/projects/${encodeName}/branches/${encodeRef}`, body: '', @@ -348,7 +588,7 @@ // supports it. const encodeName = encodeURIComponent(repo); const encodeRef = encodeURIComponent(ref); - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: `/projects/${encodeName}/tags/${encodeRef}`, body: '', @@ -369,7 +609,7 @@ // supports it. const encodeName = encodeURIComponent(name); const encodeBranch = encodeURIComponent(branch); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/projects/${encodeName}/branches/${encodeBranch}`, body: revision, @@ -390,7 +630,7 @@ // supports it. const encodeName = encodeURIComponent(name); const encodeTag = encodeURIComponent(tag); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/projects/${encodeName}/tags/${encodeTag}`, body: revision, @@ -415,7 +655,7 @@ getGroupMembers(groupName, opt_errFn) { const encodeName = encodeURIComponent(groupName); - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/groups/${encodeName}/members/`, errFn: opt_errFn, anonymizedUrl: '/groups/*/members', @@ -423,7 +663,7 @@ }, getIncludedGroup(groupName) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/groups/${encodeURIComponent(groupName)}/groups/`, anonymizedUrl: '/groups/*/groups', }); @@ -431,7 +671,7 @@ saveGroupName(groupId, name) { const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeId}/name`, body: {name}, @@ -441,7 +681,7 @@ saveGroupOwner(groupId, ownerId) { const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeId}/owner`, body: {owner: ownerId}, @@ -451,7 +691,7 @@ saveGroupDescription(groupId, description) { const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeId}/description`, body: {description}, @@ -461,7 +701,7 @@ saveGroupOptions(groupId, options) { const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeId}/options`, body: options, @@ -480,7 +720,7 @@ saveGroupMembers(groupName, groupMembers) { const encodeName = encodeURIComponent(groupName); const encodeMember = encodeURIComponent(groupMembers); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/groups/${encodeName}/members/${encodeMember}`, parseResponse: true, @@ -497,7 +737,7 @@ errFn: opt_errFn, anonymizedUrl: '/groups/*/groups/*', }; - return this._restApiHelper.send(req).then(response => { + return this._send(req).then(response => { if (response.ok) { return this.getResponseObject(response); } @@ -507,7 +747,7 @@ deleteGroupMembers(groupName, groupMembers) { const encodeName = encodeURIComponent(groupName); const encodeMember = encodeURIComponent(groupMembers); - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: `/groups/${encodeName}/members/${encodeMember}`, anonymizedUrl: '/groups/*/members/*', @@ -517,7 +757,7 @@ deleteIncludedGroup(groupName, includedGroup) { const encodeName = encodeURIComponent(groupName); const encodeIncludedGroup = encodeURIComponent(includedGroup); - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, anonymizedUrl: '/groups/*/groups/*', @@ -604,7 +844,7 @@ prefs.download_scheme = prefs.download_scheme.toLowerCase(); } - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/preferences', body: prefs, @@ -620,7 +860,7 @@ saveDiffPreferences(prefs, opt_errFn) { // Invalidate the cache. this._cache.delete('/accounts/self/preferences.diff'); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/preferences.diff', body: prefs, @@ -636,7 +876,7 @@ saveEditPreferences(prefs, opt_errFn) { // Invalidate the cache. this._cache.delete('/accounts/self/preferences.edit'); - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/preferences.edit', body: prefs, @@ -670,14 +910,14 @@ }, getExternalIds() { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/accounts/self/external.ids', reportUrlAsIs: true, }); }, deleteAccountIdentity(id) { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: '/accounts/self/external.ids:delete', body: id, @@ -691,7 +931,7 @@ * @return {!Promise} */ getAccountDetails(userId) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/accounts/${encodeURIComponent(userId)}/detail`, anonymizedUrl: '/accounts/*/detail', }); @@ -709,7 +949,7 @@ * @param {function(?Response, string=)=} opt_errFn */ addAccountEmail(email, opt_errFn) { - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/emails/' + encodeURIComponent(email), errFn: opt_errFn, @@ -722,7 +962,7 @@ * @param {function(?Response, string=)=} opt_errFn */ deleteAccountEmail(email, opt_errFn) { - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: '/accounts/self/emails/' + encodeURIComponent(email), errFn: opt_errFn, @@ -742,7 +982,7 @@ errFn: opt_errFn, anonymizedUrl: '/accounts/self/emails/*/preferred', }; - return this._restApiHelper.send(req).then(() => { + return this._send(req).then(() => { // If result of getAccountEmails is in cache, update it in the cache // so we don't have to invalidate it. const cachedEmails = this._cache.get('/accounts/self/emails'); @@ -786,7 +1026,7 @@ parseResponse: true, reportUrlAsIs: true, }; - return this._restApiHelper.send(req) + return this._send(req) .then(newName => this._updateCachedAccount({name: newName})); }, @@ -803,7 +1043,7 @@ parseResponse: true, reportUrlAsIs: true, }; - return this._restApiHelper.send(req) + return this._send(req) .then(newName => this._updateCachedAccount({username: newName})); }, @@ -820,33 +1060,33 @@ parseResponse: true, reportUrlAsIs: true, }; - return this._restApiHelper.send(req) + return this._send(req) .then(newStatus => this._updateCachedAccount({status: newStatus})); }, getAccountStatus(userId) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/accounts/${encodeURIComponent(userId)}/status`, anonymizedUrl: '/accounts/*/status', }); }, getAccountGroups() { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/accounts/self/groups', reportUrlAsIs: true, }); }, getAccountAgreements() { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/accounts/self/agreements', reportUrlAsIs: true, }); }, saveAccountAgreement(name) { - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/agreements', body: name, @@ -891,7 +1131,34 @@ }, checkCredentials() { - return this._restApiHelper.checkCredentials(); + if (this._credentialCheck.checking) { + return; + } + this._credentialCheck.checking = true; + let req = {url: '/accounts/self/detail', reportUrlAsIs: true}; + req = this._addAcceptJsonHeader(req); + // Skip the REST response cache. + return this._fetchRawJSON(req).then(res => { + if (!res) { return; } + if (res.status === 403) { + this.fire('auth-error'); + this._cache.delete('/accounts/self/detail'); + } else if (res.ok) { + return this.getResponseObject(res); + } + }).then(res => { + this._credentialCheck.checking = false; + if (res) { + this._cache.set('/accounts/self/detail', res); + } + return res; + }).catch(err => { + this._credentialCheck.checking = false; + if (err && err.message === FAILED_TO_FETCH_ERROR) { + this.fire('auth-error'); + this._cache.delete('/accounts/self/detail'); + } + }); }, getDefaultPreferences() { @@ -937,7 +1204,7 @@ * @param {function(?Response, string=)=} opt_errFn */ saveWatchedProjects(projects, opt_errFn) { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: '/accounts/self/watched.projects', body: projects, @@ -952,7 +1219,7 @@ * @param {function(?Response, string=)=} opt_errFn */ deleteWatchedProjects(projects, opt_errFn) { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: '/accounts/self/watched.projects:delete', body: projects, @@ -961,6 +1228,45 @@ }); }, + /** + * @param {Defs.FetchJSONRequest} req + */ + _fetchSharedCacheURL(req) { + if (this._sharedFetchPromises[req.url]) { + return this._sharedFetchPromises[req.url]; + } + // TODO(andybons): Periodic cache invalidation. + if (this._cache.has(req.url)) { + return Promise.resolve(this._cache.get(req.url)); + } + this._sharedFetchPromises[req.url] = this._fetchJSON(req) + .then(response => { + if (response !== undefined) { + this._cache.set(req.url, response); + } + this._sharedFetchPromises[req.url] = undefined; + return response; + }).catch(err => { + this._sharedFetchPromises[req.url] = undefined; + throw err; + }); + return this._sharedFetchPromises[req.url]; + }, + + /** + * @param {string} prefix + */ + _invalidateSharedFetchPromisesPrefix(prefix) { + const newObject = {}; + Object.entries(this._sharedFetchPromises).forEach(([key, value]) => { + if (!key.startsWith(prefix)) { + newObject[key] = value; + } + }); + this._sharedFetchPromises = newObject; + this._cache.invalidatePrefix(prefix); + }, + _isNarrowScreen() { return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; }, @@ -1002,7 +1308,7 @@ params, reportUrlAsIs: true, }; - return this._restApiHelper.fetchJSON(req).then(response => { + return this._fetchJSON(req).then(response => { // Response may be an array of changes OR an array of arrays of // changes. if (opt_query instanceof Array) { @@ -1101,8 +1407,7 @@ */ _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) { return this.getChangeActionURL(changeNum, null, '/detail').then(url => { - const urlWithParams = this._restApiHelper - .urlWithParams(url, optionsHex); + const urlWithParams = this._urlWithParams(url, optionsHex); const params = {O: optionsHex}; let req = { url, @@ -1112,10 +1417,10 @@ fetchOptions: this._etags.getOptions(urlWithParams), anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex, }; - req = this._restApiHelper.addAcceptJsonHeader(req); - return this._restApiHelper.fetchRawJSON(req).then(response => { + req = this._addAcceptJsonHeader(req); + return this._fetchRawJSON(req).then(response => { if (response && response.status === 304) { - return Promise.resolve(this._restApiHelper.parsePrefixedJSON( + return Promise.resolve(this._parsePrefixedJSON( this._etags.getCachedPayload(urlWithParams))); } @@ -1129,7 +1434,7 @@ } const payloadPromise = response ? - this._restApiHelper.readResponsePayload(response) : + this._readResponsePayload(response) : Promise.resolve(null); return payloadPromise.then(payload => { @@ -1342,11 +1647,11 @@ }, invalidateGroupsCache() { - this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?'); + this._invalidateSharedFetchPromisesPrefix('/groups/?'); }, invalidateReposCache() { - this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?'); + this._invalidateSharedFetchPromisesPrefix('/projects/?'); }, /** @@ -1384,7 +1689,7 @@ setRepoHead(repo, ref) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/projects/${encodeURIComponent(repo)}/HEAD`, body: {ref}, @@ -1408,7 +1713,7 @@ const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`; // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url, errFn: opt_errFn, anonymizedUrl: '/projects/*/branches?*', @@ -1432,7 +1737,7 @@ encodedFilter; // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url, errFn: opt_errFn, anonymizedUrl: '/projects/*/tags', @@ -1451,7 +1756,7 @@ const encodedFilter = this._computeFilter(filter); const n = pluginsPerPage + 1; const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`; - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url, errFn: opt_errFn, anonymizedUrl: '/plugins/?all', @@ -1461,7 +1766,7 @@ getRepoAccessRights(repoName, opt_errFn) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/projects/${encodeURIComponent(repoName)}/access`, errFn: opt_errFn, anonymizedUrl: '/projects/*/access', @@ -1471,7 +1776,7 @@ setRepoAccessRights(repoName, repoInfo) { // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend // supports it. - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: `/projects/${encodeURIComponent(repoName)}/access`, body: repoInfo, @@ -1480,7 +1785,7 @@ }, setRepoAccessRightsForReview(projectName, projectInfo) { - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: `/projects/${encodeURIComponent(projectName)}/access:review`, body: projectInfo, @@ -1497,7 +1802,7 @@ getSuggestedGroups(inputVal, opt_n, opt_errFn) { const params = {s: inputVal}; if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/groups/', errFn: opt_errFn, params, @@ -1517,7 +1822,7 @@ type: 'ALL', }; if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/projects/', errFn: opt_errFn, params, @@ -1536,7 +1841,7 @@ } const params = {suggest: null, q: inputVal}; if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/accounts/', errFn: opt_errFn, params, @@ -1567,7 +1872,7 @@ throw Error('Unsupported HTTP method: ' + method); } - return this._restApiHelper.send({method, url, body}); + return this._send({method, url, body}); }); }, @@ -1597,7 +1902,7 @@ O: options, q: 'status:open is:mergeable conflicts:' + changeNum, }; - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/changes/', params, anonymizedUrl: '/changes/conflicts:*', @@ -1619,7 +1924,7 @@ O: options, q: query, }; - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/changes/', params, anonymizedUrl: '/changes/change:*', @@ -1642,7 +1947,7 @@ O: options, q: query, }; - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/changes/', params, anonymizedUrl: '/changes/topic:*', @@ -1688,7 +1993,7 @@ this.getChangeActionURL(changeNum, patchNum, '/review'), ]; return Promise.all(promises).then(([, url]) => { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url, body: review, @@ -1722,7 +2027,7 @@ */ createChange(project, branch, subject, opt_topic, opt_isPrivate, opt_workInProgress, opt_baseChange, opt_baseCommit) { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: '/changes/', body: { @@ -1897,7 +2202,7 @@ return this.getFromProjectLookup(changeNum).then(project => { const url = '/accounts/self/starred.changes/' + (project ? encodeURIComponent(project) + '~' : '') + changeNum; - return this._restApiHelper.send({ + return this._send({ method: starred ? 'PUT' : 'DELETE', url, anonymizedUrl: '/accounts/self/starred.changes/*', @@ -1914,7 +2219,59 @@ }, /** - * Public version of the _restApiHelper.send method preserved for plugins. + * Send an XHR. + * @param {Defs.SendRequest} req + * @return {Promise} + */ + _send(req) { + const options = {method: req.method}; + if (req.body) { + options.headers = new Headers(); + options.headers.set( + 'Content-Type', req.contentType || 'application/json'); + options.body = typeof req.body === 'string' ? + req.body : JSON.stringify(req.body); + } + if (req.headers) { + if (!options.headers) { options.headers = new Headers(); } + for (const header in req.headers) { + if (!req.headers.hasOwnProperty(header)) { continue; } + options.headers.set(header, req.headers[header]); + } + } + const url = req.url.startsWith('http') ? + req.url : this.getBaseUrl() + req.url; + const fetchReq = { + url, + fetchOptions: options, + anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl, + }; + const xhr = this._fetch(fetchReq).then(response => { + if (!response.ok) { + if (req.errFn) { + return req.errFn.call(undefined, response); + } + this.fire('server-error', {request: fetchReq, response}); + } + return response; + }).catch(err => { + this.fire('network-error', {error: err}); + if (req.errFn) { + return req.errFn.call(undefined, null, err); + } else { + throw err; + } + }); + + if (req.parseResponse) { + return xhr.then(res => this.getResponseObject(res)); + } + + return xhr; + }, + + /** + * Public version of the _send method preserved for plugins. * @param {string} method * @param {string} url * @param {?string|number|Object=} opt_body passed as null sometimes @@ -1927,7 +2284,7 @@ */ send(method, url, opt_body, opt_errFn, opt_contentType, opt_headers) { - return this._restApiHelper.send({ + return this._send({ method, url, body: opt_body, @@ -2184,7 +2541,7 @@ }, getCommitInfo(project, commit) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/projects/' + encodeURIComponent(project) + '/commits/' + encodeURIComponent(commit), anonymizedUrl: '/projects/*/comments/*', @@ -2192,7 +2549,7 @@ }, _fetchB64File(url) { - return this._restApiHelper.fetch({url: this.getBaseUrl() + url}) + return this._fetch({url: this.getBaseUrl() + url}) .then(response => { if (!response.ok) { return Promise.reject(new Error(response.statusText)); @@ -2316,7 +2673,7 @@ }, deleteAccountHttpPassword() { - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: '/accounts/self/password.http', reportUrlAsIs: true, @@ -2329,7 +2686,7 @@ * parameter. */ generateAccountHttpPassword() { - return this._restApiHelper.send({ + return this._send({ method: 'PUT', url: '/accounts/self/password.http', body: {generate: true}, @@ -2353,7 +2710,7 @@ contentType: 'plain/text', reportUrlAsIs: true, }; - return this._restApiHelper.send(req) + return this._send(req) .then(response => { if (response.status < 200 && response.status >= 300) { return Promise.reject(new Error('error')); @@ -2367,7 +2724,7 @@ }, deleteAccountSSHKey(id) { - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: '/accounts/self/sshkeys/' + id, anonymizedUrl: '/accounts/self/sshkeys/*', @@ -2375,7 +2732,7 @@ }, getAccountGPGKeys() { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/accounts/self/gpgkeys', reportUrlAsIs: true, }); @@ -2388,7 +2745,7 @@ body: key, reportUrlAsIs: true, }; - return this._restApiHelper.send(req) + return this._send(req) .then(response => { if (response.status < 200 && response.status >= 300) { return Promise.reject(new Error('error')); @@ -2402,7 +2759,7 @@ }, deleteAccountGPGKey(id) { - return this._restApiHelper.send({ + return this._send({ method: 'DELETE', url: '/accounts/self/gpgkeys/' + id, anonymizedUrl: '/accounts/self/gpgkeys/*', @@ -2435,7 +2792,7 @@ body: {token}, reportUrlAsIs: true, }; - return this._restApiHelper.send(req).then(response => { + return this._send(req).then(response => { if (response.status === 204) { return 'Email confirmed successfully.'; } @@ -2444,7 +2801,7 @@ }, getCapabilities(opt_errFn) { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: '/config/server/capabilities', errFn: opt_errFn, reportUrlAsIs: true, @@ -2550,7 +2907,7 @@ */ getChange(changeNum, opt_errFn) { // Cannot use _changeBaseURL, as this function is used by _projectLookup. - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: `/changes/?q=change:${changeNum}`, errFn: opt_errFn, anonymizedUrl: '/changes/?q=change:*', @@ -2610,7 +2967,7 @@ req.endpoint : req.anonymizedEndpoint; return this._changeBaseURL(req.changeNum, req.patchNum).then(url => { - return this._restApiHelper.send({ + return this._send({ method: req.method, url: url + req.endpoint, body: req.body, @@ -2635,7 +2992,7 @@ const anonymizedBaseUrl = req.patchNum ? ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; return this._changeBaseURL(req.changeNum, req.patchNum).then(url => { - return this._restApiHelper.fetchJSON({ + return this._fetchJSON({ url: url + req.endpoint, errFn: req.errFn, params: req.params, @@ -2762,7 +3119,7 @@ }, deleteDraftComments(query) { - return this._restApiHelper.send({ + return this._send({ method: 'POST', url: '/accounts/self/drafts:delete', body: {query}, diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html index ea71522a59..9d0d83ac93 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html @@ -63,8 +63,116 @@ limitations under the License. sandbox.restore(); }); + suite('fetchJSON()', () => { + test('Sets header to accept application/json', () => { + const authFetchStub = sandbox.stub(element._auth, 'fetch') + .returns(Promise.resolve()); + element._fetchJSON({url: '/dummy/url'}); + assert.isTrue(authFetchStub.called); + assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), + 'application/json'); + }); + + test('Use header option accept when provided', () => { + const authFetchStub = sandbox.stub(element._auth, 'fetch') + .returns(Promise.resolve()); + const headers = new Headers(); + headers.append('Accept', '*/*'); + const fetchOptions = {headers}; + element._fetchJSON({url: '/dummy/url', fetchOptions}); + assert.isTrue(authFetchStub.called); + assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), + '*/*'); + }); + }); + + test('JSON prefix is properly removed', done => { + element._fetchJSON({url: '/dummy/url'}).then(obj => { + assert.deepEqual(obj, {hello: 'bonjour'}); + done(); + }); + }); + + test('cached results', done => { + let n = 0; + sandbox.stub(element, '_fetchJSON', () => { + return Promise.resolve(++n); + }); + const promises = []; + promises.push(element._fetchSharedCacheURL('/foo')); + promises.push(element._fetchSharedCacheURL('/foo')); + promises.push(element._fetchSharedCacheURL('/foo')); + + Promise.all(promises).then(results => { + assert.deepEqual(results, [1, 1, 1]); + element._fetchSharedCacheURL('/foo').then(foo => { + assert.equal(foo, 1); + done(); + }); + }); + }); + + test('cached promise', done => { + const promise = Promise.reject(new Error('foo')); + element._cache.set('/foo', promise); + element._fetchSharedCacheURL({url: '/foo'}).catch(p => { + assert.equal(p.message, 'foo'); + done(); + }); + }); + + test('cache invalidation', () => { + element._cache.set('/foo/bar', 1); + element._cache.set('/bar', 2); + element._sharedFetchPromises['/foo/bar'] = 3; + element._sharedFetchPromises['/bar'] = 4; + element._invalidateSharedFetchPromisesPrefix('/foo/'); + assert.isFalse(element._cache.has('/foo/bar')); + assert.isTrue(element._cache.has('/bar')); + assert.isUndefined(element._sharedFetchPromises['/foo/bar']); + assert.strictEqual(4, element._sharedFetchPromises['/bar']); + }); + + test('params are properly encoded', () => { + let url = element._urlWithParams('/path/', { + sp: 'hola', + gr: 'guten tag', + noval: null, + }); + assert.equal(url, + window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval'); + + url = element._urlWithParams('/path/', { + sp: 'hola', + en: ['hey', 'hi'], + }); + assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi'); + + // Order must be maintained with array params. + url = element._urlWithParams('/path/', { + l: ['c', 'b', 'a'], + }); + assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a'); + }); + + test('request callbacks can be canceled', done => { + let cancelCalled = false; + window.fetch.returns(Promise.resolve({ + body: { + cancel() { cancelCalled = true; }, + }, + })); + const cancelCondition = () => { return true; }; + element._fetchJSON({url: '/dummy/url', cancelCondition}).then( + obj => { + assert.isUndefined(obj); + assert.isTrue(cancelCalled); + done(); + }); + }); + test('parent diff comments are properly grouped', done => { - sandbox.stub(element._restApiHelper, 'fetchJSON', () => { + sandbox.stub(element, '_fetchJSON', () => { return Promise.resolve({ '/COMMIT_MSG': [], 'sieve.go': [ @@ -207,7 +315,7 @@ limitations under the License. test('differing patch diff comments are properly grouped', done => { sandbox.stub(element, 'getFromProjectLookup') .returns(Promise.resolve('test')); - sandbox.stub(element._restApiHelper, 'fetchJSON', request => { + sandbox.stub(element, '_fetchJSON', request => { const url = request.url; if (url === '/changes/test~42/revisions/1') { return Promise.resolve({ @@ -324,7 +432,7 @@ limitations under the License. suite('rebase action', () => { let resolve_fetchJSON; setup(() => { - sandbox.stub(element._restApiHelper, 'fetchJSON').returns( + sandbox.stub(element, '_fetchJSON').returns( new Promise(resolve => { resolve_fetchJSON = resolve; })); @@ -359,7 +467,7 @@ limitations under the License. element.addEventListener('server-error', resolve); }); - element._restApiHelper.fetchJSON({}).then(response => { + element._fetchJSON({}).then(response => { assert.isUndefined(response); assert.isTrue(getResponseObjectStub.notCalled); serverErrorEventPromise.then(() => done()); @@ -375,12 +483,12 @@ limitations under the License. Promise.reject(new Error('Failed to fetch'))); window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse)); // Emulate logged in. - element._restApiHelper._cache.set('/accounts/self/detail', {}); + element._cache.set('/accounts/self/detail', {}); const serverErrorStub = sandbox.stub(); element.addEventListener('server-error', serverErrorStub); const authErrorStub = sandbox.stub(); element.addEventListener('auth-error', authErrorStub); - element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => { + element._fetchJSON({url: '/bar'}).finally(r => { flush(() => { assert.isTrue(authErrorStub.called); assert.isFalse(serverErrorStub.called); @@ -399,7 +507,7 @@ limitations under the License. element.addEventListener('server-error', serverErrorStub); const authErrorStub = sandbox.stub(); element.addEventListener('auth-error', authErrorStub); - element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => { + element._fetchJSON({url: '/bar'}).finally(r => { flush(() => { assert.isTrue(authErrorStub.called); assert.isFalse(serverErrorStub.called); @@ -450,8 +558,7 @@ limitations under the License. test('checkCredentials promise rejection', () => { window.fetch.restore(); element._cache.set('/accounts/self/detail', true); - const checkCredentialsSpy = - sandbox.spy(element._restApiHelper, 'checkCredentials'); + sandbox.spy(element, 'checkCredentials'); sandbox.stub(window, 'fetch', url => { return Promise.reject(new Error('Failed to fetch')); }); @@ -463,7 +570,7 @@ limitations under the License. // The second fetch call also fails, which leads to a second // invocation of checkCredentials, which should immediately // return instead of making further fetch calls. - assert.isTrue(checkCredentialsSpy .calledTwice); + assert.isTrue(element.checkCredentials.calledTwice); assert.isTrue(window.fetch.calledTwice); }); }); @@ -478,7 +585,7 @@ limitations under the License. }); test('legacy n,z key in change url is replaced', () => { - const stub = sandbox.stub(element._restApiHelper, 'fetchJSON') + const stub = sandbox.stub(element, '_fetchJSON') .returns(Promise.resolve([])); element.getChanges(1, null, 'n,z'); assert.equal(stub.lastCall.args[0].params.S, 0); @@ -486,38 +593,38 @@ limitations under the License. test('saveDiffPreferences invalidates cache line', () => { const cacheKey = '/accounts/self/preferences.diff'; - const sendStub = sandbox.stub(element._restApiHelper, 'send'); + sandbox.stub(element, '_send'); element._cache.set(cacheKey, {tab_size: 4}); element.saveDiffPreferences({tab_size: 8}); - assert.isTrue(sendStub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + assert.isTrue(element._send.called); + assert.isFalse(element._cache.has(cacheKey)); }); test('getAccount when resp is null does not add anything to the cache', done => { const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + const stub = sandbox.stub(element, '_fetchSharedCacheURL', () => Promise.resolve()); element.getAccount().then(() => { - assert.isTrue(stub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + assert.isTrue(element._fetchSharedCacheURL.called); + assert.isFalse(element._cache.has(cacheKey)); done(); }); - element._restApiHelper._cache.set(cacheKey, 'fake cache'); + element._cache.set(cacheKey, 'fake cache'); stub.lastCall.args[0].errFn(); }); test('getAccount does not add to the cache when resp.status is 403', done => { const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + const stub = sandbox.stub(element, '_fetchSharedCacheURL', () => Promise.resolve()); element.getAccount().then(() => { - assert.isTrue(stub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + assert.isTrue(element._fetchSharedCacheURL.called); + assert.isFalse(element._cache.has(cacheKey)); done(); }); element._cache.set(cacheKey, 'fake cache'); @@ -526,15 +633,15 @@ limitations under the License. test('getAccount when resp is successful', done => { const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + const stub = sandbox.stub(element, '_fetchSharedCacheURL', () => Promise.resolve()); element.getAccount().then(response => { - assert.isTrue(stub.called); - assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache'); + assert.isTrue(element._fetchSharedCacheURL.called); + assert.equal(element._cache.get(cacheKey), 'fake cache'); done(); }); - element._restApiHelper._cache.set(cacheKey, 'fake cache'); + element._cache.set(cacheKey, 'fake cache'); stub.lastCall.args[0].errFn({}); }); @@ -546,7 +653,7 @@ limitations under the License. sandbox.stub(element, '_isNarrowScreen', () => { return smallScreen; }); - sandbox.stub(element._restApiHelper, 'fetchCacheURL', () => { + sandbox.stub(element, '_fetchSharedCacheURL', () => { return Promise.resolve(testJSON); }); }; @@ -611,10 +718,10 @@ limitations under the License. }); test('savPreferences normalizes download scheme', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); + sandbox.stub(element, '_send'); element.savePreferences({download_scheme: 'HTTP'}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http'); + assert.isTrue(element._send.called); + assert.equal(element._send.lastCall.args[0].body.download_scheme, 'http'); }); test('getDiffPreferences returns correct defaults', done => { @@ -640,10 +747,10 @@ limitations under the License. }); test('saveDiffPreferences set show_tabs to false', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); + sandbox.stub(element, '_send'); element.saveDiffPreferences({show_tabs: false}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); + assert.isTrue(element._send.called); + assert.equal(element._send.lastCall.args[0].body.show_tabs, false); }); test('getEditPreferences returns correct defaults', done => { @@ -673,36 +780,34 @@ limitations under the License. }); test('saveEditPreferences set show_tabs to false', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); + sandbox.stub(element, '_send'); element.saveEditPreferences({show_tabs: false}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); + assert.isTrue(element._send.called); + assert.equal(element._send.lastCall.args[0].body.show_tabs, false); }); test('confirmEmail', () => { - const sendStub = sandbox.spy(element._restApiHelper, 'send'); + sandbox.spy(element, '_send'); element.confirmEmail('foo'); - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].method, 'PUT'); - assert.equal(sendStub.lastCall.args[0].url, + assert.isTrue(element._send.calledOnce); + assert.equal(element._send.lastCall.args[0].method, 'PUT'); + assert.equal(element._send.lastCall.args[0].url, '/config/server/email.confirm'); - assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'}); + assert.deepEqual(element._send.lastCall.args[0].body, {token: 'foo'}); }); test('setAccountStatus', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send') - .returns(Promise.resolve('OOO')); + sandbox.stub(element, '_send').returns(Promise.resolve('OOO')); element._cache.set('/accounts/self/detail', {}); return element.setAccountStatus('OOO').then(() => { - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].method, 'PUT'); - assert.equal(sendStub.lastCall.args[0].url, + assert.isTrue(element._send.calledOnce); + assert.equal(element._send.lastCall.args[0].method, 'PUT'); + assert.equal(element._send.lastCall.args[0].url, '/accounts/self/status'); - assert.deepEqual(sendStub.lastCall.args[0].body, + assert.deepEqual(element._send.lastCall.args[0].body, + {status: 'OOO'}); + assert.deepEqual(element._cache.get('/accounts/self/detail'), {status: 'OOO'}); - assert.deepEqual(element._restApiHelper - ._cache.get('/accounts/self/detail'), - {status: 'OOO'}); }); }); @@ -791,20 +896,18 @@ limitations under the License. const change_num = '1'; const file_name = 'index.php'; const file_contents = ' { - assert.isTrue(element._restApiHelper.send.calledOnce); - assert.equal(element._restApiHelper.send.lastCall.args[0].method, - 'PUT'); - assert.equal(element._restApiHelper.send.lastCall.args[0].url, + assert.isTrue(element._send.calledOnce); + assert.equal(element._send.lastCall.args[0].method, 'PUT'); + assert.equal(element._send.lastCall.args[0].url, '/changes/test~1/edit/' + file_name); - assert.equal(element._restApiHelper.send.lastCall.args[0].body, - file_contents); + assert.equal(element._send.lastCall.args[0].body, file_contents); }); }); @@ -812,18 +915,17 @@ limitations under the License. element._projectLookup = {1: 'test'}; const change_num = '1'; const message = 'this is a commit message'; - sandbox.stub(element._restApiHelper, 'send').returns( + sandbox.stub(element, '_send').returns( Promise.resolve([change_num, message])); sandbox.stub(element, 'getResponseObject') .returns(Promise.resolve([change_num, message])); element._cache.set('/changes/' + change_num + '/message', {}); return element.putChangeCommitMessage(change_num, message).then(() => { - assert.isTrue(element._restApiHelper.send.calledOnce); - assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT'); - assert.equal(element._restApiHelper.send.lastCall.args[0].url, + assert.isTrue(element._send.calledOnce); + assert.equal(element._send.lastCall.args[0].method, 'PUT'); + assert.equal(element._send.lastCall.args[0].url, '/changes/test~1/message'); - assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body, - {message}); + assert.deepEqual(element._send.lastCall.args[0].body, {message}); }); }); @@ -879,7 +981,7 @@ limitations under the License. }); test('createRepo encodes name', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send') + const sendStub = sandbox.stub(element, '_send') .returns(Promise.resolve()); return element.createRepo({name: 'x/y'}).then(() => { assert.isTrue(sendStub.calledOnce); @@ -925,65 +1027,64 @@ limitations under the License. suite('getRepos', () => { const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only'; - let fetchCacheURLStub; + setup(() => { - fetchCacheURLStub = - sandbox.stub(element._restApiHelper, 'fetchCacheURL'); + sandbox.stub(element, '_fetchSharedCacheURL'); }); test('normal use', () => { element.getRepos('test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=test'); element.getRepos(null, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, `/projects/?n=26&S=0&query=${defaultQuery}`); element.getRepos('test', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=25&query=test'); }); test('with blank', () => { element.getRepos('test/test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest'); }); test('with hyphen', () => { element.getRepos('foo-bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); }); test('with leading hyphen', () => { element.getRepos('-bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Abar'); }); test('with trailing hyphen', () => { element.getRepos('foo-bar-', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); }); test('with underscore', () => { element.getRepos('foo_bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); }); test('with underscore', () => { element.getRepos('foo_bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); }); test('hyphen only', () => { element.getRepos('-', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, `/projects/?n=26&S=0&query=${defaultQuery}`); }); }); @@ -1012,45 +1113,43 @@ limitations under the License. }); suite('getGroups', () => { - let fetchCacheURLStub; setup(() => { - fetchCacheURLStub = - sandbox.stub(element._restApiHelper, 'fetchCacheURL'); + sandbox.stub(element, '_fetchSharedCacheURL'); }); test('normal use', () => { element.getGroups('test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/groups/?n=26&S=0&m=test'); element.getGroups(null, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/groups/?n=26&S=0'); element.getGroups('test', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/groups/?n=26&S=25&m=test'); }); test('regex', () => { element.getGroups('^test.*', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/groups/?n=26&S=0&r=%5Etest.*'); element.getGroups('^test.*', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, + assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url, '/groups/?n=26&S=25&r=%5Etest.*'); }); }); test('gerrit auth is used', () => { sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve()); - element._restApiHelper.fetchJSON({url: 'foo'}); + element._fetchJSON({url: 'foo'}); assert(Gerrit.Auth.fetch.called); }); test('getSuggestedAccounts does not return _fetchJSON', () => { - const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON'); + const _fetchJSONSpy = sandbox.spy(element, '_fetchJSON'); return element.getSuggestedAccounts().then(accts => { assert.isFalse(_fetchJSONSpy.called); assert.equal(accts.length, 0); @@ -1058,7 +1157,7 @@ limitations under the License. }); test('_fetchJSON gets called by getSuggestedAccounts', () => { - const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON', + const _fetchJSONStub = sandbox.stub(element, '_fetchJSON', () => Promise.resolve()); return element.getSuggestedAccounts('own').then(() => { assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, { @@ -1130,7 +1229,7 @@ limitations under the License. const errFn = sinon.stub(); sandbox.stub(element, 'getChangeActionURL') .returns(Promise.resolve('')); - sandbox.stub(element._restApiHelper, 'fetchRawJSON') + sandbox.stub(element, '_fetchRawJSON') .returns(Promise.resolve({ok: false, status: 500})); return element._getChangeDetail(123, '516714', errFn).then(() => { assert.isTrue(errFn.called); @@ -1150,15 +1249,14 @@ limitations under the License. test('_getChangeDetail populates _projectLookup', () => { sandbox.stub(element, 'getChangeActionURL') .returns(Promise.resolve('')); - sandbox.stub(element._restApiHelper, 'fetchRawJSON') + sandbox.stub(element, '_fetchRawJSON') .returns(Promise.resolve({ok: true})); const mockResponse = {_number: 1, project: 'test'}; - sandbox.stub(element._restApiHelper, 'readResponsePayload') - .returns(Promise.resolve({ - parsed: mockResponse, - raw: JSON.stringify(mockResponse), - })); + sandbox.stub(element, '_readResponsePayload').returns(Promise.resolve({ + parsed: mockResponse, + raw: JSON.stringify(mockResponse), + })); return element._getChangeDetail(1, '516714').then(() => { assert.equal(Object.keys(element._projectLookup).length, 1); assert.equal(element._projectLookup[1], 'test'); @@ -1176,8 +1274,7 @@ limitations under the License. const mockResponse = {foo: 'bar', baz: 42}; mockResponseSerial = element.JSON_PREFIX + JSON.stringify(mockResponse); - sandbox.stub(element._restApiHelper, 'urlWithParams') - .returns(requestUrl); + sandbox.stub(element, '_urlWithParams').returns(requestUrl); sandbox.stub(element, 'getChangeActionURL') .returns(Promise.resolve(requestUrl)); collectSpy = sandbox.spy(element._etags, 'collect'); @@ -1185,12 +1282,11 @@ limitations under the License. }); test('contributes to cache', () => { - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ - text: () => Promise.resolve(mockResponseSerial), - status: 200, - ok: true, - })); + sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({ + text: () => Promise.resolve(mockResponseSerial), + status: 200, + ok: true, + })); return element._getChangeDetail(123, '516714').then(detail => { assert.isFalse(getPayloadSpy.called); @@ -1201,12 +1297,11 @@ limitations under the License. }); test('uses cache on HTTP 304', () => { - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ - text: () => Promise.resolve(mockResponseSerial), - status: 304, - ok: true, - })); + sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({ + text: () => Promise.resolve(mockResponseSerial), + status: 304, + ok: true, + })); return element._getChangeDetail(123, {}).then(detail => { assert.isFalse(collectSpy.called); @@ -1251,7 +1346,7 @@ limitations under the License. suite('getChanges populates _projectLookup', () => { test('multiple queries', () => { - sandbox.stub(element._restApiHelper, 'fetchJSON') + sandbox.stub(element, '_fetchJSON') .returns(Promise.resolve([ [ {_number: 1, project: 'test'}, @@ -1271,7 +1366,7 @@ limitations under the License. }); test('no query', () => { - sandbox.stub(element._restApiHelper, 'fetchJSON') + sandbox.stub(element, '_fetchJSON') .returns(Promise.resolve([ {_number: 1, project: 'test'}, {_number: 2, project: 'test'}, @@ -1291,7 +1386,7 @@ limitations under the License. test('_getChangeURLAndFetch', () => { element._projectLookup = {1: 'test'}; - const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON') + const fetchStub = sandbox.stub(element, '_fetchJSON') .returns(Promise.resolve()); const req = {changeNum: 1, endpoint: '/test', patchNum: 1}; return element._getChangeURLAndFetch(req).then(() => { @@ -1302,7 +1397,7 @@ limitations under the License. test('_getChangeURLAndSend', () => { element._projectLookup = {1: 'test'}; - const sendStub = sandbox.stub(element._restApiHelper, 'send') + const sendStub = sandbox.stub(element, '_send') .returns(Promise.resolve()); const req = { @@ -1324,17 +1419,16 @@ limitations under the License. const mockObject = {foo: 'bar', baz: 'foo'}; const serial = element.JSON_PREFIX + JSON.stringify(mockObject); const mockResponse = {text: () => Promise.resolve(serial)}; - return element._restApiHelper.readResponsePayload(mockResponse) - .then(payload => { - assert.deepEqual(payload.parsed, mockObject); - assert.equal(payload.raw, serial); - }); + return element._readResponsePayload(mockResponse).then(payload => { + assert.deepEqual(payload.parsed, mockObject); + assert.equal(payload.raw, serial); + }); }); test('_parsePrefixedJSON', () => { const obj = {x: 3, y: {z: 4}, w: 23}; const serial = element.JSON_PREFIX + JSON.stringify(obj); - const result = element._restApiHelper.parsePrefixedJSON(serial); + const result = element._parsePrefixedJSON(serial); assert.deepEqual(result, obj); }); }); @@ -1356,7 +1450,7 @@ limitations under the License. }); test('generateAccountHttpPassword', () => { - const sendSpy = sandbox.spy(element._restApiHelper, 'send'); + const sendSpy = sandbox.spy(element, '_send'); return element.generateAccountHttpPassword().then(() => { assert.isTrue(sendSpy.calledOnce); assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true}); @@ -1441,12 +1535,11 @@ limitations under the License. }); test('getDashboard', () => { - const fetchCacheURLStub = sandbox.stub(element._restApiHelper, - 'fetchCacheURL'); + const fetchStub = sandbox.stub(element, '_fetchSharedCacheURL'); element.getDashboard('gerrit/project', 'default:main'); - assert.isTrue(fetchCacheURLStub.calledOnce); + assert.isTrue(fetchStub.calledOnce); assert.equal( - fetchCacheURLStub.lastCall.args[0].url, + fetchStub.lastCall.args[0].url, '/projects/gerrit%2Fproject/dashboards/default%3Amain'); }); @@ -1514,7 +1607,7 @@ limitations under the License. }); test('_fetch forwards request and logs', () => { - const logStub = sandbox.stub(element._restApiHelper, '_logCall'); + const logStub = sandbox.stub(element, '_logCall'); const response = {status: 404, text: sinon.stub()}; const url = 'my url'; const fetchOptions = {method: 'DELETE'}; @@ -1522,7 +1615,7 @@ limitations under the License. const startTime = 123; sandbox.stub(Date, 'now').returns(startTime); const req = {url, fetchOptions}; - return element._restApiHelper.fetch(req).then(() => { + return element._fetch(req).then(() => { assert.isTrue(logStub.calledOnce); assert.isTrue(logStub.calledWith(req, startTime, response.status)); assert.isFalse(response.text.called); @@ -1534,11 +1627,10 @@ limitations under the License. const handler = sinon.stub(); element.addEventListener('rpc-log', handler); - element._restApiHelper._logCall({url: 'url'}, 100, 200); + element._logCall({url: 'url'}, 100, 200); assert.isFalse(handler.called); - element._restApiHelper - ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200); + element._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200); flushAsynchronousOperations(); assert.isTrue(handler.calledOnce); }); @@ -1547,7 +1639,7 @@ limitations under the License. sandbox.stub(element, 'getFromProjectLookup') .returns(Promise.resolve('test')); const sendStub = - sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve()); + sandbox.stub(element, '_send').returns(Promise.resolve()); await element.saveChangeStarred(123, true); assert.isTrue(sendStub.calledOnce); diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js deleted file mode 100644 index d42abc351d..0000000000 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js +++ /dev/null @@ -1,456 +0,0 @@ -/** - * @license - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -(function(window) { - 'use strict'; - - const Defs = {}; - - /** - * @typedef {{ - * url: string, - * fetchOptions: (Object|null|undefined), - * anonymizedUrl: (string|undefined), - * }} - */ - Defs.FetchRequest; - - /** - * Object to describe a request for passing into fetchJSON or fetchRawJSON. - * - url is the URL for the request (excluding get params) - * - errFn is a function to invoke when the request fails. - * - cancelCondition is a function that, if provided and returns true, will - * cancel the response after it resolves. - * - params is a key-value hash to specify get params for the request URL. - * @typedef {{ - * url: string, - * errFn: (function(?Response, string=)|null|undefined), - * cancelCondition: (function()|null|undefined), - * params: (Object|null|undefined), - * fetchOptions: (Object|null|undefined), - * anonymizedUrl: (string|undefined), - * reportUrlAsIs: (boolean|undefined), - * }} - */ - Defs.FetchJSONRequest; - - const JSON_PREFIX = ')]}\''; - const FAILED_TO_FETCH_ERROR = 'Failed to fetch'; - - /** - * Wrapper around Map for caching server responses. Site-based so that - * changes to CANONICAL_PATH will result in a different cache going into - * effect. - */ - class SiteBasedCache { - constructor() { - // Container of per-canonical-path caches. - this._data = new Map(); - if (window.INITIAL_DATA != undefined) { - // Put all data shipped with index.html into the cache. This makes it - // so that we spare more round trips to the server when the app loads - // initially. - Object - .entries(window.INITIAL_DATA) - .forEach(e => this._cache().set(e[0], e[1])); - } - } - - // Returns the cache for the current canonical path. - _cache() { - if (!this._data.has(window.CANONICAL_PATH)) { - this._data.set(window.CANONICAL_PATH, new Map()); - } - return this._data.get(window.CANONICAL_PATH); - } - - has(key) { - return this._cache().has(key); - } - - get(key) { - return this._cache().get(key); - } - - set(key, value) { - this._cache().set(key, value); - } - - delete(key) { - this._cache().delete(key); - } - - invalidatePrefix(prefix) { - const newMap = new Map(); - for (const [key, value] of this._cache().entries()) { - if (!key.startsWith(prefix)) { - newMap.set(key, value); - } - } - this._data.set(window.CANONICAL_PATH, newMap); - } - } - - class FetchPromisesCache { - constructor() { - this._data = {}; - } - - has(key) { - return !!this._data[key]; - } - - get(key) { - return this._data[key]; - } - - set(key, value) { - this._data[key] = value; - } - - invalidatePrefix(prefix) { - const newData = {}; - Object.entries(this._data).forEach(([key, value]) => { - if (!key.startsWith(prefix)) { - newData[key] = value; - } - }); - this._data = newData; - } - } - - class GrRestApiHelper { - /** - * @param {SiteBasedCache} cache - * @param {object} auth - * @param {FetchPromisesCache} fetchPromisesCache - * @param {object} credentialCheck - * @param {object} restApiInterface - */ - constructor(cache, auth, fetchPromisesCache, credentialCheck, - restApiInterface) { - this._cache = cache;// TODO: make it public - this._auth = auth; - this._fetchPromisesCache = fetchPromisesCache; - this._credentialCheck = credentialCheck; - this._restApiInterface = restApiInterface; - } - - /** - * Wraps calls to the underlying authenticated fetch function (_auth.fetch) - * with timing and logging. - * @param {Defs.FetchRequest} req - */ - fetch(req) { - const start = Date.now(); - const xhr = this._auth.fetch(req.url, req.fetchOptions); - - // Log the call after it completes. - xhr.then(res => this._logCall(req, start, res ? res.status : null)); - - // Return the XHR directly (without the log). - return xhr; - } - - /** - * Log information about a REST call. Because the elapsed time is determined - * by this method, it should be called immediately after the request - * finishes. - * @param {Defs.FetchRequest} req - * @param {number} startTime the time that the request was started. - * @param {number} status the HTTP status of the response. The status value - * is used here rather than the response object so there is no way this - * method can read the body stream. - */ - _logCall(req, startTime, status) { - const method = (req.fetchOptions && req.fetchOptions.method) ? - req.fetchOptions.method : 'GET'; - const endTime = Date.now(); - const elapsed = (endTime - startTime); - const startAt = new Date(startTime); - const endAt = new Date(endTime); - console.log([ - 'HTTP', - status, - method, - elapsed + 'ms', - req.anonymizedUrl || req.url, - `(${startAt.toISOString()}, ${endAt.toISOString()})`, - ].join(' ')); - if (req.anonymizedUrl) { - this.fire('rpc-log', - {status, method, elapsed, anonymizedUrl: req.anonymizedUrl}); - } - } - - /** - * Fetch JSON from url provided. - * Returns a Promise that resolves to a native Response. - * Doesn't do error checking. Supports cancel condition. Performs auth. - * Validates auth expiry errors. - * @param {Defs.FetchJSONRequest} req - */ - fetchRawJSON(req) { - const urlWithParams = this.urlWithParams(req.url, req.params); - const fetchReq = { - url: urlWithParams, - fetchOptions: req.fetchOptions, - anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl, - }; - return this.fetch(fetchReq).then(res => { - if (req.cancelCondition && req.cancelCondition()) { - res.body.cancel(); - return; - } - return res; - }).catch(err => { - const isLoggedIn = !!this._cache.get('/accounts/self/detail'); - if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) { - this.checkCredentials(); - } else { - if (req.errFn) { - req.errFn.call(undefined, null, err); - } else { - this.fire('network-error', {error: err}); - } - } - throw err; - }); - } - - /** - * Fetch JSON from url provided. - * Returns a Promise that resolves to a parsed response. - * Same as {@link fetchRawJSON}, plus error handling. - * @param {Defs.FetchJSONRequest} req - */ - fetchJSON(req) { - req = this.addAcceptJsonHeader(req); - return this.fetchRawJSON(req).then(response => { - if (!response) { - return; - } - if (!response.ok) { - if (req.errFn) { - req.errFn.call(null, response); - return; - } - this.fire('server-error', {request: req, response}); - return; - } - return response && this.getResponseObject(response); - }); - } - - /** - * @param {string} url - * @param {?Object|string=} opt_params URL params, key-value hash. - * @return {string} - */ - urlWithParams(url, opt_params) { - if (!opt_params) { return this.getBaseUrl() + url; } - - const params = []; - for (const p in opt_params) { - if (!opt_params.hasOwnProperty(p)) { continue; } - if (opt_params[p] == null) { - params.push(encodeURIComponent(p)); - continue; - } - for (const value of [].concat(opt_params[p])) { - params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`); - } - } - return this.getBaseUrl() + url + '?' + params.join('&'); - } - - /** - * @param {!Object} response - * @return {?} - */ - getResponseObject(response) { - return this.readResponsePayload(response) - .then(payload => payload.parsed); - } - - /** - * @param {!Object} response - * @return {!Object} - */ - readResponsePayload(response) { - return response.text().then(text => { - let result; - try { - result = this.parsePrefixedJSON(text); - } catch (_) { - result = null; - } - return {parsed: result, raw: text}; - }); - } - - /** - * @param {string} source - * @return {?} - */ - parsePrefixedJSON(source) { - return JSON.parse(source.substring(JSON_PREFIX.length)); - } - - /** - * @param {Defs.FetchJSONRequest} req - * @return {Defs.FetchJSONRequest} - */ - addAcceptJsonHeader(req) { - if (!req.fetchOptions) req.fetchOptions = {}; - if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers(); - if (!req.fetchOptions.headers.has('Accept')) { - req.fetchOptions.headers.append('Accept', 'application/json'); - } - return req; - } - - getBaseUrl() { - return this._restApiInterface.getBaseUrl(); - } - - fire(type, detail, options) { - return this._restApiInterface.fire(type, detail, options); - } - - /** - * @param {Defs.FetchJSONRequest} req - */ - fetchCacheURL(req) { - if (this._fetchPromisesCache.has(req.url)) { - return this._fetchPromisesCache.get(req.url); - } - // TODO(andybons): Periodic cache invalidation. - if (this._cache.has(req.url)) { - return Promise.resolve(this._cache.get(req.url)); - } - this._fetchPromisesCache.set(req.url, - this.fetchJSON(req).then(response => { - if (response !== undefined) { - this._cache.set(req.url, response); - } - this._fetchPromisesCache.set(req.url, undefined); - return response; - }).catch(err => { - this._fetchPromisesCache.set(req.url, undefined); - throw err; - }) - ); - return this._fetchPromisesCache.get(req.url); - } - - /** - * Send an XHR. - * @param {Defs.SendRequest} req - * @return {Promise} - */ - send(req) { - const options = {method: req.method}; - if (req.body) { - options.headers = new Headers(); - options.headers.set( - 'Content-Type', req.contentType || 'application/json'); - options.body = typeof req.body === 'string' ? - req.body : JSON.stringify(req.body); - } - if (req.headers) { - if (!options.headers) { options.headers = new Headers(); } - for (const header in req.headers) { - if (!req.headers.hasOwnProperty(header)) { continue; } - options.headers.set(header, req.headers[header]); - } - } - const url = req.url.startsWith('http') ? - req.url : this.getBaseUrl() + req.url; - const fetchReq = { - url, - fetchOptions: options, - anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl, - }; - const xhr = this.fetch(fetchReq).then(response => { - if (!response.ok) { - if (req.errFn) { - return req.errFn.call(undefined, response); - } - this.fire('server-error', {request: fetchReq, response}); - } - return response; - }).catch(err => { - this.fire('network-error', {error: err}); - if (req.errFn) { - return req.errFn.call(undefined, null, err); - } else { - throw err; - } - }); - - if (req.parseResponse) { - return xhr.then(res => this.getResponseObject(res)); - } - - return xhr; - } - - checkCredentials() { - if (this._credentialCheck.checking) { - return; - } - this._credentialCheck.checking = true; - let req = {url: '/accounts/self/detail', reportUrlAsIs: true}; - req = this.addAcceptJsonHeader(req); - // Skip the REST response cache. - return this.fetchRawJSON(req).then(res => { - if (!res) { return; } - if (res.status === 403) { - this.fire('auth-error'); - this._cache.delete('/accounts/self/detail'); - } else if (res.ok) { - return this.getResponseObject(res); - } - }).then(res => { - this._credentialCheck.checking = false; - if (res) { - this._cache.set('/accounts/self/detail', res); - } - return res; - }).catch(err => { - this._credentialCheck.checking = false; - if (err && err.message === FAILED_TO_FETCH_ERROR) { - this.fire('auth-error'); - this._cache.delete('/accounts/self/detail'); - } - }); - } - - /** - * @param {string} prefix - */ - invalidateFetchPromisesPrefix(prefix) { - this._fetchPromisesCache.invalidatePrefix(prefix); - this._cache.invalidatePrefix(prefix); - } - } - - window.SiteBasedCache = SiteBasedCache; - window.FetchPromisesCache = FetchPromisesCache; - window.GrRestApiHelper = GrRestApiHelper; -})(window); - diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html deleted file mode 100644 index 4eaf1bcdcd..0000000000 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - -gr-rest-api-helper - - - - - - - - - - - - - diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js index ac0dbebabc..3de6227733 100644 --- a/polygerrit-ui/app/template_test_srcs/template_test.js +++ b/polygerrit-ui/app/template_test_srcs/template_test.js @@ -35,9 +35,6 @@ const EXTERN_NAMES = [ 'GrReviewerUpdatesParser', 'GrCountStringFormatter', 'GrThemeApi', - 'SiteBasedCache', - 'FetchPromisesCache', - 'GrRestApiHelper', 'moment', 'page', 'util', diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 9c061134e7..0fbc8f1388 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -187,7 +187,6 @@ limitations under the License. 'shared/gr-rest-api-interface/gr-auth_test.html', 'shared/gr-rest-api-interface/gr-rest-api-interface_test.html', 'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html', - 'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html', 'shared/gr-select/gr-select_test.html', 'shared/gr-storage/gr-storage_test.html', 'shared/gr-textarea/gr-textarea_test.html',