Convert gr-auth to class and use new API for auth check
auth-check API is available since: https://gerrit-review.googlesource.com/c/gerrit/+/185990 Change-Id: Icd5e0183ee42e746c32bfd7929af9796ab752627
This commit is contained in:
@@ -23,6 +23,9 @@ limitations under the License.
|
|||||||
<link rel="import" href="../../shared/gr-alert/gr-alert.html">
|
<link rel="import" href="../../shared/gr-alert/gr-alert.html">
|
||||||
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
|
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
|
||||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||||
|
<!-- Import to get Gerrit interface -->
|
||||||
|
<!-- TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface -->
|
||||||
|
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
|
||||||
|
|
||||||
<dom-module id="gr-error-manager">
|
<dom-module id="gr-error-manager">
|
||||||
<template>
|
<template>
|
||||||
@@ -33,6 +36,13 @@ limitations under the License.
|
|||||||
confirm-label="Dismiss"
|
confirm-label="Dismiss"
|
||||||
confirm-on-enter></gr-error-dialog>
|
confirm-on-enter></gr-error-dialog>
|
||||||
</gr-overlay>
|
</gr-overlay>
|
||||||
|
<gr-overlay
|
||||||
|
id="noInteractionOverlay"
|
||||||
|
with-backdrop
|
||||||
|
always-on-top
|
||||||
|
no-cancel-on-esc-key
|
||||||
|
no-cancel-on-outside-click>
|
||||||
|
</gr-overlay>
|
||||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||||
<gr-reporting id="reporting"></gr-reporting>
|
<gr-reporting id="reporting"></gr-reporting>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -64,15 +64,29 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @type {!Gerrit.Auth} */
|
||||||
|
this._authService = Gerrit.Auth;
|
||||||
|
|
||||||
|
/** @type {?Function} */
|
||||||
|
this._authErrorHandlerDeregistrationHook;
|
||||||
|
}
|
||||||
|
|
||||||
attached() {
|
attached() {
|
||||||
super.attached();
|
super.attached();
|
||||||
this.listen(document, 'server-error', '_handleServerError');
|
this.listen(document, 'server-error', '_handleServerError');
|
||||||
this.listen(document, 'network-error', '_handleNetworkError');
|
this.listen(document, 'network-error', '_handleNetworkError');
|
||||||
this.listen(document, 'auth-error', '_handleAuthError');
|
|
||||||
this.listen(document, 'show-alert', '_handleShowAlert');
|
this.listen(document, 'show-alert', '_handleShowAlert');
|
||||||
this.listen(document, 'show-error', '_handleShowErrorDialog');
|
this.listen(document, 'show-error', '_handleShowErrorDialog');
|
||||||
this.listen(document, 'visibilitychange', '_handleVisibilityChange');
|
this.listen(document, 'visibilitychange', '_handleVisibilityChange');
|
||||||
this.listen(document, 'show-auth-required', '_handleAuthRequired');
|
this.listen(document, 'show-auth-required', '_handleAuthRequired');
|
||||||
|
|
||||||
|
this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error',
|
||||||
|
event => {
|
||||||
|
this._handleAuthError(event.message, event.action);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
detached() {
|
detached() {
|
||||||
@@ -80,10 +94,11 @@
|
|||||||
this._clearHideAlertHandle();
|
this._clearHideAlertHandle();
|
||||||
this.unlisten(document, 'server-error', '_handleServerError');
|
this.unlisten(document, 'server-error', '_handleServerError');
|
||||||
this.unlisten(document, 'network-error', '_handleNetworkError');
|
this.unlisten(document, 'network-error', '_handleNetworkError');
|
||||||
this.unlisten(document, 'auth-error', '_handleAuthError');
|
|
||||||
this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
|
this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
|
||||||
this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
|
this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
|
||||||
this.unlisten(document, 'show-error', '_handleShowErrorDialog');
|
this.unlisten(document, 'show-error', '_handleShowErrorDialog');
|
||||||
|
|
||||||
|
this._authErrorHandlerDeregistrationHook();
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldSuppressError(msg) {
|
_shouldSuppressError(msg) {
|
||||||
@@ -95,32 +110,41 @@
|
|||||||
'Log in is required to perform that action.', 'Log in.');
|
'Log in is required to perform that action.', 'Log in.');
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAuthError() {
|
_handleAuthError(msg, action) {
|
||||||
this._showAuthErrorAlert('Auth error', 'Refresh credentials.');
|
this.$.noInteractionOverlay.open().then(() => {
|
||||||
|
this._showAuthErrorAlert(msg, action);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleServerError(e) {
|
_handleServerError(e) {
|
||||||
const {request, response} = e.detail;
|
const {request, response} = e.detail;
|
||||||
Promise.all([response.text(), this._getLoggedIn()])
|
response.text().then(errorText => {
|
||||||
.then(([errorText, loggedIn]) => {
|
const url = request && (request.anonymizedUrl || request.url);
|
||||||
const url = request && (request.anonymizedUrl || request.url);
|
const {status, statusText} = response;
|
||||||
const {status, statusText} = response;
|
if (response.status === 403
|
||||||
if (response.status === 403 &&
|
&& !this._authService.isAuthed
|
||||||
loggedIn &&
|
&& errorText === AUTHENTICATION_REQUIRED) {
|
||||||
errorText === AUTHENTICATION_REQUIRED) {
|
// if not authed previously, this is trying to access auth required APIs
|
||||||
// The app was logged at one point and is now getting auth errors.
|
// show auth required alert
|
||||||
// This indicates the auth token is no longer valid.
|
this._handleAuthRequired();
|
||||||
this._handleAuthError();
|
} else if (response.status === 403
|
||||||
} else if (!this._shouldSuppressError(errorText)) {
|
&& this._authService.isAuthed
|
||||||
this._showErrorDialog(this._constructServerErrorMsg({
|
&& errorText === AUTHENTICATION_REQUIRED) {
|
||||||
status,
|
// The app was logged at one point and is now getting auth errors.
|
||||||
statusText,
|
// This indicates the auth token may no longer valid.
|
||||||
errorText,
|
// Re-check on auth
|
||||||
url,
|
this._authService.clearCache();
|
||||||
}));
|
this.$.restAPI.getLoggedIn();
|
||||||
}
|
} else if (!this._shouldSuppressError(errorText)) {
|
||||||
console.error(errorText);
|
this._showErrorDialog(this._constructServerErrorMsg({
|
||||||
});
|
status,
|
||||||
|
statusText,
|
||||||
|
errorText,
|
||||||
|
url,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
console.log(`server error: ${errorText}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_constructServerErrorMsg({errorText, status, statusText, url}) {
|
_constructServerErrorMsg({errorText, status, statusText, url}) {
|
||||||
@@ -142,10 +166,6 @@
|
|||||||
console.error(e.detail.error.message);
|
console.error(e.detail.error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getLoggedIn() {
|
|
||||||
return this.$.restAPI.getLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @param {?string=} opt_actionText
|
* @param {?string=} opt_actionText
|
||||||
@@ -222,7 +242,11 @@
|
|||||||
this.knownAccountId !== undefined &&
|
this.knownAccountId !== undefined &&
|
||||||
timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
|
timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
|
||||||
this._lastCredentialCheck = Date.now();
|
this._lastCredentialCheck = Date.now();
|
||||||
this.$.restAPI.checkCredentials();
|
|
||||||
|
// check auth status in case:
|
||||||
|
// - user signed out
|
||||||
|
// - user switched account
|
||||||
|
this._checkSignedIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,22 +256,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_checkSignedIn() {
|
_checkSignedIn() {
|
||||||
this.$.restAPI.checkCredentials().then(account => {
|
this._lastCredentialCheck = Date.now();
|
||||||
const isLoggedIn = !!account;
|
|
||||||
this._lastCredentialCheck = Date.now();
|
|
||||||
if (this._refreshingCredentials) {
|
|
||||||
if (isLoggedIn) {
|
|
||||||
// If the credentials were refreshed but the account is different
|
|
||||||
// then reload the page completely.
|
|
||||||
if (account._account_id !== this.knownAccountId) {
|
|
||||||
this._reloadPage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._handleCredentialRefreshed();
|
// force to refetch account info
|
||||||
} else {
|
this.$.restAPI.invalidateAccountsCache();
|
||||||
this._requestCheckLoggedIn();
|
this._authService.clearCache();
|
||||||
}
|
|
||||||
|
this.$.restAPI.getLoggedIn().then(isLoggedIn => {
|
||||||
|
// do nothing if its refreshing
|
||||||
|
if (!this._refreshingCredentials) return;
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
// check later
|
||||||
|
// 1. guest mode
|
||||||
|
// 2. or signed out
|
||||||
|
// in case #2, auth-error is taken care of separately
|
||||||
|
this._requestCheckLoggedIn();
|
||||||
|
} else {
|
||||||
|
// check account
|
||||||
|
this.$.restAPI.getAccount().then(account => {
|
||||||
|
if (this._refreshingCredentials) {
|
||||||
|
// If the credentials were refreshed but the account is different
|
||||||
|
// then reload the page completely.
|
||||||
|
if (account._account_id !== this.knownAccountId) {
|
||||||
|
this._reloadPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._handleCredentialRefreshed();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -277,6 +315,10 @@
|
|||||||
this._refreshingCredentials = false;
|
this._refreshingCredentials = false;
|
||||||
this._hideAlert();
|
this._hideAlert();
|
||||||
this._showAlert('Credentials refreshed.');
|
this._showAlert('Credentials refreshed.');
|
||||||
|
this.$.noInteractionOverlay.close();
|
||||||
|
|
||||||
|
// Clear the cache for auth
|
||||||
|
this._authService.clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleWindowFocus() {
|
_handleWindowFocus() {
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ limitations under the License.
|
|||||||
|
|
||||||
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
|
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
|
||||||
<script src="/bower_components/web-component-tester/browser.js"></script>
|
<script src="/bower_components/web-component-tester/browser.js"></script>
|
||||||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
<link rel="import" href="../../../test/common-test-setup.html" />
|
||||||
<link rel="import" href="gr-error-manager.html">
|
<link rel="import" href="gr-error-manager.html">
|
||||||
|
|
||||||
<script>void(0);</script>
|
<script>void (0);</script>
|
||||||
|
|
||||||
<test-fixture id="basic">
|
<test-fixture id="basic">
|
||||||
<template>
|
<template>
|
||||||
@@ -41,285 +41,324 @@ limitations under the License.
|
|||||||
|
|
||||||
setup(() => {
|
setup(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
stub('gr-rest-api-interface', {
|
|
||||||
getLoggedIn() { return Promise.resolve(true); },
|
|
||||||
});
|
|
||||||
element = fixture('basic');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(() => {
|
teardown(() => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not show auth error on 403 by default', done => {
|
suite('when authed', () => {
|
||||||
const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
|
setup(() => {
|
||||||
const responseText = Promise.resolve('server says no.');
|
sandbox.stub(window, 'fetch')
|
||||||
element.fire('server-error',
|
.returns(Promise.resolve({ok: true, status: 204}));
|
||||||
{response: {status: 403, text() { return responseText; }}}
|
element = fixture('basic');
|
||||||
);
|
|
||||||
Promise.all([
|
|
||||||
element.$.restAPI.getLoggedIn.lastCall.returnValue,
|
|
||||||
responseText,
|
|
||||||
]).then(() => {
|
|
||||||
assert.isFalse(showAuthErrorStub.calledOnce);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test('shows auth error on 403 and Authentication required', done => {
|
test('does not show auth error on 403 by default', done => {
|
||||||
const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
|
const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
|
||||||
const responseText = Promise.resolve('Authentication required\n');
|
const responseText = Promise.resolve('server says no.');
|
||||||
element.fire('server-error',
|
element.fire('server-error',
|
||||||
{response: {status: 403, text() { return responseText; }}}
|
{response: {status: 403, text() { return responseText; }}}
|
||||||
);
|
);
|
||||||
Promise.all([
|
|
||||||
element.$.restAPI.getLoggedIn.lastCall.returnValue,
|
|
||||||
responseText,
|
|
||||||
]).then(() => {
|
|
||||||
assert.isTrue(showAuthErrorStub.calledOnce);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show logged in error', () => {
|
|
||||||
sandbox.stub(element, '_showAuthErrorAlert');
|
|
||||||
element.fire('show-auth-required');
|
|
||||||
assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
|
|
||||||
'Log in is required to perform that action.', 'Log in.'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show normal Error', done => {
|
|
||||||
const showErrorStub = sandbox.stub(element, '_showErrorDialog');
|
|
||||||
const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); });
|
|
||||||
element.fire('server-error', {response: {status: 500, text: textSpy}});
|
|
||||||
|
|
||||||
assert.isTrue(textSpy.called);
|
|
||||||
Promise.all([
|
|
||||||
element.$.restAPI.getLoggedIn.lastCall.returnValue,
|
|
||||||
textSpy.lastCall.returnValue,
|
|
||||||
]).then(() => {
|
|
||||||
assert.isTrue(showErrorStub.calledOnce);
|
|
||||||
assert.isTrue(showErrorStub.lastCall.calledWithExactly(
|
|
||||||
'Error 500: ZOMG'));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('_constructServerErrorMsg', () => {
|
|
||||||
const errorText = 'change conflicts';
|
|
||||||
const status = 409;
|
|
||||||
const statusText = 'Conflict';
|
|
||||||
const url = '/my/test/url';
|
|
||||||
|
|
||||||
assert.equal(element._constructServerErrorMsg({status}),
|
|
||||||
'Error 409');
|
|
||||||
assert.equal(element._constructServerErrorMsg({status, url}),
|
|
||||||
'Error 409: \nEndpoint: /my/test/url');
|
|
||||||
assert.equal(element._constructServerErrorMsg({status, statusText, url}),
|
|
||||||
'Error 409 (Conflict): \nEndpoint: /my/test/url');
|
|
||||||
assert.equal(element._constructServerErrorMsg({
|
|
||||||
status,
|
|
||||||
statusText,
|
|
||||||
errorText,
|
|
||||||
url,
|
|
||||||
}), 'Error 409 (Conflict): change conflicts' +
|
|
||||||
'\nEndpoint: /my/test/url');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('suppress TOO_MANY_FILES error', done => {
|
|
||||||
const showAlertStub = sandbox.stub(element, '_showAlert');
|
|
||||||
const textSpy = sandbox.spy(() => {
|
|
||||||
return Promise.resolve('too many files to find conflicts');
|
|
||||||
});
|
|
||||||
element.fire('server-error', {response: {status: 500, text: textSpy}});
|
|
||||||
|
|
||||||
assert.isTrue(textSpy.called);
|
|
||||||
Promise.all([
|
|
||||||
element.$.restAPI.getLoggedIn.lastCall.returnValue,
|
|
||||||
textSpy.lastCall.returnValue,
|
|
||||||
]).then(() => {
|
|
||||||
assert.isFalse(showAlertStub.called);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show network error', done => {
|
|
||||||
const consoleErrorStub = sandbox.stub(console, 'error');
|
|
||||||
const showAlertStub = sandbox.stub(element, '_showAlert');
|
|
||||||
element.fire('network-error', {error: new Error('ZOMG')});
|
|
||||||
flush(() => {
|
|
||||||
assert.isTrue(showAlertStub.calledOnce);
|
|
||||||
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
|
|
||||||
'Server unavailable'));
|
|
||||||
assert.isTrue(consoleErrorStub.calledOnce);
|
|
||||||
assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show auth refresh toast', done => {
|
|
||||||
const refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials',
|
|
||||||
() => { return Promise.resolve(true); });
|
|
||||||
const toastSpy = sandbox.spy(element, '_createToastAlert');
|
|
||||||
const windowOpen = sandbox.stub(window, 'open');
|
|
||||||
const responseText = Promise.resolve('Authentication required\n');
|
|
||||||
element.fire('server-error',
|
|
||||||
{response: {status: 403, text() { return responseText; }}}
|
|
||||||
);
|
|
||||||
Promise.all([
|
|
||||||
element.$.restAPI.getLoggedIn.lastCall.returnValue,
|
|
||||||
responseText,
|
|
||||||
]).then(() => {
|
|
||||||
assert.isTrue(toastSpy.called);
|
|
||||||
let toast = toastSpy.lastCall.returnValue;
|
|
||||||
assert.isOk(toast);
|
|
||||||
assert.include(
|
|
||||||
Polymer.dom(toast.root).textContent, 'Auth error');
|
|
||||||
assert.include(
|
|
||||||
Polymer.dom(toast.root).textContent, 'Refresh credentials.');
|
|
||||||
|
|
||||||
assert.isFalse(windowOpen.called);
|
|
||||||
MockInteractions.tap(toast.$$('gr-button.action'));
|
|
||||||
assert.isTrue(windowOpen.called);
|
|
||||||
|
|
||||||
// @see Issue 5822: noopener breaks closeAfterLogin
|
|
||||||
assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
|
|
||||||
-1);
|
|
||||||
|
|
||||||
const hideToastSpy = sandbox.spy(toast, 'hide');
|
|
||||||
|
|
||||||
element._handleWindowFocus();
|
|
||||||
assert.isTrue(refreshStub.called);
|
|
||||||
element.flushDebouncer('checkLoggedIn');
|
|
||||||
flush(() => {
|
flush(() => {
|
||||||
assert.isTrue(refreshStub.called);
|
assert.isFalse(showAuthErrorStub.calledOnce);
|
||||||
assert.isTrue(hideToastSpy.called);
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
|
test('show auth required for 403 with auth error and not authed before',
|
||||||
toast = toastSpy.lastCall.returnValue;
|
done => {
|
||||||
assert.isOk(toast);
|
const showAuthErrorStub = sandbox.stub(
|
||||||
assert.include(
|
element, '_showAuthErrorAlert'
|
||||||
Polymer.dom(toast.root).textContent, 'Credentials refreshed');
|
);
|
||||||
|
const responseText = Promise.resolve('Authentication required\n');
|
||||||
|
sinon.stub(element.$.restAPI, 'getLoggedIn')
|
||||||
|
.returns(Promise.resolve(true));
|
||||||
|
element.fire('server-error',
|
||||||
|
{response: {status: 403, text() { return responseText; }}}
|
||||||
|
);
|
||||||
|
flush(() => {
|
||||||
|
assert.isTrue(showAuthErrorStub.calledOnce);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recheck auth for 403 with auth error if authed before', done => {
|
||||||
|
// starts with authed state
|
||||||
|
element.$.restAPI.getLoggedIn();
|
||||||
|
const responseText = Promise.resolve('Authentication required\n');
|
||||||
|
sinon.stub(element.$.restAPI, 'getLoggedIn')
|
||||||
|
.returns(Promise.resolve(true));
|
||||||
|
element.fire('server-error',
|
||||||
|
{response: {status: 403, text() { return responseText; }}}
|
||||||
|
);
|
||||||
|
flush(() => {
|
||||||
|
assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show logged in error', () => {
|
||||||
|
sandbox.stub(element, '_showAuthErrorAlert');
|
||||||
|
element.fire('show-auth-required');
|
||||||
|
assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
|
||||||
|
'Log in is required to perform that action.', 'Log in.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show normal Error', done => {
|
||||||
|
const showErrorStub = sandbox.stub(element, '_showErrorDialog');
|
||||||
|
const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); });
|
||||||
|
element.fire('server-error', {response: {status: 500, text: textSpy}});
|
||||||
|
|
||||||
|
assert.isTrue(textSpy.called);
|
||||||
|
flush(() => {
|
||||||
|
assert.isTrue(showErrorStub.calledOnce);
|
||||||
|
assert.isTrue(showErrorStub.lastCall.calledWithExactly(
|
||||||
|
'Error 500: ZOMG'));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_constructServerErrorMsg', () => {
|
||||||
|
const errorText = 'change conflicts';
|
||||||
|
const status = 409;
|
||||||
|
const statusText = 'Conflict';
|
||||||
|
const url = '/my/test/url';
|
||||||
|
|
||||||
|
assert.equal(element._constructServerErrorMsg({status}),
|
||||||
|
'Error 409');
|
||||||
|
assert.equal(element._constructServerErrorMsg({status, url}),
|
||||||
|
'Error 409: \nEndpoint: /my/test/url');
|
||||||
|
assert.equal(element._constructServerErrorMsg({status, statusText, url}),
|
||||||
|
'Error 409 (Conflict): \nEndpoint: /my/test/url');
|
||||||
|
assert.equal(element._constructServerErrorMsg({
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
errorText,
|
||||||
|
url,
|
||||||
|
}), 'Error 409 (Conflict): change conflicts' +
|
||||||
|
'\nEndpoint: /my/test/url');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suppress TOO_MANY_FILES error', done => {
|
||||||
|
const showAlertStub = sandbox.stub(element, '_showAlert');
|
||||||
|
const textSpy = sandbox.spy(() => {
|
||||||
|
return Promise.resolve('too many files to find conflicts');
|
||||||
|
});
|
||||||
|
element.fire('server-error', {response: {status: 500, text: textSpy}});
|
||||||
|
|
||||||
|
assert.isTrue(textSpy.called);
|
||||||
|
flush(() => {
|
||||||
|
assert.isFalse(showAlertStub.called);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show network error', done => {
|
||||||
|
const consoleErrorStub = sandbox.stub(console, 'error');
|
||||||
|
const showAlertStub = sandbox.stub(element, '_showAlert');
|
||||||
|
element.fire('network-error', {error: new Error('ZOMG')});
|
||||||
|
flush(() => {
|
||||||
|
assert.isTrue(showAlertStub.calledOnce);
|
||||||
|
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
|
||||||
|
'Server unavailable'));
|
||||||
|
assert.isTrue(consoleErrorStub.calledOnce);
|
||||||
|
assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show auth refresh toast', done => {
|
||||||
|
// starts with authed state
|
||||||
|
element.$.restAPI.getLoggedIn();
|
||||||
|
const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
|
||||||
|
() => { return Promise.resolve({}); });
|
||||||
|
const toastSpy = sandbox.spy(element, '_createToastAlert');
|
||||||
|
const windowOpen = sandbox.stub(window, 'open');
|
||||||
|
const responseText = Promise.resolve('Authentication required\n');
|
||||||
|
// fake failed auth
|
||||||
|
window.fetch.returns(Promise.resolve({status: 403}));
|
||||||
|
element.fire('server-error',
|
||||||
|
{response: {status: 403, text() { return responseText; }}}
|
||||||
|
);
|
||||||
|
assert.equal(window.fetch.callCount, 1);
|
||||||
|
flush(() => {
|
||||||
|
// auth check again
|
||||||
|
assert.equal(window.fetch.callCount, 2);
|
||||||
|
flush(() => {
|
||||||
|
// auth-error fired
|
||||||
|
assert.isTrue(toastSpy.called);
|
||||||
|
|
||||||
|
// toast
|
||||||
|
let toast = toastSpy.lastCall.returnValue;
|
||||||
|
assert.isOk(toast);
|
||||||
|
assert.include(
|
||||||
|
Polymer.dom(toast.root).textContent, 'Credentails expired.');
|
||||||
|
assert.include(
|
||||||
|
Polymer.dom(toast.root).textContent, 'Refresh credentials');
|
||||||
|
|
||||||
|
// noInteractionOverlay
|
||||||
|
const noInteractionOverlay = element.$.noInteractionOverlay;
|
||||||
|
assert.isOk(noInteractionOverlay);
|
||||||
|
sinon.spy(noInteractionOverlay, 'close');
|
||||||
|
assert.equal(
|
||||||
|
noInteractionOverlay.backdropElement.getAttribute('opened'),
|
||||||
|
'');
|
||||||
|
assert.isFalse(windowOpen.called);
|
||||||
|
MockInteractions.tap(toast.$$('gr-button.action'));
|
||||||
|
assert.isTrue(windowOpen.called);
|
||||||
|
|
||||||
|
// @see Issue 5822: noopener breaks closeAfterLogin
|
||||||
|
assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
|
||||||
|
-1);
|
||||||
|
|
||||||
|
const hideToastSpy = sandbox.spy(toast, 'hide');
|
||||||
|
|
||||||
|
// now fake authed
|
||||||
|
window.fetch.returns(Promise.resolve({status: 204}));
|
||||||
|
element._handleWindowFocus();
|
||||||
|
element.flushDebouncer('checkLoggedIn');
|
||||||
|
flush(() => {
|
||||||
|
assert.isTrue(refreshStub.called);
|
||||||
|
assert.isTrue(hideToastSpy.called);
|
||||||
|
|
||||||
|
// toast update
|
||||||
|
assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
|
||||||
|
toast = toastSpy.lastCall.returnValue;
|
||||||
|
assert.isOk(toast);
|
||||||
|
assert.include(
|
||||||
|
Polymer.dom(toast.root).textContent, 'Credentials refreshed');
|
||||||
|
|
||||||
|
// close overlay
|
||||||
|
assert.isTrue(noInteractionOverlay.close.called);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show alert', () => {
|
||||||
|
const alertObj = {message: 'foo'};
|
||||||
|
sandbox.stub(element, '_showAlert');
|
||||||
|
element.fire('show-alert', alertObj);
|
||||||
|
assert.isTrue(element._showAlert.calledOnce);
|
||||||
|
assert.equal(element._showAlert.lastCall.args[0], 'foo');
|
||||||
|
assert.isNotOk(element._showAlert.lastCall.args[1]);
|
||||||
|
assert.isNotOk(element._showAlert.lastCall.args[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checks stale credentials on visibility change', () => {
|
||||||
|
const refreshStub = sandbox.stub(element,
|
||||||
|
'_checkSignedIn');
|
||||||
|
sandbox.stub(Date, 'now').returns(999999);
|
||||||
|
element._lastCredentialCheck = 0;
|
||||||
|
element._handleVisibilityChange();
|
||||||
|
|
||||||
|
// Since there is no known account, it should not test credentials.
|
||||||
|
assert.isFalse(refreshStub.called);
|
||||||
|
assert.equal(element._lastCredentialCheck, 0);
|
||||||
|
|
||||||
|
element.knownAccountId = 123;
|
||||||
|
element._handleVisibilityChange();
|
||||||
|
|
||||||
|
// Should test credentials, since there is a known account.
|
||||||
|
assert.isTrue(refreshStub.called);
|
||||||
|
assert.equal(element._lastCredentialCheck, 999999);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshes with same credentials', done => {
|
||||||
|
const accountPromise = Promise.resolve({_account_id: 1234});
|
||||||
|
sandbox.stub(element.$.restAPI, 'getAccount')
|
||||||
|
.returns(accountPromise);
|
||||||
|
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
||||||
|
const handleRefreshStub = sandbox.stub(element,
|
||||||
|
'_handleCredentialRefreshed');
|
||||||
|
const reloadStub = sandbox.stub(element, '_reloadPage');
|
||||||
|
|
||||||
|
element.knownAccountId = 1234;
|
||||||
|
element._refreshingCredentials = true;
|
||||||
|
element._checkSignedIn();
|
||||||
|
|
||||||
|
flush(() => {
|
||||||
|
assert.isFalse(requestCheckStub.called);
|
||||||
|
assert.isTrue(handleRefreshStub.called);
|
||||||
|
assert.isFalse(reloadStub.called);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_showAlert hides existing alerts', () => {
|
||||||
|
element._alertElement = element._createToastAlert();
|
||||||
|
const hideStub = sandbox.stub(element, '_hideAlert');
|
||||||
|
element._showAlert();
|
||||||
|
assert.isTrue(hideStub.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show-error', () => {
|
||||||
|
const openStub = sandbox.stub(element.$.errorOverlay, 'open');
|
||||||
|
const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
|
||||||
|
const reportStub = sandbox.stub(element.$.reporting, 'reportErrorDialog');
|
||||||
|
|
||||||
|
const message = 'test message';
|
||||||
|
element.fire('show-error', {message});
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
|
||||||
|
assert.isTrue(openStub.called);
|
||||||
|
assert.isTrue(reportStub.called);
|
||||||
|
assert.equal(element.$.errorDialog.text, message);
|
||||||
|
|
||||||
|
element.$.errorDialog.fire('dismiss');
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
|
||||||
|
assert.isTrue(closeStub.called);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reloads when refreshed credentials differ', done => {
|
||||||
|
const accountPromise = Promise.resolve({_account_id: 1234});
|
||||||
|
sandbox.stub(element.$.restAPI, 'getAccount')
|
||||||
|
.returns(accountPromise);
|
||||||
|
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
||||||
|
const handleRefreshStub = sandbox.stub(element,
|
||||||
|
'_handleCredentialRefreshed');
|
||||||
|
const reloadStub = sandbox.stub(element, '_reloadPage');
|
||||||
|
|
||||||
|
element.knownAccountId = 4321; // Different from 1234
|
||||||
|
element._refreshingCredentials = true;
|
||||||
|
element._checkSignedIn();
|
||||||
|
|
||||||
|
flush(() => {
|
||||||
|
assert.isFalse(requestCheckStub.called);
|
||||||
|
assert.isFalse(handleRefreshStub.called);
|
||||||
|
assert.isTrue(reloadStub.called);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('show alert', () => {
|
suite('when not authed', () => {
|
||||||
const alertObj = {message: 'foo'};
|
setup(() => {
|
||||||
sandbox.stub(element, '_showAlert');
|
stub('gr-rest-api-interface', {
|
||||||
element.fire('show-alert', alertObj);
|
getLoggedIn() { return Promise.resolve(false); },
|
||||||
assert.isTrue(element._showAlert.calledOnce);
|
});
|
||||||
assert.equal(element._showAlert.lastCall.args[0], 'foo');
|
element = fixture('basic');
|
||||||
assert.isNotOk(element._showAlert.lastCall.args[1]);
|
|
||||||
assert.isNotOk(element._showAlert.lastCall.args[2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checks stale credentials on visibility change', () => {
|
|
||||||
const refreshStub = sandbox.stub(element.$.restAPI,
|
|
||||||
'checkCredentials');
|
|
||||||
sandbox.stub(Date, 'now').returns(999999);
|
|
||||||
element._lastCredentialCheck = 0;
|
|
||||||
element._handleVisibilityChange();
|
|
||||||
|
|
||||||
// Since there is no known account, it should not test credentials.
|
|
||||||
assert.isFalse(refreshStub.called);
|
|
||||||
assert.equal(element._lastCredentialCheck, 0);
|
|
||||||
|
|
||||||
element.knownAccountId = 123;
|
|
||||||
element._handleVisibilityChange();
|
|
||||||
|
|
||||||
// Should test credentials, since there is a known account.
|
|
||||||
assert.isTrue(refreshStub.called);
|
|
||||||
assert.equal(element._lastCredentialCheck, 999999);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('refresh loop continues on credential fail', done => {
|
|
||||||
const accountPromise = Promise.resolve(null);
|
|
||||||
sandbox.stub(element.$.restAPI, 'checkCredentials')
|
|
||||||
.returns(accountPromise);
|
|
||||||
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
|
||||||
const handleRefreshStub = sandbox.stub(element,
|
|
||||||
'_handleCredentialRefreshed');
|
|
||||||
const reloadStub = sandbox.stub(element, '_reloadPage');
|
|
||||||
|
|
||||||
element._refreshingCredentials = true;
|
|
||||||
element._checkSignedIn();
|
|
||||||
|
|
||||||
accountPromise.then(() => {
|
|
||||||
assert.isTrue(requestCheckStub.called);
|
|
||||||
assert.isFalse(handleRefreshStub.called);
|
|
||||||
assert.isFalse(reloadStub.called);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test('refreshes with same credentials', done => {
|
test('refresh loop continues on credential fail', done => {
|
||||||
const accountPromise = Promise.resolve({_account_id: 1234});
|
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
||||||
sandbox.stub(element.$.restAPI, 'checkCredentials')
|
const handleRefreshStub = sandbox.stub(element,
|
||||||
.returns(accountPromise);
|
'_handleCredentialRefreshed');
|
||||||
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
const reloadStub = sandbox.stub(element, '_reloadPage');
|
||||||
const handleRefreshStub = sandbox.stub(element,
|
|
||||||
'_handleCredentialRefreshed');
|
|
||||||
const reloadStub = sandbox.stub(element, '_reloadPage');
|
|
||||||
|
|
||||||
element.knownAccountId = 1234;
|
element._refreshingCredentials = true;
|
||||||
element._refreshingCredentials = true;
|
element._checkSignedIn();
|
||||||
element._checkSignedIn();
|
|
||||||
|
|
||||||
accountPromise.then(() => {
|
flush(() => {
|
||||||
assert.isFalse(requestCheckStub.called);
|
assert.isTrue(requestCheckStub.called);
|
||||||
assert.isTrue(handleRefreshStub.called);
|
assert.isFalse(handleRefreshStub.called);
|
||||||
assert.isFalse(reloadStub.called);
|
assert.isFalse(reloadStub.called);
|
||||||
done();
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reloads when refreshed credentials differ', done => {
|
|
||||||
const accountPromise = Promise.resolve({_account_id: 1234});
|
|
||||||
sandbox.stub(element.$.restAPI, 'checkCredentials')
|
|
||||||
.returns(accountPromise);
|
|
||||||
const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
|
|
||||||
const handleRefreshStub = sandbox.stub(element,
|
|
||||||
'_handleCredentialRefreshed');
|
|
||||||
const reloadStub = sandbox.stub(element, '_reloadPage');
|
|
||||||
|
|
||||||
element.knownAccountId = 4321; // Different from 1234
|
|
||||||
element._refreshingCredentials = true;
|
|
||||||
element._checkSignedIn();
|
|
||||||
|
|
||||||
accountPromise.then(() => {
|
|
||||||
assert.isFalse(requestCheckStub.called);
|
|
||||||
assert.isFalse(handleRefreshStub.called);
|
|
||||||
assert.isTrue(reloadStub.called);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('_showAlert hides existing alerts', () => {
|
|
||||||
element._alertElement = element._createToastAlert();
|
|
||||||
const hideStub = sandbox.stub(element, '_hideAlert');
|
|
||||||
element._showAlert();
|
|
||||||
assert.isTrue(hideStub.calledOnce);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show-error', () => {
|
|
||||||
const openStub = sandbox.stub(element.$.errorOverlay, 'open');
|
|
||||||
const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
|
|
||||||
const reportStub = sandbox.stub(element.$.reporting, 'reportErrorDialog');
|
|
||||||
|
|
||||||
const message = 'test message';
|
|
||||||
element.fire('show-error', {message});
|
|
||||||
flushAsynchronousOperations();
|
|
||||||
|
|
||||||
assert.isTrue(openStub.called);
|
|
||||||
assert.isTrue(reportStub.called);
|
|
||||||
assert.equal(element.$.errorDialog.text, message);
|
|
||||||
|
|
||||||
element.$.errorDialog.fire('dismiss');
|
|
||||||
flushAsynchronousOperations();
|
|
||||||
|
|
||||||
assert.isTrue(closeStub.called);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -324,7 +324,11 @@ limitations under the License.
|
|||||||
element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
|
element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getAccount', done => {
|
test('getLoggedIn', done => {
|
||||||
|
// fake fetch for authCheck
|
||||||
|
sandbox.stub(window, 'fetch', () => {
|
||||||
|
return Promise.resolve({status: 204});
|
||||||
|
});
|
||||||
plugin.restApi().getLoggedIn().then(loggedIn => {
|
plugin.restApi().getLoggedIn().then(loggedIn => {
|
||||||
assert.isTrue(loggedIn);
|
assert.isTrue(loggedIn);
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -20,22 +20,86 @@
|
|||||||
// Prevent redefinition.
|
// Prevent redefinition.
|
||||||
if (window.Gerrit.Auth) { return; }
|
if (window.Gerrit.Auth) { return; }
|
||||||
|
|
||||||
|
const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
|
||||||
const MAX_GET_TOKEN_RETRIES = 2;
|
const MAX_GET_TOKEN_RETRIES = 2;
|
||||||
|
|
||||||
Gerrit.Auth = {
|
/**
|
||||||
TYPE: {
|
* Auth class.
|
||||||
XSRF_TOKEN: 'xsrf_token',
|
*
|
||||||
ACCESS_TOKEN: 'access_token',
|
* Gerrit.Auth is an instance of this class.
|
||||||
},
|
*/
|
||||||
|
class Auth {
|
||||||
|
constructor() {
|
||||||
|
this._type = null;
|
||||||
|
this._cachedTokenPromise = null;
|
||||||
|
this._defaultOptions = {};
|
||||||
|
this._retriesLeft = MAX_GET_TOKEN_RETRIES;
|
||||||
|
this._status = Auth.STATUS.UNDETERMINED;
|
||||||
|
this._authCheckPromise = null;
|
||||||
|
this._last_auth_check_time = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
_type: null,
|
/**
|
||||||
_cachedTokenPromise: null,
|
* Returns if user is authed or not.
|
||||||
_defaultOptions: {},
|
*
|
||||||
_retriesLeft: MAX_GET_TOKEN_RETRIES,
|
* @returns {!Promise<boolean>}
|
||||||
|
*/
|
||||||
|
authCheck() {
|
||||||
|
if (!this._authCheckPromise ||
|
||||||
|
(Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
|
||||||
|
) {
|
||||||
|
// Refetch after last check expired
|
||||||
|
this._authCheckPromise = fetch('/auth-check');
|
||||||
|
this._last_auth_check_time = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._authCheckPromise.then(res => {
|
||||||
|
// auth-check will return 204 if authed
|
||||||
|
// treat the rest as unauthed
|
||||||
|
if (res.status === 204) {
|
||||||
|
this._setStatus(Auth.STATUS.AUTHED);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this._setStatus(Auth.STATUS.NOT_AUTHED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
this._setStatus(Auth.STATUS.ERROR);
|
||||||
|
// Reset _authCheckPromise to avoid caching the failed promise
|
||||||
|
this._authCheckPromise = null;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
this._authCheckPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} status
|
||||||
|
*/
|
||||||
|
_setStatus(status) {
|
||||||
|
if (this._status === status) return;
|
||||||
|
|
||||||
|
if (this._status === Auth.STATUS.AUTHED) {
|
||||||
|
Gerrit.emit('auth-error', {
|
||||||
|
message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthed() {
|
||||||
|
return this._status === Auth.STATUS.AUTHED;
|
||||||
|
}
|
||||||
|
|
||||||
_getToken() {
|
_getToken() {
|
||||||
return Promise.resolve(this._cachedTokenPromise);
|
return Promise.resolve(this._cachedTokenPromise);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable cross-domain authentication using OAuth access token.
|
* Enable cross-domain authentication using OAuth access token.
|
||||||
@@ -51,7 +115,7 @@
|
|||||||
setup(getToken, defaultOptions) {
|
setup(getToken, defaultOptions) {
|
||||||
this._retriesLeft = MAX_GET_TOKEN_RETRIES;
|
this._retriesLeft = MAX_GET_TOKEN_RETRIES;
|
||||||
if (getToken) {
|
if (getToken) {
|
||||||
this._type = Gerrit.Auth.TYPE.ACCESS_TOKEN;
|
this._type = Auth.TYPE.ACCESS_TOKEN;
|
||||||
this._cachedTokenPromise = null;
|
this._cachedTokenPromise = null;
|
||||||
this._getToken = getToken;
|
this._getToken = getToken;
|
||||||
}
|
}
|
||||||
@@ -61,7 +125,7 @@
|
|||||||
this._defaultOptions[p] = defaultOptions[p];
|
this._defaultOptions[p] = defaultOptions[p];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform network fetch with authentication.
|
* Perform network fetch with authentication.
|
||||||
@@ -74,7 +138,7 @@
|
|||||||
const options = Object.assign({
|
const options = Object.assign({
|
||||||
headers: new Headers(),
|
headers: new Headers(),
|
||||||
}, this._defaultOptions, opt_options);
|
}, this._defaultOptions, opt_options);
|
||||||
if (this._type === Gerrit.Auth.TYPE.ACCESS_TOKEN) {
|
if (this._type === Auth.TYPE.ACCESS_TOKEN) {
|
||||||
return this._getAccessToken().then(
|
return this._getAccessToken().then(
|
||||||
accessToken =>
|
accessToken =>
|
||||||
this._fetchWithAccessToken(url, options, accessToken)
|
this._fetchWithAccessToken(url, options, accessToken)
|
||||||
@@ -82,7 +146,7 @@
|
|||||||
} else {
|
} else {
|
||||||
return this._fetchWithXsrfToken(url, options);
|
return this._fetchWithXsrfToken(url, options);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_getCookie(name) {
|
_getCookie(name) {
|
||||||
const key = name + '=';
|
const key = name + '=';
|
||||||
@@ -95,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
}
|
||||||
|
|
||||||
_isTokenValid(token) {
|
_isTokenValid(token) {
|
||||||
if (!token) { return false; }
|
if (!token) { return false; }
|
||||||
@@ -105,7 +169,7 @@
|
|||||||
if (Date.now() >= expiration.getTime()) { return false; }
|
if (Date.now() >= expiration.getTime()) { return false; }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
|
|
||||||
_fetchWithXsrfToken(url, options) {
|
_fetchWithXsrfToken(url, options) {
|
||||||
if (options.method && options.method !== 'GET') {
|
if (options.method && options.method !== 'GET') {
|
||||||
@@ -116,7 +180,7 @@
|
|||||||
}
|
}
|
||||||
options.credentials = 'same-origin';
|
options.credentials = 'same-origin';
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {!Promise<string>}
|
* @return {!Promise<string>}
|
||||||
@@ -138,7 +202,7 @@
|
|||||||
// Fall back to anonymous access.
|
// Fall back to anonymous access.
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_fetchWithAccessToken(url, options, accessToken) {
|
_fetchWithAccessToken(url, options, accessToken) {
|
||||||
const params = [];
|
const params = [];
|
||||||
@@ -180,8 +244,24 @@
|
|||||||
url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
|
url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
|
||||||
}
|
}
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth.TYPE = {
|
||||||
|
XSRF_TOKEN: 'xsrf_token',
|
||||||
|
ACCESS_TOKEN: 'access_token',
|
||||||
};
|
};
|
||||||
|
|
||||||
window.Gerrit.Auth = Gerrit.Auth;
|
Auth.STATUS = {
|
||||||
|
UNDETERMINED: 0,
|
||||||
|
AUTHED: 1,
|
||||||
|
NOT_AUTHED: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
Auth.CREDS_EXPIRED_MSG = 'Credentails expired.';
|
||||||
|
|
||||||
|
// TODO(taoalpha): this whole thing should be moved to a service
|
||||||
|
window.Auth = Auth;
|
||||||
|
Gerrit.Auth = new Auth();
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ limitations under the License.
|
|||||||
|
|
||||||
setup(() => {
|
setup(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
|
|
||||||
auth = Gerrit.Auth;
|
auth = Gerrit.Auth;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,29 +42,222 @@ limitations under the License.
|
|||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('default (xsrf token header)', () => {
|
suite('Auth class methods', () => {
|
||||||
test('GET', () => {
|
let fakeFetch;
|
||||||
return auth.fetch('/url', {bar: 'bar'}).then(() => {
|
setup(() => {
|
||||||
const [url, options] = fetch.lastCall.args;
|
auth = new Auth();
|
||||||
assert.equal(url, '/url');
|
fakeFetch = sandbox.stub(window, 'fetch');
|
||||||
assert.equal(options.credentials, 'same-origin');
|
});
|
||||||
|
|
||||||
|
test('auth-check returns 403', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST', () => {
|
test('auth-check returns 204', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isTrue(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth-check returns 502', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 502}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth-check failed', done => {
|
||||||
|
fakeFetch.returns(Promise.reject(new Error('random error')));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.ERROR);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('cache and events behaivor', () => {
|
||||||
|
let fakeFetch;
|
||||||
|
let clock;
|
||||||
|
setup(() => {
|
||||||
|
auth = new Auth();
|
||||||
|
clock = sinon.useFakeTimers();
|
||||||
|
fakeFetch = sandbox.stub(window, 'fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cache auth-check result', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearCache should refetch auth-check result', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.clearCache();
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isTrue(authed2);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cache expired on auth-check after certain time', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
clock.tick(1000 * 10000);
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isTrue(authed2);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no cache if auth-check failed', done => {
|
||||||
|
fakeFetch.returns(Promise.reject(new Error('random error')));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.ERROR);
|
||||||
|
assert.equal(fakeFetch.callCount, 1);
|
||||||
|
auth.authCheck().then(() => {
|
||||||
|
assert.equal(fakeFetch.callCount, 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fire event when switch from authed to unauthed', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isTrue(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
clock.tick(1000 * 10000);
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
const emitStub = sinon.stub();
|
||||||
|
Gerrit.emit = emitStub;
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isFalse(authed2);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
assert.isTrue(emitStub.called);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fire event when switch from authed to error', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isTrue(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
clock.tick(1000 * 10000);
|
||||||
|
fakeFetch.returns(Promise.reject(new Error('random error')));
|
||||||
|
const emitStub = sinon.stub();
|
||||||
|
Gerrit.emit = emitStub;
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isFalse(authed2);
|
||||||
|
assert.isTrue(emitStub.called);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.ERROR);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no event from non-authed to other status', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
clock.tick(1000 * 10000);
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 204}));
|
||||||
|
const emitStub = sinon.stub();
|
||||||
|
Gerrit.emit = emitStub;
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isTrue(authed2);
|
||||||
|
assert.isFalse(emitStub.called);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.AUTHED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no event from non-authed to other status', done => {
|
||||||
|
fakeFetch.returns(Promise.resolve({status: 403}));
|
||||||
|
auth.authCheck().then(authed => {
|
||||||
|
assert.isFalse(authed);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
|
||||||
|
clock.tick(1000 * 10000);
|
||||||
|
fakeFetch.returns(Promise.reject(new Error('random error')));
|
||||||
|
const emitStub = sinon.stub();
|
||||||
|
Gerrit.emit = emitStub;
|
||||||
|
auth.authCheck().then(authed2 => {
|
||||||
|
assert.isFalse(authed2);
|
||||||
|
assert.isFalse(emitStub.called);
|
||||||
|
assert.equal(auth.status, Auth.STATUS.ERROR);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('default (xsrf token header)', () => {
|
||||||
|
setup(() => {
|
||||||
|
sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET', done => {
|
||||||
|
auth.fetch('/url', {bar: 'bar'}).then(() => {
|
||||||
|
const [url, options] = fetch.lastCall.args;
|
||||||
|
assert.equal(url, '/url');
|
||||||
|
assert.equal(options.credentials, 'same-origin');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST', done => {
|
||||||
sandbox.stub(auth, '_getCookie')
|
sandbox.stub(auth, '_getCookie')
|
||||||
.withArgs('XSRF_TOKEN')
|
.withArgs('XSRF_TOKEN')
|
||||||
.returns('foobar');
|
.returns('foobar');
|
||||||
return auth.fetch('/url', {method: 'POST'}).then(() => {
|
auth.fetch('/url', {method: 'POST'}).then(() => {
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.equal(url, '/url');
|
assert.equal(url, '/url');
|
||||||
assert.equal(options.credentials, 'same-origin');
|
assert.equal(options.credentials, 'same-origin');
|
||||||
assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
|
assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('cors (access token)', () => {
|
suite('cors (access token)', () => {
|
||||||
|
setup(() => {
|
||||||
|
sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
|
||||||
|
});
|
||||||
|
|
||||||
let getToken;
|
let getToken;
|
||||||
|
|
||||||
const makeToken = opt_accessToken => {
|
const makeToken = opt_accessToken => {
|
||||||
@@ -81,62 +273,68 @@ limitations under the License.
|
|||||||
auth.setup(getToken);
|
auth.setup(getToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('base url support', () => {
|
test('base url support', done => {
|
||||||
const baseUrl = 'http://foo';
|
const baseUrl = 'http://foo';
|
||||||
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
|
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
|
||||||
return auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
|
auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
|
||||||
const [url] = fetch.lastCall.args;
|
const [url] = fetch.lastCall.args;
|
||||||
assert.equal(url, 'http://foo/a/url?access_token=zbaz');
|
assert.equal(url, 'http://foo/a/url?access_token=zbaz');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetch not signed in', () => {
|
test('fetch not signed in', done => {
|
||||||
getToken.returns(Promise.resolve());
|
getToken.returns(Promise.resolve());
|
||||||
return auth.fetch('/url', {bar: 'bar'}).then(() => {
|
auth.fetch('/url', {bar: 'bar'}).then(() => {
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.equal(url, '/url');
|
assert.equal(url, '/url');
|
||||||
assert.equal(options.bar, 'bar');
|
assert.equal(options.bar, 'bar');
|
||||||
assert.equal(Object.keys(options.headers).length, 0);
|
assert.equal(Object.keys(options.headers).length, 0);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetch signed in', () => {
|
test('fetch signed in', done => {
|
||||||
return auth.fetch('/url', {bar: 'bar'}).then(() => {
|
auth.fetch('/url', {bar: 'bar'}).then(() => {
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.equal(url, '/a/url?access_token=zbaz');
|
assert.equal(url, '/a/url?access_token=zbaz');
|
||||||
assert.equal(options.bar, 'bar');
|
assert.equal(options.bar, 'bar');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getToken calls are cached', () => {
|
test('getToken calls are cached', done => {
|
||||||
return Promise.all([
|
Promise.all([
|
||||||
auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
|
auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
|
||||||
assert.equal(getToken.callCount, 1);
|
assert.equal(getToken.callCount, 1);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getToken refreshes token', () => {
|
test('getToken refreshes token', done => {
|
||||||
sandbox.stub(auth, '_isTokenValid');
|
sandbox.stub(auth, '_isTokenValid');
|
||||||
auth._isTokenValid
|
auth._isTokenValid
|
||||||
.onFirstCall().returns(true)
|
.onFirstCall().returns(true)
|
||||||
.onSecondCall().returns(false)
|
.onSecondCall().returns(false)
|
||||||
.onThirdCall().returns(true);
|
.onThirdCall().returns(true);
|
||||||
return auth.fetch('/url-one').then(() => {
|
auth.fetch('/url-one').then(() => {
|
||||||
getToken.returns(Promise.resolve(makeToken('bzzbb')));
|
getToken.returns(Promise.resolve(makeToken('bzzbb')));
|
||||||
return auth.fetch('/url-two');
|
return auth.fetch('/url-two');
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const [[firstUrl], [secondUrl]] = fetch.args;
|
const [[firstUrl], [secondUrl]] = fetch.args;
|
||||||
assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
|
assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
|
||||||
assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
|
assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signed in token error falls back to anonymous', () => {
|
test('signed in token error falls back to anonymous', done => {
|
||||||
getToken.returns(Promise.resolve('rubbish'));
|
getToken.returns(Promise.resolve('rubbish'));
|
||||||
return auth.fetch('/url', {bar: 'bar'}).then(() => {
|
auth.fetch('/url', {bar: 'bar'}).then(() => {
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.equal(url, '/url');
|
assert.equal(url, '/url');
|
||||||
assert.equal(options.bar, 'bar');
|
assert.equal(options.bar, 'bar');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,12 +352,12 @@ limitations under the License.
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('HTTP PUT with content type', () => {
|
test('HTTP PUT with content type', done => {
|
||||||
const originalOptions = {
|
const originalOptions = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: new Headers({'Content-Type': 'mail/pigeon'}),
|
headers: new Headers({'Content-Type': 'mail/pigeon'}),
|
||||||
};
|
};
|
||||||
return auth.fetch('/url', originalOptions).then(() => {
|
auth.fetch('/url', originalOptions).then(() => {
|
||||||
assert.isTrue(getToken.called);
|
assert.isTrue(getToken.called);
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.include(url, '$ct=mail%2Fpigeon');
|
assert.include(url, '$ct=mail%2Fpigeon');
|
||||||
@@ -167,14 +365,15 @@ limitations under the License.
|
|||||||
assert.include(url, 'access_token=zbaz');
|
assert.include(url, 'access_token=zbaz');
|
||||||
assert.equal(options.method, 'POST');
|
assert.equal(options.method, 'POST');
|
||||||
assert.equal(options.headers.get('Content-Type'), 'text/plain');
|
assert.equal(options.headers.get('Content-Type'), 'text/plain');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('HTTP PUT without content type', () => {
|
test('HTTP PUT without content type', done => {
|
||||||
const originalOptions = {
|
const originalOptions = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
};
|
};
|
||||||
return auth.fetch('/url', originalOptions).then(() => {
|
auth.fetch('/url', originalOptions).then(() => {
|
||||||
assert.isTrue(getToken.called);
|
assert.isTrue(getToken.called);
|
||||||
const [url, options] = fetch.lastCall.args;
|
const [url, options] = fetch.lastCall.args;
|
||||||
assert.include(url, '$ct=text%2Fplain');
|
assert.include(url, '$ct=text%2Fplain');
|
||||||
@@ -182,6 +381,7 @@ limitations under the License.
|
|||||||
assert.include(url, 'access_token=zbaz');
|
assert.include(url, 'access_token=zbaz');
|
||||||
assert.equal(options.method, 'POST');
|
assert.equal(options.method, 'POST');
|
||||||
assert.equal(options.headers.get('Content-Type'), 'text/plain');
|
assert.equal(options.headers.get('Content-Type'), 'text/plain');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,12 +66,6 @@
|
|||||||
* @event network-error
|
* @event network-error
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired when credentials were rejected by server (e.g. expired).
|
|
||||||
*
|
|
||||||
* @event auth-error
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired after an RPC completes.
|
* Fired after an RPC completes.
|
||||||
*
|
*
|
||||||
@@ -89,10 +83,6 @@
|
|||||||
type: Object,
|
type: Object,
|
||||||
value: new SiteBasedCache(), // Shared across instances.
|
value: new SiteBasedCache(), // Shared across instances.
|
||||||
},
|
},
|
||||||
_credentialCheck: {
|
|
||||||
type: Object,
|
|
||||||
value: {checking: false}, // Shared across instances.
|
|
||||||
},
|
|
||||||
_sharedFetchPromises: {
|
_sharedFetchPromises: {
|
||||||
type: Object,
|
type: Object,
|
||||||
value: new FetchPromisesCache(), // Shared across instances.
|
value: new FetchPromisesCache(), // Shared across instances.
|
||||||
@@ -112,40 +102,12 @@
|
|||||||
type: Object,
|
type: Object,
|
||||||
value: {}, // Intentional to share the object across instances.
|
value: {}, // Intentional to share the object across instances.
|
||||||
},
|
},
|
||||||
_auth: {
|
|
||||||
type: Object,
|
|
||||||
value: Gerrit.Auth, // Share across instances.
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
super.created();
|
super.created();
|
||||||
/* Polymer 1 and Polymer 2 have slightly different lifecycle.
|
this._auth = Gerrit.Auth;
|
||||||
* Differences are not very well documented (see
|
|
||||||
* https://github.com/Polymer/old-docs-site/issues/2322).
|
|
||||||
* In Polymer 1, created() is called when properties values is not set
|
|
||||||
* and ready() is always called later, even if element is not added
|
|
||||||
* to a DOM. I.e. in Polymer 1 _cache and other properties are undefined,
|
|
||||||
* while in Polymer 2 they are set to default values.
|
|
||||||
* In Polymer 2, created() is called after properties values set and
|
|
||||||
* ready() is called only after element is attached to a DOM.
|
|
||||||
* There are several places in the code, where element is created with
|
|
||||||
* document.createElement('gr-rest-api-interface') and is not added
|
|
||||||
* to a DOM.
|
|
||||||
* In such cases, Polymer 1 calls both created() and ready() methods,
|
|
||||||
* but Polymer 2 calls only created() method.
|
|
||||||
* To workaround these differences, we should try to create _restApiHelper
|
|
||||||
* in both methods.
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
|
|
||||||
this._initRestApiHelper();
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
// See comments in created()
|
|
||||||
this._initRestApiHelper();
|
this._initRestApiHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +115,9 @@
|
|||||||
if (this._restApiHelper) {
|
if (this._restApiHelper) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._cache && this._auth && this._sharedFetchPromises &&
|
if (this._cache && this._auth && this._sharedFetchPromises) {
|
||||||
this._credentialCheck) {
|
|
||||||
this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
|
this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
|
||||||
this._sharedFetchPromises, this._credentialCheck, this);
|
this._sharedFetchPromises, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,11 +811,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLoggedIn() {
|
getLoggedIn() {
|
||||||
return this.getAccount().then(account => {
|
return this._auth.authCheck();
|
||||||
return account != null;
|
|
||||||
}).catch(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsAdmin() {
|
getIsAdmin() {
|
||||||
@@ -869,10 +826,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCredentials() {
|
|
||||||
return this._restApiHelper.checkCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultPreferences() {
|
getDefaultPreferences() {
|
||||||
return this._fetchSharedCacheURL({
|
return this._fetchSharedCacheURL({
|
||||||
url: '/config/server/preferences',
|
url: '/config/server/preferences',
|
||||||
@@ -1347,6 +1300,10 @@
|
|||||||
this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
|
this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateAccountsCache() {
|
||||||
|
this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} filter
|
* @param {string} filter
|
||||||
* @param {number} groupsPerPage
|
* @param {number} groupsPerPage
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ limitations under the License.
|
|||||||
window.CANONICAL_PATH = `test${ctr}`;
|
window.CANONICAL_PATH = `test${ctr}`;
|
||||||
|
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
element = fixture('basic');
|
|
||||||
element._projectLookup = {};
|
|
||||||
const testJSON = ')]}\'\n{"hello": "bonjour"}';
|
const testJSON = ')]}\'\n{"hello": "bonjour"}';
|
||||||
sandbox.stub(window, 'fetch').returns(Promise.resolve({
|
sandbox.stub(window, 'fetch').returns(Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -57,6 +55,10 @@ limitations under the License.
|
|||||||
return Promise.resolve(testJSON);
|
return Promise.resolve(testJSON);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
// fake auth
|
||||||
|
sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true));
|
||||||
|
element = fixture('basic');
|
||||||
|
element._projectLookup = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(() => {
|
teardown(() => {
|
||||||
@@ -365,117 +367,6 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('auth failure', done => {
|
|
||||||
const fakeAuthResponse = {
|
|
||||||
ok: false,
|
|
||||||
status: 403,
|
|
||||||
};
|
|
||||||
window.fetch.onFirstCall().returns(
|
|
||||||
Promise.reject(new Error('Failed to fetch')));
|
|
||||||
window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
|
|
||||||
// Emulate logged in.
|
|
||||||
element._restApiHelper._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 => {
|
|
||||||
flush(() => {
|
|
||||||
assert.isTrue(authErrorStub.called);
|
|
||||||
assert.isFalse(serverErrorStub.called);
|
|
||||||
assert.isFalse(element._cache.has('/accounts/self/detail'));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('auth failure - test all failed to fetch', done => {
|
|
||||||
window.fetch.returns(
|
|
||||||
Promise.reject(new Error('Failed to fetch')));
|
|
||||||
// Emulate logged in.
|
|
||||||
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 => {
|
|
||||||
flush(() => {
|
|
||||||
assert.isTrue(authErrorStub.called);
|
|
||||||
assert.isFalse(serverErrorStub.called);
|
|
||||||
assert.isFalse(element._cache.has('/accounts/self/detail'));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getLoggedIn returns false when network/auth failure', done => {
|
|
||||||
window.fetch.returns(
|
|
||||||
Promise.reject(new Error('Failed to fetch')));
|
|
||||||
element.getLoggedIn().then(isLoggedIn => {
|
|
||||||
assert.isFalse(isLoggedIn);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checkCredentials', done => {
|
|
||||||
const responses = [
|
|
||||||
{
|
|
||||||
ok: false,
|
|
||||||
status: 403,
|
|
||||||
text() { return Promise.resolve(); },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
text() { return Promise.resolve(')]}\'{}'); },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
window.fetch.restore();
|
|
||||||
sandbox.stub(window, 'fetch', url => {
|
|
||||||
if (url === window.CANONICAL_PATH + '/accounts/self/detail') {
|
|
||||||
return Promise.resolve(responses.shift());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
element.getLoggedIn().then(account => {
|
|
||||||
assert.isNotOk(account);
|
|
||||||
element.checkCredentials().then(account => {
|
|
||||||
assert.isOk(account);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checkCredentials promise rejection', () => {
|
|
||||||
window.fetch.restore();
|
|
||||||
element._cache.set('/accounts/self/detail', true);
|
|
||||||
const checkCredentialsSpy =
|
|
||||||
sandbox.spy(element._restApiHelper, 'checkCredentials');
|
|
||||||
sandbox.stub(window, 'fetch', url => {
|
|
||||||
return Promise.reject(new Error('Failed to fetch'));
|
|
||||||
});
|
|
||||||
return element.getConfig(true)
|
|
||||||
.catch(err => undefined)
|
|
||||||
.then(() => {
|
|
||||||
// When the top-level fetch call throws an error, it invokes
|
|
||||||
// checkCredentials, which in turn makes another fetch call.
|
|
||||||
// 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(window.fetch.calledTwice);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checkCredentials accepts only json', () => {
|
|
||||||
const authFetchStub = sandbox.stub(element._auth, 'fetch')
|
|
||||||
.returns(Promise.resolve());
|
|
||||||
element.checkCredentials();
|
|
||||||
assert.isTrue(authFetchStub.called);
|
|
||||||
assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
|
|
||||||
'application/json');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('legacy n,z key in change url is replaced', () => {
|
test('legacy n,z key in change url is replaced', () => {
|
||||||
const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
|
const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
|
||||||
.returns(Promise.resolve([]));
|
.returns(Promise.resolve([]));
|
||||||
@@ -922,6 +813,18 @@ limitations under the License.
|
|||||||
assert.isFalse(element._cache.has(url));
|
assert.isFalse(element._cache.has(url));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('invalidateAccountsCache', () => {
|
||||||
|
const url = '/accounts/self/detail';
|
||||||
|
|
||||||
|
element._cache.set(url, {});
|
||||||
|
|
||||||
|
element.invalidateAccountsCache();
|
||||||
|
|
||||||
|
assert.isUndefined(element._sharedFetchPromises[url]);
|
||||||
|
|
||||||
|
assert.isFalse(element._cache.has(url));
|
||||||
|
});
|
||||||
|
|
||||||
suite('getRepos', () => {
|
suite('getRepos', () => {
|
||||||
const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
|
const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
|
||||||
let fetchCacheURLStub;
|
let fetchCacheURLStub;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const JSON_PREFIX = ')]}\'';
|
const JSON_PREFIX = ')]}\'';
|
||||||
const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around Map for caching server responses. Site-based so that
|
* Wrapper around Map for caching server responses. Site-based so that
|
||||||
@@ -107,15 +106,13 @@
|
|||||||
* @param {SiteBasedCache} cache
|
* @param {SiteBasedCache} cache
|
||||||
* @param {object} auth
|
* @param {object} auth
|
||||||
* @param {FetchPromisesCache} fetchPromisesCache
|
* @param {FetchPromisesCache} fetchPromisesCache
|
||||||
* @param {object} credentialCheck
|
|
||||||
* @param {object} restApiInterface
|
* @param {object} restApiInterface
|
||||||
*/
|
*/
|
||||||
constructor(cache, auth, fetchPromisesCache, credentialCheck,
|
constructor(cache, auth, fetchPromisesCache,
|
||||||
restApiInterface) {
|
restApiInterface) {
|
||||||
this._cache = cache;// TODO: make it public
|
this._cache = cache;// TODO: make it public
|
||||||
this._auth = auth;
|
this._auth = auth;
|
||||||
this._fetchPromisesCache = fetchPromisesCache;
|
this._fetchPromisesCache = fetchPromisesCache;
|
||||||
this._credentialCheck = credentialCheck;
|
|
||||||
this._restApiInterface = restApiInterface;
|
this._restApiInterface = restApiInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +187,10 @@
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
const isLoggedIn = !!this._cache.get('/accounts/self/detail');
|
if (req.errFn) {
|
||||||
if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
|
req.errFn.call(undefined, null, err);
|
||||||
this.checkCredentials();
|
|
||||||
} else {
|
} else {
|
||||||
if (req.errFn) {
|
this.fire('network-error', {error: err});
|
||||||
req.errFn.call(undefined, null, err);
|
|
||||||
} else {
|
|
||||||
this.fire('network-error', {error: err});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -384,37 +376,6 @@
|
|||||||
return xhr;
|
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
|
* @param {string} prefix
|
||||||
*/
|
*/
|
||||||
@@ -428,4 +389,3 @@
|
|||||||
window.FetchPromisesCache = FetchPromisesCache;
|
window.FetchPromisesCache = FetchPromisesCache;
|
||||||
window.GrRestApiHelper = GrRestApiHelper;
|
window.GrRestApiHelper = GrRestApiHelper;
|
||||||
})(window);
|
})(window);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ limitations under the License.
|
|||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
cache = new SiteBasedCache();
|
cache = new SiteBasedCache();
|
||||||
fetchPromisesCache = new FetchPromisesCache();
|
fetchPromisesCache = new FetchPromisesCache();
|
||||||
const credentialCheck = {checking: false};
|
|
||||||
|
|
||||||
window.CANONICAL_PATH = 'testhelper';
|
window.CANONICAL_PATH = 'testhelper';
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ limitations under the License.
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
|
helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
|
||||||
credentialCheck, mockRestApiInterface);
|
mockRestApiInterface);
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(() => {
|
teardown(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user