Merge "Handle network failures during project lookup"
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index f07daa5..2ef65f4 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -23,6 +23,9 @@
 <link rel="import" href="../gr-reporting/gr-reporting.html">
 
 <dom-module id="gr-router">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
   <script src="../../../bower_components/page/page.js"></script>
   <script src="gr-router.js"></script>
 </dom-module>
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 3186c33..3a724bb 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -154,10 +154,6 @@
     is: 'gr-router',
 
     properties: {
-      _restAPI: {
-        type: Object,
-        value: () => document.createElement('gr-rest-api-interface'),
-      },
       _app: {
         type: Object,
         value: app,
@@ -277,8 +273,12 @@
     _normalizeLegacyRouteParams(params) {
       if (!params.changeNum) { return Promise.resolve(); }
 
-      return this._restAPI.getFromProjectLookup(params.changeNum)
+      return this.$.restAPI.getFromProjectLookup(params.changeNum)
           .then(project => {
+            // Do nothing if the lookup request failed. This avoids an infinite
+            // loop of project lookups.
+            if (!project) { return; }
+
             params.project = project;
             this._normalizePatchRangeParams(params);
             this._redirect(this._generateUrl(params));
@@ -370,7 +370,7 @@
      *     (if it resolves).
      */
     _redirectIfNotLoggedIn(data) {
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
           return Promise.resolve();
         } else {
@@ -382,7 +382,7 @@
 
     /**  Page.js middleware that warms the REST API's logged-in cache line. */
     _loadUserMiddleware(ctx, next) {
-      this._restAPI.getLoggedIn().then(() => { next(); });
+      this.$.restAPI.getLoggedIn().then(() => { next(); });
     },
 
     /**
@@ -585,7 +585,7 @@
         this._redirect(newUrl);
         return null;
       }
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
           this._redirect('/dashboard/self');
         } else {
@@ -600,7 +600,7 @@
         return;
       }
 
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (!loggedIn) {
           if (data.params[0].toLowerCase() === 'self') {
             this._redirectToLogin(data.canonicalPath);
@@ -938,7 +938,7 @@
         this._redirect(this._generateUrl(params));
       } else {
         this._setParams(params);
-        this._restAPI.setInProjectLookup(params.changeNum,
+        this.$.restAPI.setInProjectLookup(params.changeNum,
             params.project);
       }
     },
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 3a9d2d0..c294b22 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -183,7 +183,7 @@
     });
 
     test('_redirectIfNotLoggedIn while logged in', () => {
-      sandbox.stub(element._restAPI, 'getLoggedIn')
+      sandbox.stub(element.$.restAPI, 'getLoggedIn')
           .returns(Promise.resolve(true));
       const data = {canonicalPath: ''};
       const redirectStub = sandbox.stub(element, '_redirectToLogin');
@@ -193,7 +193,7 @@
     });
 
     test('_redirectIfNotLoggedIn while logged out', () => {
-      sandbox.stub(element._restAPI, 'getLoggedIn')
+      sandbox.stub(element.$.restAPI, 'getLoggedIn')
           .returns(Promise.resolve(false));
       const redirectStub = sandbox.stub(element, '_redirectToLogin');
       const data = {canonicalPath: ''};
@@ -304,34 +304,51 @@
 
       setup(() => {
         projectLookupStub = sandbox
-            .stub(element._restAPI, 'getFromProjectLookup')
-            .returns(Promise.resolve('foo/bar'));
+            .stub(element.$.restAPI, 'getFromProjectLookup');
         sandbox.stub(element, '_generateUrl');
       });
 
       suite('_normalizeLegacyRouteParams', () => {
         let rangeStub;
+        let redirectStub;
 
         setup(() => {
           rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
               .returns(Promise.resolve());
+          redirectStub = sandbox.stub(element, '_redirect');
         });
 
         test('w/o changeNum', () => {
+          projectLookupStub.returns(Promise.resolve('foo/bar'));
           const params = {};
           return element._normalizeLegacyRouteParams(params).then(() => {
             assert.isFalse(projectLookupStub.called);
             assert.isFalse(rangeStub.called);
             assert.isNotOk(params.project);
+            assert.isFalse(redirectStub.called);
           });
         });
 
         test('w/ changeNum', () => {
+          projectLookupStub.returns(Promise.resolve('foo/bar'));
           const params = {changeNum: 1234};
           return element._normalizeLegacyRouteParams(params).then(() => {
             assert.isTrue(projectLookupStub.called);
             assert.isTrue(rangeStub.called);
             assert.equal(params.project, 'foo/bar');
+            assert.isTrue(redirectStub.calledOnce);
+          });
+        });
+
+        test('halts on project lookup failure', () => {
+          projectLookupStub.returns(Promise.resolve(undefined));
+
+          const params = {changeNum: 1234};
+          return element._normalizeLegacyRouteParams(params).then(() => {
+            assert.isTrue(projectLookupStub.called);
+            assert.isFalse(rangeStub.called);
+            assert.isUndefined(params.project);
+            assert.isFalse(redirectStub.called);
           });
         });
       });
@@ -472,7 +489,7 @@
         });
 
         test('redirects to dahsboard if logged in', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(true));
           const data = {
             canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -485,7 +502,7 @@
         });
 
         test('redirects to open changes if not logged in', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {
             canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -592,7 +609,7 @@
         });
 
         test('own dahsboard but signed out redirects to login', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {canonicalPath: '/dashboard', params: {0: 'seLF'}};
           const result = element._handleDashboardRoute(data);
@@ -605,7 +622,7 @@
         });
 
         test('non-self dahsboard but signed out does not redirect', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {canonicalPath: '/dashboard', params: {0: 'foo'}};
           const result = element._handleDashboardRoute(data);
@@ -619,7 +636,7 @@
         });
 
         test('dahsboard while signed in sets params', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(true));
           const data = {canonicalPath: '/dashboard', params: {0: 'foo'}};
           const result = element._handleDashboardRoute(data);
@@ -1051,7 +1068,7 @@
           setup(() => {
             normalizeRangeStub = sandbox.stub(element,
                 '_normalizePatchRangeParams');
-            sandbox.stub(element._restAPI, 'setInProjectLookup');
+            sandbox.stub(element.$.restAPI, 'setInProjectLookup');
           });
 
           test('needs redirect', () => {
@@ -1103,7 +1120,7 @@
         test('_handleDiffEditRoute', () => {
           const normalizeRangeStub =
               sandbox.stub(element, '_normalizePatchRangeParams');
-          sandbox.stub(element._restAPI, 'setInProjectLookup');
+          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
           const ctx = {
             params: [
               'foo/bar', // 0 Project
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 406ec0c..06a3fcf 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
@@ -1811,7 +1811,13 @@
     getFromProjectLookup(changeNum) {
       const project = this._projectLookup[changeNum];
       if (project) { return Promise.resolve(project); }
-      return this.getChange(changeNum).then(change => {
+
+      const onError = response => {
+        // Fire a page error so that the visual 404 is displayed.
+        this.fire('page-error', {response});
+      };
+
+      return this.getChange(changeNum, onError).then(change => {
         if (!change || !change.project) { return; }
         this.setInProjectLookup(changeNum, change.project);
         return change.project;
@@ -1842,7 +1848,7 @@
 
    /**
     * Alias for _changeBaseURL.then(fetchJSON).
-     * @todo(beckysiegel) clean up comments
+    * @todo(beckysiegel) clean up comments
     * @param {string|number} changeNum
     * @param {string} endpoint
     * @param {?string|number=} opt_patchNum gets passed as null.
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 afa1c53..eb4f418 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
@@ -865,8 +865,7 @@
       });
 
       test('getChange succeeds, no project', () => {
-        sandbox.stub(element, 'getChange')
-            .returns(Promise.resolve({}));
+        sandbox.stub(element, 'getChange').returns(Promise.resolve());
         return element.getFromProjectLookup().then(val => {
           assert.strictEqual(val, undefined);
           assert.deepEqual(element._projectLookup, {});