Revert "Refactor gr-rest-api-interface - extract common methods to helper"
This reverts commit e2d84b21ad953e48cc9a7c702da78fd766c1b4da.
Reason for revert: document.createElement('gr-rest-api-interface').send(...) doesn't work in Polymer 2, because the ready() method of gr-rest-api-interface calls later.
Change-Id: I42234c2e2e6801b144e4d1db33e9d7929ace219e
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 7461ac4..87ea02b 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 @@
<dom-module id="gr-rest-api-interface">
<!-- NB: Order is important, because of namespaced classes. -->
- <script src="gr-rest-apis/gr-rest-api-helper.js"></script>
<script src="gr-auth.js"></script>
<script src="gr-reviewer-updates-parser.js"></script>
<script src="gr-rest-api-interface.js"></script>
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 d634f14..9b63f75 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
@@ -29,6 +29,34 @@
/**
* @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),
* endpoint: string,
* patchNum: (string|number|null|undefined),
@@ -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<!Object>}
*/
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 ea71522..9d0d83a 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
});
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 @@
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 @@
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 @@
sandbox.stub(element, '_isNarrowScreen', () => {
return smallScreen;
});
- sandbox.stub(element._restApiHelper, 'fetchCacheURL', () => {
+ sandbox.stub(element, '_fetchSharedCacheURL', () => {
return Promise.resolve(testJSON);
});
};
@@ -611,10 +718,10 @@
});
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 @@
});
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 @@
});
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._restApiHelper
- ._cache.get('/accounts/self/detail'),
- {status: 'OOO'});
+ assert.deepEqual(element._cache.get('/accounts/self/detail'),
+ {status: 'OOO'});
});
});
@@ -791,20 +896,18 @@
const change_num = '1';
const file_name = 'index.php';
const file_contents = '<?php';
- sandbox.stub(element._restApiHelper, 'send').returns(
+ sandbox.stub(element, '_send').returns(
Promise.resolve([change_num, file_name, file_contents]));
sandbox.stub(element, 'getResponseObject')
.returns(Promise.resolve([change_num, file_name, file_contents]));
element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
return element.saveChangeEdit(change_num, file_name, file_contents)
.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/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 @@
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 @@
});
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 @@
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 @@
});
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 @@
});
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 @@
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 @@
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 @@
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 @@
});
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 @@
});
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 @@
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 @@
});
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 @@
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 @@
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 @@
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 @@
});
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 @@
});
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 @@
});
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 @@
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 @@
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 @@
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 d42abc3..0000000
--- 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 4eaf1bc..0000000
--- 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 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-rest-api-helper</title>
-<script src="/test/common-test-setup.js"></script>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../../test/common-test-setup.html"/>
-<script src="../../../../scripts/util.js"></script>
-<script src="../gr-auth.js"></script>
-<script src="gr-rest-api-helper.js"></script>
-
-<script>void(0);</script>
-
-<script>
- suite('gr-rest-api-helper tests', () => {
- let helper;
- let sandbox;
- let cache;
- let fetchPromisesCache;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- cache = new SiteBasedCache();
- fetchPromisesCache = new FetchPromisesCache();
- const credentialCheck = {checking: false};
-
- window.CANONICAL_PATH = 'testhelper';
-
- const mockRestApiInterface = {
- getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
- fire: sinon.stub(),
- };
-
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sandbox.stub(window, 'fetch').returns(Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- }));
-
- helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
- credentialCheck, mockRestApiInterface);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('fetchJSON()', () => {
- test('Sets header to accept application/json', () => {
- const authFetchStub = sandbox.stub(helper._auth, 'fetch')
- .returns(Promise.resolve());
- helper.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(helper._auth, 'fetch')
- .returns(Promise.resolve());
- const headers = new Headers();
- headers.append('Accept', '*/*');
- const fetchOptions = {headers};
- helper.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 => {
- helper.fetchJSON({url: '/dummy/url'}).then(obj => {
- assert.deepEqual(obj, {hello: 'bonjour'});
- done();
- });
- });
-
- test('cached results', done => {
- let n = 0;
- sandbox.stub(helper, 'fetchJSON', () => {
- return Promise.resolve(++n);
- });
- const promises = [];
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
-
- Promise.all(promises).then(results => {
- assert.deepEqual(results, [1, 1, 1]);
- helper.fetchCacheURL('/foo').then(foo => {
- assert.equal(foo, 1);
- done();
- });
- });
- });
-
- test('cached promise', done => {
- const promise = Promise.reject(new Error('foo'));
- cache.set('/foo', promise);
- helper.fetchCacheURL({url: '/foo'}).catch(p => {
- assert.equal(p.message, 'foo');
- done();
- });
- });
-
- test('cache invalidation', () => {
- cache.set('/foo/bar', 1);
- cache.set('/bar', 2);
- fetchPromisesCache.set('/foo/bar', 3);
- fetchPromisesCache.set('/bar', 4);
- helper.invalidateFetchPromisesPrefix('/foo/');
- assert.isFalse(cache.has('/foo/bar'));
- assert.isTrue(cache.has('/bar'));
- assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
- assert.strictEqual(4, fetchPromisesCache.get('/bar'));
- });
-
- test('params are properly encoded', () => {
- let url = helper.urlWithParams('/path/', {
- sp: 'hola',
- gr: 'guten tag',
- noval: null,
- });
- assert.equal(url,
- window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
-
- url = helper.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 = helper.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; };
- helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
- obj => {
- assert.isUndefined(obj);
- assert.isTrue(cancelCalled);
- done();
- });
- });
- });
-</script>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index ac0dbeb..3de6227 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 @@
'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 9c06113..0fbc8f1 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -187,7 +187,6 @@
'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',