Implement _projectLookup in gr-rest-api-interface

This change creates a new object, _projectLookup, for use as a shortcut
in supporting slicer.

It is a map of changeNums to projects, with added logic to fetch the
change and populate the lookup if it has not already been populated.

This object is then populated with data in the router and rest API.

Bug: Issue 6708
Change-Id: Ide5bf94539d7e97c929d17505deb59aaf0e6c192
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 f0bf6b0..8c9989c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -428,6 +428,7 @@
           normalizePatchRangeParams(params);
           app.params = params;
           upgradeUrl(params);
+          restAPI.setInProjectLookup(params.changeNum, params.project);
         });
 
     // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
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 e688d82..4119081 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
@@ -75,6 +75,13 @@
         type: Object,
         value: new GrEtagDecorator(), // Share across instances.
       },
+      /**
+       * Used to maintain a mapping of changeNums to project names.
+       */
+      _projectLookup: {
+        type: Object,
+        value: {}, // Intentional to share the object across instances.
+      },
     },
 
     JSON_PREFIX,
@@ -547,7 +554,19 @@
       if (opt_query && opt_query.length > 0) {
         params.q = opt_query;
       }
-      return this.fetchJSON('/changes/', null, null, params);
+      return this.fetchJSON('/changes/', null, null, params)
+          .then(response => {
+            // Response is an array of arrays of changes. Iterate over each and
+            // set in _projectLookup.
+            for (const arr of (response || [])) {
+              for (const change of (arr || [])) {
+                if (change && change.project) {
+                  this.setInProjectLookup(change._number, change.project);
+                }
+              }
+            }
+            return response;
+          });
     },
 
     getChangeActionURL(changeNum, opt_patchNum, endpoint) {
@@ -593,9 +612,13 @@
                   this._etags.getCachedPayload(urlWithParams));
             } else {
               const payloadPromise = response ?
-                    this.getResponseObject(response) : Promise.resolve();
+                  this.getResponseObject(response) :
+                  Promise.resolve();
               payloadPromise.then(payload => {
                 this._etags.collect(urlWithParams, response, payload);
+                if (payload && payload.project) {
+                  this.setInProjectLookup(payload._number, payload.project);
+                }
               });
               return payloadPromise;
             }
@@ -1316,5 +1339,46 @@
       return this.send('POST', url, {reason}).then(response =>
         this.getResponseObject(response));
     },
+
+    /**
+     * Given a changeNum, gets the change.
+     *
+     * @param {string} changeNum
+     * @return {Promise<Object>} The change
+     */
+    getChange(changeNum) {
+      return this.fetchJSON(`/changes/${changeNum}`);
+    },
+
+    /**
+     * @param {string|number} changeNum
+     * @param {string} project
+     */
+    setInProjectLookup(changeNum, project) {
+      if (this._projectLookup[changeNum] &&
+          this._projectLookup[changeNum] !== project) {
+        console.warn('Change set with multiple project nums.' +
+            'One of them must be invalid.');
+      }
+      this._projectLookup[changeNum] = project;
+    },
+
+    /**
+     * Checks in _projectLookup for the changeNum. If it exists, returns the
+     * project. If not, calls the restAPI to get the change, populates
+     * _projectLookup with the project for that change, and returns the project.
+     *
+     * @param {string|number} changeNum
+     * @return {Promise<string>}
+     */
+    _getFromProjectLookup(changeNum) {
+      const project = this._projectLookup[changeNum];
+      if (project) { return Promise.resolve(project); }
+      return this.getChange(changeNum).then(change => {
+        if (!change || !change.project) { return; }
+        this.setInProjectLookup(changeNum, change.project);
+        return change.project;
+      });
+    },
   });
 })();
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 2cf1fa6..e4c4a5a 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
@@ -42,6 +42,7 @@
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
       element._cache = {};
+      element._projectLookup = {};
       const testJSON = ')]}\'\n{"hello": "bonjour"}';
       sandbox.stub(window, 'fetch').returns(Promise.resolve({
         ok: true,
@@ -480,7 +481,8 @@
     });
 
     test('legacy n,z key in change url is replaced', () => {
-      const stub = sandbox.stub(element, 'fetchJSON');
+      const stub = sandbox.stub(element, 'fetchJSON')
+          .returns(Promise.resolve([]));
       element.getChanges(1, null, 'n,z');
       assert.equal(stub.args[0][3].S, 0);
     });
@@ -781,5 +783,67 @@
         assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
       });
     });
+
+    test('setInProjectLookup', () => {
+      element.setInProjectLookup('test', 'project');
+      assert.deepEqual(element._projectLookup, {test: 'project'});
+    });
+
+    suite('_getFromProjectLookup', () => {
+      test('getChange fails', () => {
+        sandbox.stub(element, 'getChange')
+            .returns(Promise.resolve());
+        return element._getFromProjectLookup().then(val => {
+          assert.strictEqual(val, undefined);
+          assert.deepEqual(element._projectLookup, {});
+        });
+      });
+
+      test('getChange succeeds, no project', () => {
+        sandbox.stub(element, 'getChange')
+            .returns(Promise.resolve({}));
+        return element._getFromProjectLookup().then(val => {
+          assert.strictEqual(val, undefined);
+          assert.deepEqual(element._projectLookup, {});
+        });
+      });
+
+      test('getChange succeeds with project', () => {
+        sandbox.stub(element, 'getChange')
+            .returns(Promise.resolve({project: 'project'}));
+        return element._getFromProjectLookup('test').then(val => {
+          assert.equal(val, 'project');
+          assert.deepEqual(element._projectLookup, {test: 'project'});
+        });
+      });
+    });
+
+    test('getChanges populates _projectLookup', () => {
+      sandbox.stub(element, 'fetchJSON')
+          .returns(Promise.resolve([
+            [
+              {_number: 1, project: 'test'},
+              {_number: 2, project: 'test'},
+            ], [
+              {_number: 3, project: 'test/test'},
+            ],
+          ]));
+      return element.getChanges().then(() => {
+        assert.equal(Object.keys(element._projectLookup).length, 3);
+        assert.equal(element._projectLookup[1], 'test');
+        assert.equal(element._projectLookup[2], 'test');
+        assert.equal(element._projectLookup[3], 'test/test');
+      });
+    });
+
+    test('getChangeDetail populates _projectLookup', () => {
+      sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve(true));
+      sandbox.stub(element, 'getResponseObject')
+          .returns(Promise.resolve({_number: 1, project: 'test'}));
+      return element._getChangeDetail().then(() => {
+        assert.equal(Object.keys(element._projectLookup).length, 1);
+        assert.equal(element._projectLookup[1], 'test');
+      });
+    });
   });
 </script>