Revert "Refactor gr-rest-api-interface - extract common methods to helper"

This reverts commit e2d84b21ad.

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
This commit is contained in:
Dmitrii Filippov
2019-09-11 12:21:50 +00:00
parent e2d84b21ad
commit 07c6c0c2ec
7 changed files with 668 additions and 857 deletions

View File

@@ -29,7 +29,6 @@ limitations under the License.
<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>

View File

@@ -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,35 +780,33 @@ 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._restApiHelper
._cache.get('/accounts/self/detail'),
assert.deepEqual(element._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 = '<?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 @@ 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,12 +1249,11 @@ 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({
sandbox.stub(element, '_readResponsePayload').returns(Promise.resolve({
parsed: mockResponse,
raw: JSON.stringify(mockResponse),
}));
@@ -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,8 +1282,7 @@ limitations under the License.
});
test('contributes to cache', () => {
sandbox.stub(element._restApiHelper, 'fetchRawJSON')
.returns(Promise.resolve({
sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
text: () => Promise.resolve(mockResponseSerial),
status: 200,
ok: true,
@@ -1201,8 +1297,7 @@ limitations under the License.
});
test('uses cache on HTTP 304', () => {
sandbox.stub(element._restApiHelper, 'fetchRawJSON')
.returns(Promise.resolve({
sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
text: () => Promise.resolve(mockResponseSerial),
status: 304,
ok: true,
@@ -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,8 +1419,7 @@ 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 => {
return element._readResponsePayload(mockResponse).then(payload => {
assert.deepEqual(payload.parsed, mockObject);
assert.equal(payload.raw, serial);
});
@@ -1334,7 +1428,7 @@ limitations under the License.
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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -35,9 +35,6 @@ const EXTERN_NAMES = [
'GrReviewerUpdatesParser',
'GrCountStringFormatter',
'GrThemeApi',
'SiteBasedCache',
'FetchPromisesCache',
'GrRestApiHelper',
'moment',
'page',
'util',

View File

@@ -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',