Refresh expired credentials
Show error toast with a link to sign-in child window. Close child window
on successful login and refresh credentials without reloading page.
Listen to window focus (using Page Visibility API) to refresh
credentials immediately after window/tab focus.
Feature: Issue 4097
Change-Id: If5c798b6ac3b4cb97f2ac537c9f5b4b58cf58f9a
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 757d79f..f2b0d78 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,16 +14,17 @@
(function() {
'use strict';
+ var HIDE_ALERT_TIMEOUT_MS = 5000;
+ var CHECK_SIGN_IN_INTERVAL_MS = 60000;
+ var SIGN_IN_WIDTH_PX = 690;
+ var SIGN_IN_HEIGHT_PX = 500;
+
Polymer({
is: 'gr-error-manager',
properties: {
_alertElement: Element,
_hideAlertHandle: Number,
- _hideAlertTimeout: {
- type: Number,
- value: 5000,
- },
},
attached: function() {
@@ -67,7 +68,7 @@
this._clearHideAlertHandle();
this._hideAlertHandle =
- this.async(this._hideAlert.bind(this), this._hideAlertTimeout);
+ this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
var el = this._createToastAlert();
el.show(text);
this._alertElement = el;
@@ -88,12 +89,17 @@
},
_showAuthErrorAlert: function() {
+ // TODO(viktard): close alert if it's not for auth error.
if (this._alertElement) { return; }
- var el = this._createToastAlert();
- el.addEventListener('action', this._refreshPage.bind(this));
- el.show('Auth error', 'Refresh page');
- this._alertElement = el;
+ this._alertElement = this._createToastAlert();
+ this._alertElement.show('Auth error', 'Refresh credentials.');
+ this.listen(this._alertElement, 'action', '_createLoginPopup');
+
+ if (typeof document.hidden !== undefined) {
+ this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+ }
+ this._requestCheckLoggedIn();
},
_createToastAlert: function() {
@@ -102,8 +108,44 @@
return el;
},
- _refreshPage: function() {
- window.location.reload();
+ _handleVisibilityChange: function() {
+ if (!document.hidden) {
+ this.flushDebouncer('checkLoggedIn');
+ }
+ },
+
+ _requestCheckLoggedIn: function() {
+ this.debounce(
+ 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+ },
+
+ _checkSignedIn: function() {
+ this.$.restAPI.refreshCredentials().then(function(isLoggedIn) {
+ if (isLoggedIn) {
+ this._handleCredentialRefresh();
+ } else {
+ this._requestCheckLoggedIn();
+ }
+ }.bind(this));
+ },
+
+ _createLoginPopup: function(e) {
+ var left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
+ var top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
+ var options = [
+ 'width=' + SIGN_IN_WIDTH_PX,
+ 'height=' + SIGN_IN_HEIGHT_PX,
+ 'left=' + left,
+ 'top=' + top,
+ ];
+ window.open('/login/%3FcloseAfterLogin', '_blank', options.join(','));
+ },
+
+ _handleCredentialRefresh: function() {
+ this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
+ this.unlisten(this._alertElement, 'action', '_createLoginPopup');
+ this._hideAlert();
+ this._showAlert('Credentials refreshed.');
},
});
})();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 08ce303..90e1ef8 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -33,27 +33,32 @@
<script>
suite('gr-error-manager tests', function() {
var element;
+ var sandbox;
setup(function() {
+ sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn: function() { return Promise.resolve(true); },
});
element = fixture('basic');
});
+ teardown(function() {
+ sandbox.restore();
+ });
+
test('show auth error', function(done) {
- var showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+ var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
element.fire('server-error', {response: {status: 403}});
flush(function() {
assert.isTrue(showAuthErrorStub.calledOnce);
- showAuthErrorStub.restore();
done();
});
});
test('show normal server error', function(done) {
- var showAlertStub = sinon.stub(element, '_showAlert');
- var textSpy = sinon.spy(function() { return Promise.resolve('ZOMG'); });
+ var showAlertStub = sandbox.stub(element, '_showAlert');
+ var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); });
element.fire('server-error', {response: {status: 500, text: textSpy}});
assert.isTrue(textSpy.called);
@@ -61,14 +66,13 @@
assert.isTrue(showAlertStub.calledOnce);
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
'Server error: ZOMG'));
- showAlertStub.restore();
done();
});
});
test('show network error', function(done) {
- var consoleErrorStub = sinon.stub(console, 'error');
- var showAlertStub = sinon.stub(element, '_showAlert');
+ var consoleErrorStub = sandbox.stub(console, 'error');
+ var showAlertStub = sandbox.stub(element, '_showAlert');
element.fire('network-error', {error: new Error('ZOMG')});
flush(function() {
assert.isTrue(showAlertStub.calledOnce);
@@ -76,10 +80,45 @@
'Server unavailable'));
assert.isTrue(consoleErrorStub.calledOnce);
assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
- showAlertStub.restore();
- consoleErrorStub.restore();
done();
});
});
+
+ test('show auth refresh toast', function(done) {
+ var refreshStub = sandbox.stub(element.$.restAPI, 'refreshCredentials',
+ function() { return Promise.resolve(true); });
+ var toastSpy = sandbox.spy(element, '_createToastAlert');
+ var windowOpen = sandbox.stub(window, 'open');
+ element.fire('server-error', {response: {status: 403}});
+ flush(function() {
+ assert.isTrue(toastSpy.called);
+ var 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);
+ toast.fire('action');
+ assert.isTrue(windowOpen.called);
+
+ var hideToastSpy = sandbox.spy(toast, 'hide');
+
+ assert.isFalse(refreshStub.called);
+ element.flushDebouncer('checkLoggedIn');
+ flush(function() {
+ assert.isTrue(refreshStub.called);
+ assert.isTrue(hideToastSpy.called);
+
+ assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
+ toast = toastSpy.lastCall.returnValue;
+ assert.isOk(toast);
+ assert.include(
+ Polymer.dom(toast.root).textContent, 'Credentials refreshed');
+ done();
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 32246ca..9d8d409b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -40,6 +40,10 @@
// Routes.
page('/', loadUser, function(data) {
+ if (data.querystring.match(/^closeAfterLogin/)) {
+ // Close child window on redirect after login.
+ window.close();
+ }
// For backward compatibility with GWT links.
if (data.hash) {
page.redirect(data.hash);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 25a432b..44976d3 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -250,6 +250,11 @@
});
},
+ refreshCredentials: function() {
+ this._cache['/accounts/self/detail'] = undefined;
+ return this.getLoggedIn();
+ },
+
getPreferences: function() {
return this.getLoggedIn().then(function(loggedIn) {
if (loggedIn) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index a8932bf..e7f3e2a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -33,29 +33,34 @@
<script>
suite('gr-rest-api-interface tests', function() {
var element;
+ var sandbox;
setup(function() {
+ sandbox = sinon.sandbox.create();
element = fixture('basic');
});
+ teardown(function() {
+ sandbox.restore();
+ });
+
test('JSON prefix is properly removed', function(done) {
var testJSON = ')]}\'\n{"hello": "bonjour"}';
- var fetchStub = sinon.stub(window, 'fetch', function() {
+ sandbox.stub(window, 'fetch', function() {
return Promise.resolve({ok: true, text: function() {
return Promise.resolve(testJSON);
}});
});
element.fetchJSON('/dummy/url').then(function(obj) {
assert.deepEqual(obj, {hello: 'bonjour'});
- fetchStub.restore();
done();
});
});
test('cached results', function(done) {
var n = 0;
- var fetchJSONStub = sinon.stub(element, 'fetchJSON', function() {
+ sandbox.stub(element, 'fetchJSON', function() {
return Promise.resolve(++n);
});
var promises = [];
@@ -67,7 +72,6 @@
assert.deepEqual(results, [1, 1, 1]);
element._fetchSharedCacheURL('/foo').then(function(foo) {
assert.equal(foo, 1);
- fetchJSONStub.restore();
done();
});
});
@@ -105,7 +109,7 @@
test('request callbacks can be canceled', function(done) {
var cancelCalled = false;
- var fetchStub = sinon.stub(window, 'fetch', function() {
+ sandbox.stub(window, 'fetch', function() {
return Promise.resolve({body: {
cancel: function() { cancelCalled = true; }
}});
@@ -114,13 +118,12 @@
function(obj) {
assert.isUndefined(obj);
assert.isTrue(cancelCalled);
- fetchStub.restore();
done();
});
});
test('parent diff comments are properly grouped', function(done) {
- var fetchJSONStub = sinon.stub(element, 'fetchJSON', function() {
+ sandbox.stub(element, 'fetchJSON', function() {
return Promise.resolve({
'/COMMIT_MSG': [],
'sieve.go': [
@@ -147,13 +150,12 @@
message: 'this isn’t quite right',
path: 'sieve.go',
});
- fetchJSONStub.restore();
done();
});
});
test('differing patch diff comments are properly grouped', function(done) {
- var fetchJSONStub = sinon.stub(element, 'fetchJSON', function(url) {
+ sandbox.stub(element, 'fetchJSON', function(url) {
if (url == '/changes/42/revisions/1') {
return Promise.resolve({
'/COMMIT_MSG': [],
@@ -201,7 +203,6 @@
message: '¯\\_(ツ)_/¯',
path: 'sieve.go',
});
- fetchJSONStub.restore();
done();
});
});
@@ -235,22 +236,21 @@
test('rebase always enabled', function(done) {
var resolveFetchJSON;
- var fetchJSONStub = sinon.stub(element, 'fetchJSON').returns(
+ sandbox.stub(element, 'fetchJSON').returns(
new Promise(function(resolve) {
resolveFetchJSON = resolve;
}));
element.getChangeRevisionActions('42', '1337').then(
function(response) {
assert.isTrue(response.rebase.enabled);
- fetchJSONStub.restore();
done();
});
resolveFetchJSON({rebase: {}});
});
test('server error', function(done) {
- var getResponseObjectStub = sinon.stub(element, 'getResponseObject');
- var fetchStub = sinon.stub(window, 'fetch', function() {
+ var getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+ sandbox.stub(window, 'fetch', function() {
return Promise.resolve({ok: false});
});
var serverErrorEventPromise = new Promise(function(resolve) {
@@ -261,12 +261,37 @@
function(response) {
assert.isUndefined(response);
assert.isTrue(getResponseObjectStub.notCalled);
- getResponseObjectStub.restore();
- fetchStub.restore();
serverErrorEventPromise.then(function() {
done();
});
});
});
+
+ test('refreshCredentials', function(done) {
+ var responses = [
+ {
+ ok: false,
+ status: 403,
+ text: function() { return Promise.resolve(); }
+ },
+ {
+ ok: true,
+ status: 200,
+ text: function() { return Promise.resolve(')]}\'{}'); }
+ },
+ ];
+ var fetchStub = sandbox.stub(window, 'fetch', function(url) {
+ if (url === '/accounts/self/detail') {
+ return Promise.resolve(responses.shift());
+ }
+ });
+ element.getLoggedIn().then(function(isLoggedIn) {
+ assert.isFalse(isLoggedIn);
+ element.refreshCredentials().then(function(isRefreshed) {
+ assert.isTrue(isRefreshed);
+ done();
+ });
+ });
+ });
});
</script>