Invalid token error when trying to create an A/B analysis for a range
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Jun 2016 19:47:38 +0000 (19:47 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 13 Jun 2016 19:47:38 +0000 (19:47 +0000)
https://bugs.webkit.org/show_bug.cgi?id=158679

Reviewed by Chris Dumez.

The problem in this particular case was due to another website overriding cookies for our subdomain.
Make PrivilegedAPI robust against its token becoming invalid in general to fix the bug since the cookie
is only available under /privileged-api/ and the v3 UI can't access it for security reasons.

This patch factors out PrivilegedAPI out of remote.js so that it can be tested separately in server tests
as well as unit tests even though RemoteAPI itself is implemented differently in each case.

* init-database.sql: Added a forgotten default value "false" to run_marked_outlier.
* public/v3/index.html:
* public/v3/privileged-api.js: Added. Extracted out of public/v3/remote.js.
(PrivilegedAPI.sendRequest): Fixed the bug. When the initial request fails with "InvalidToken" error,
re-generate the token and re-issue the request.
(PrivilegedAPI.requestCSRFToken):
* public/v3/remote.js:
(RemoteAPI.postJSON): Added to match tools/js/remote.js.
(RemoteAPI.postJSONWithStatus): Ditto.
(PrivilegedAPI): Moved to privileged-api.js.
* server-tests/api-measurement-set-tests.js: Removed the unused require for crypto.
* server-tests/privileged-api-upate-run-status.js: Added tests for /privileged-api/update-run-status.
* server-tests/resources/test-server.js:
(TestServer.prototype.inject): Clear the cookies as well as tokens in PrivilegedAPI.
* tools/js/remote.js:
(RemoteAPI): Added the support for PrivilegedAPI by making cookie set by the server persist.
(RemoteAPI.prototype.clearCookies): Added for tests.
(RemoteAPI.prototype.postJSON): Make sure sendHttpRequest always sends a valid JSON.
(RemoteAPI.prototype.postJSONWithStatus): Added since this API is used PrivilegedAPI.
(RemoteAPI.prototype.sendHttpRequest): Retain the cookie set by the server and send it back in each request.
* tools/js/v3-models.js:
* unit-tests/privileged-api-tests.js: Added unit tests for PrivilegedAPI.
* unit-tests/resources/mock-remote-api.js:
(MockRemoteAPI.postJSON): Added for unit testing.
(MockRemoteAPI.postJSONWithStatus): Ditto.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@202001 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/privileged-api.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/remote.js
Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js
Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/resources/test-server.js
Websites/perf.webkit.org/tools/js/remote.js
Websites/perf.webkit.org/tools/js/v3-models.js
Websites/perf.webkit.org/unit-tests/privileged-api-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js

index c39e872b4035871a3029e92564be7af144d20fc5..a2f563cbd21e16ee7ab1b3e865e0b3044103da37 100644 (file)
@@ -1,3 +1,43 @@
+2016-06-13  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Invalid token error when trying to create an A/B analysis for a range
+        https://bugs.webkit.org/show_bug.cgi?id=158679
+
+        Reviewed by Chris Dumez.
+
+        The problem in this particular case was due to another website overriding cookies for our subdomain.
+        Make PrivilegedAPI robust against its token becoming invalid in general to fix the bug since the cookie
+        is only available under /privileged-api/ and the v3 UI can't access it for security reasons.
+
+        This patch factors out PrivilegedAPI out of remote.js so that it can be tested separately in server tests
+        as well as unit tests even though RemoteAPI itself is implemented differently in each case.
+
+        * init-database.sql: Added a forgotten default value "false" to run_marked_outlier.
+        * public/v3/index.html:
+        * public/v3/privileged-api.js: Added. Extracted out of public/v3/remote.js.
+        (PrivilegedAPI.sendRequest): Fixed the bug. When the initial request fails with "InvalidToken" error,
+        re-generate the token and re-issue the request.
+        (PrivilegedAPI.requestCSRFToken):
+        * public/v3/remote.js:
+        (RemoteAPI.postJSON): Added to match tools/js/remote.js.
+        (RemoteAPI.postJSONWithStatus): Ditto.
+        (PrivilegedAPI): Moved to privileged-api.js.
+        * server-tests/api-measurement-set-tests.js: Removed the unused require for crypto.
+        * server-tests/privileged-api-upate-run-status.js: Added tests for /privileged-api/update-run-status.
+        * server-tests/resources/test-server.js:
+        (TestServer.prototype.inject): Clear the cookies as well as tokens in PrivilegedAPI.
+        * tools/js/remote.js:
+        (RemoteAPI): Added the support for PrivilegedAPI by making cookie set by the server persist.
+        (RemoteAPI.prototype.clearCookies): Added for tests.
+        (RemoteAPI.prototype.postJSON): Make sure sendHttpRequest always sends a valid JSON.
+        (RemoteAPI.prototype.postJSONWithStatus): Added since this API is used PrivilegedAPI.
+        (RemoteAPI.prototype.sendHttpRequest): Retain the cookie set by the server and send it back in each request.
+        * tools/js/v3-models.js:
+        * unit-tests/privileged-api-tests.js: Added unit tests for PrivilegedAPI.
+        * unit-tests/resources/mock-remote-api.js:
+        (MockRemoteAPI.postJSON): Added for unit testing.
+        (MockRemoteAPI.postJSONWithStatus): Ditto.
+
 2016-06-13  Ryosuke Niwa  <rniwa@webkit.org>
 
         /admin/tests is very slow
index e987493f2436710d3ac76bfb7324e6e03198a261..2b56ce2267beb4b153361d676ca3683e61cec58a 100644 (file)
@@ -140,7 +140,7 @@ CREATE TABLE test_runs (
     run_mean_cache double precision,
     run_sum_cache double precision,
     run_square_sum_cache double precision,
-    run_marked_outlier boolean,
+    run_marked_outlier boolean NOT NULL DEFAULT FALSE,
     CONSTRAINT test_config_build_must_be_unique UNIQUE(run_config, run_build));
 CREATE INDEX run_config_index ON test_runs(run_config);
 CREATE INDEX run_build_index ON test_runs(run_build);
index 3d11068ba790a8e13b43cf075966624d5d9245cd..fa92186490cb888ef3f4d568c4c58073c5941580 100644 (file)
@@ -42,6 +42,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
 
         <script src="instrumentation.js"></script>
         <script src="remote.js"></script>
+        <script src="privileged-api.js"></script>
 
         <script src="models/time-series.js"></script>
         <script src="models/measurement-adaptor.js"></script>
diff --git a/Websites/perf.webkit.org/public/v3/privileged-api.js b/Websites/perf.webkit.org/public/v3/privileged-api.js
new file mode 100644 (file)
index 0000000..768d15e
--- /dev/null
@@ -0,0 +1,45 @@
+"use strict";
+
+// FIXME: Use real class syntax once the dependency on data.js has been removed.
+var PrivilegedAPI = class {
+
+    static sendRequest(path, data)
+    {
+        var clonedData = {};
+        for (var key in data)
+            clonedData[key] = data[key];
+
+        return this.requestCSRFToken().then(function (token) {
+            clonedData['token'] = token;
+            return RemoteAPI.postJSONWithStatus('/privileged-api/' + path, clonedData).catch(function (status) {
+                if (status != 'InvalidToken')
+                    return Promise.reject(status);
+                PrivilegedAPI._token = null;
+                return PrivilegedAPI.requestCSRFToken().then(function (token) {
+                    clonedData['token'] = token;
+                    return RemoteAPI.postJSONWithStatus('/privileged-api/' + path, clonedData);
+                });
+            });
+        });
+    }
+
+    static requestCSRFToken()
+    {
+        var maxNetworkLatency = 3 * 60 * 1000; /* 3 minutes */
+        if (this._token && this._expiration > Date.now() + maxNetworkLatency)
+            return Promise.resolve(this._token);
+
+        return RemoteAPI.postJSONWithStatus('/privileged-api/generate-csrf-token').then(function (result) {
+            PrivilegedAPI._token = result['token'];
+            PrivilegedAPI._expiration = new Date(result['expiration']);
+            return PrivilegedAPI._token;
+        });
+    }
+
+}
+
+PrivilegedAPI._token = null;
+PrivilegedAPI._expiration = null;
+
+if (typeof module != 'undefined')
+    module.exports.PrivilegedAPI = PrivilegedAPI;
index 02e416bc4c1368e28e1d896120d9378f2c2de947..02ee949e7ea069f2e1e15a059c81825e2fbc2e96 100644 (file)
@@ -1,6 +1,18 @@
+"use strict";
 
 var RemoteAPI = {};
 
+RemoteAPI.postJSON = function (path, data)
+{
+    return this.getJSON(path, data || {});
+}
+
+RemoteAPI.postJSONWithStatus = function (path, data)
+{
+    console.log(document.cookie);
+    return this.getJSONWithStatus(path, data || {});
+}
+
 RemoteAPI.getJSON = function(path, data)
 {
     console.assert(!path.startsWith('http:') && !path.startsWith('https:') && !path.startsWith('file:'));
@@ -53,35 +65,3 @@ RemoteAPI.getJSONWithStatus = function(path, data)
         return content;
     });
 }
-
-// FIXME: Use real class syntax once the dependency on data.js has been removed.
-PrivilegedAPI = class {
-
-    static sendRequest(path, data)
-    {
-        return this.requestCSRFToken().then(function (token) {
-            var clonedData = {};
-            for (var key in data)
-                clonedData[key] = data[key];
-            clonedData['token'] = token;
-            return RemoteAPI.getJSONWithStatus('../privileged-api/' + path, clonedData);
-        });
-    }
-
-    static requestCSRFToken()
-    {
-        var maxNetworkLatency = 3 * 60 * 1000; /* 3 minutes */
-        if (this._token && this._expiration > Date.now() + maxNetworkLatency)
-            return Promise.resolve(this._token);
-
-        return RemoteAPI.getJSONWithStatus('../privileged-api/generate-csrf-token', {}).then(function (result) {
-            PrivilegedAPI._token = result['token'];
-            PrivilegedAPI._expiration = new Date(result['expiration']);
-            return PrivilegedAPI._token;
-        });
-    }
-
-}
-
-PrivilegedAPI._token = null;
-PrivilegedAPI._expiration = null;
index f0b27e8e075c6a6232c74d34b87b0b378e923cfa..cdeeaf2e35bfb5fff16640dce9500444d58c9c87 100644 (file)
@@ -1,7 +1,6 @@
 'use strict';
 
 const assert = require('assert');
-const crypto = require('crypto');
 
 const TestServer = require('./resources/test-server.js');
 const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
diff --git a/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js b/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js
new file mode 100644 (file)
index 0000000..ba6c666
--- /dev/null
@@ -0,0 +1,134 @@
+'use strict';
+
+require('../tools/js/v3-models.js');
+
+const assert = require('assert');
+
+const TestServer = require('./resources/test-server.js');
+const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
+const connectToDatabaseInEveryTest = require('./resources/common-operations.js').connectToDatabaseInEveryTest;
+
+describe("/privileged-api/update-run-status", function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    const reportWithRevision = [{
+        "buildNumber": "124",
+        "buildTime": "2013-02-28T15:34:51",
+        "revisions": {
+            "WebKit": {
+                "revision": "191622",
+                "timestamp": (new Date(1445945816878)).toISOString(),
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "some platform",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [11] }}
+                    }
+                }
+            },
+        }}];
+
+    it("should be able to mark a run as an outlier", function (done) {
+        const db = TestServer.database();
+        let id;
+        addBuilderForReport(reportWithRevision[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['mean_cache'], 11);
+            assert.equal(runRows[0]['iteration_count_cache'], 1);
+            assert.equal(runRows[0]['marked_outlier'], false);
+            id = runRows[0]['id'];
+            return PrivilegedAPI.requestCSRFToken();
+        }).then(function () {
+            return PrivilegedAPI.sendRequest('update-run-status', {'run': id, 'markedOutlier': true, 'token': PrivilegedAPI._token});
+        }).then(function () {
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['mean_cache'], 11);
+            assert.equal(runRows[0]['iteration_count_cache'], 1);
+            assert.equal(runRows[0]['marked_outlier'], true);
+            done();
+        }).catch(done);
+    });
+
+    it("should reject when the token is not set in cookie", function (done) {
+        const db = TestServer.database();
+        addBuilderForReport(reportWithRevision[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['marked_outlier'], false);
+            return PrivilegedAPI.requestCSRFToken();
+        }).then(function () {
+            RemoteAPI.clearCookies();
+            return RemoteAPI.postJSONWithStatus('/privileged-api/update-run-status', {token: PrivilegedAPI._token});
+        }).then(function () {
+            assert(false, 'PrivilegedAPI.sendRequest should reject');
+        }, function (response) {
+            assert.equal(response['status'], 'InvalidToken');
+            done();
+        }).catch(done);
+    });
+
+    it("should reject when the token in the request content is bad", function (done) {
+        const db = TestServer.database();
+        addBuilderForReport(reportWithRevision[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['marked_outlier'], false);
+            return PrivilegedAPI.requestCSRFToken();
+        }).then(function () {
+            return RemoteAPI.postJSONWithStatus('/privileged-api/update-run-status', {token: 'bad'});
+        }).then(function () {
+            assert(false, 'PrivilegedAPI.sendRequest should reject');
+        }, function (response) {
+            assert.equal(response['status'], 'InvalidToken');
+            done();
+        }).catch(done);
+    });
+
+    it("should be able to unmark a run as an outlier", function (done) {
+        const db = TestServer.database();
+        addBuilderForReport(reportWithRevision[0]).then(function () {
+            return TestServer.remoteAPI().postJSON('/api/report/', reportWithRevision);
+        }).then(function (response) {
+            assert.equal(response['status'], 'OK');
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['marked_outlier'], false);
+            return PrivilegedAPI.sendRequest('update-run-status', {'run': runRows[0]['id'], 'markedOutlier': true});
+        }).then(function () {
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['marked_outlier'], true);
+            return PrivilegedAPI.sendRequest('update-run-status', {'run': runRows[0]['id'], 'markedOutlier': false});
+        }).then(function () {
+            return db.selectAll('test_runs');
+        }).then(function (runRows) {
+            assert.equal(runRows.length, 1);
+            assert.equal(runRows[0]['marked_outlier'], false);
+            done();
+        }).catch(done);
+    });
+});
index 4fbbdd0409e5585b04eb8c2efe57a6d6803c2df6..88d25196d3c3e62252d4d401b623cc3df7da84eb 100644 (file)
@@ -233,6 +233,12 @@ class TestServer {
             self.cleanDataDirectory();
             originalRemote = global.RemoteAPI;
             global.RemoteAPI = self._remote;
+            self._remote.clearCookies();
+
+            if (global.PrivilegedAPI) {
+                global.PrivilegedAPI._token = null;
+                global.PrivilegedAPI._expiration = null;
+            }
         });
 
         after(function () {
index 14605f1eed63f00420ba6294344deaadc6b45565..e16e5f8203680233afc47956386430af5e74b1fb 100644 (file)
@@ -9,6 +9,7 @@ class RemoteAPI {
     constructor(server)
     {
         this._server = null;
+        this._cookies = new Map;
         if (server)
             this.configure(server);
     }
@@ -50,6 +51,8 @@ class RemoteAPI {
         };
     }
 
+    clearCookies() { this._cookies = new Map; }
+
     getJSON(path)
     {
         return this.sendHttpRequest(path, 'GET', null, null).then(function (result) {
@@ -69,7 +72,7 @@ class RemoteAPI {
     postJSON(path, data)
     {
         const contentType = 'application/json';
-        const payload = JSON.stringify(data);
+        const payload = JSON.stringify(data || {});
         return this.sendHttpRequest(path, 'POST', 'application/json', payload).then(function (result) {
             try {
                 return JSON.parse(result.responseText);
@@ -80,6 +83,15 @@ class RemoteAPI {
         });
     }
 
+    postJSONWithStatus(path, data)
+    {
+        return this.postJSON(path, data).then(function (result) {
+            if (result['status'] != 'OK')
+                return Promise.reject(result);
+            return result;
+        });
+    }
+
     postFormUrlencodedData(path, data)
     {
         const contentType = 'application/x-www-form-urlencoded';
@@ -92,6 +104,7 @@ class RemoteAPI {
     sendHttpRequest(path, method, contentType, content)
     {
         let server = this._server;
+        const self = this;
         return new Promise(function (resolve, reject) {
             let options = {
                 hostname: server.host,
@@ -105,7 +118,15 @@ class RemoteAPI {
                 let responseText = '';
                 response.setEncoding('utf8');
                 response.on('data', function (chunk) { responseText += chunk; });
-                response.on('end', function () { resolve({statusCode: response.statusCode, responseText: responseText}); });
+                response.on('end', function () {
+                    if ('set-cookie' in response.headers) {
+                        for (const cookie of response.headers['set-cookie']) {
+                            var nameValue = cookie.split('=')
+                            self._cookies.set(nameValue[0], nameValue[1]);
+                        }
+                    }
+                    resolve({statusCode: response.statusCode, responseText: responseText});
+                });
             });
 
             request.on('error', reject);
@@ -113,6 +134,12 @@ class RemoteAPI {
             if (contentType)
                 request.setHeader('Content-Type', contentType);
 
+            if (self._cookies.size) {
+                request.setHeader('Cookie', Array.from(self._cookies.keys()).map(function (key) {
+                    return `${key}=${self._cookies.get(key)}`;
+                }).join('; '));
+            }
+
             if (content)
                 request.write(content);
 
index a43841475971013d8c0a4d0eb511af9e18107495..826e2b09e4be7bdb06fdb748f4240f11111b76f8 100644 (file)
@@ -27,6 +27,7 @@ importFromV3('models/root-set.js', 'RootSet');
 importFromV3('models/test.js', 'Test');
 importFromV3('models/test-group.js', 'TestGroup');
 
+importFromV3('privileged-api.js', 'PrivilegedAPI');
 importFromV3('instrumentation.js', 'Instrumentation');
 
 global.Statistics = require('../../public/shared/statistics.js');
diff --git a/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js b/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js
new file mode 100644 (file)
index 0000000..83985b7
--- /dev/null
@@ -0,0 +1,182 @@
+'use strict';
+
+const assert = require('assert');
+
+let MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
+require('../tools/js/v3-models.js');
+
+describe('PrivilegedAPI', function () {
+    let requests = MockRemoteAPI.inject();
+
+    beforeEach(function () {
+        PrivilegedAPI._token = null;
+    })
+
+    describe('requestCSRFToken', function () {
+        it('should generate a new token', function () {
+            PrivilegedAPI.requestCSRFToken();
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+        });
+
+        it('should not generate a new token if the existing token had not expired', function (done) {
+            const tokenRequest = PrivilegedAPI.requestCSRFToken();
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() + 3600 * 1000,
+            });
+            tokenRequest.then(function (token) {
+                assert.equal(token, 'abc');
+                PrivilegedAPI.requestCSRFToken();
+                assert.equal(requests.length, 1);
+                done();
+            }).catch(done);
+        });
+
+        it('should generate a new token if the existing token had already expired', function (done) {
+            const tokenRequest = PrivilegedAPI.requestCSRFToken();
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() - 1,
+            });
+            tokenRequest.then(function (token) {
+                assert.equal(token, 'abc');
+                PrivilegedAPI.requestCSRFToken();
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, '/privileged-api/generate-csrf-token');
+                done();
+            }).catch(done);
+        });
+    });
+    
+    describe('sendRequest', function () {
+
+        it('should generate a new token if no token had been fetched', function (done) {
+            PrivilegedAPI.sendRequest('test', {});
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() + 100 * 1000,
+            });
+            Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, '/privileged-api/test');
+                done();
+            }).catch(done);
+        });
+
+        it('should not generate a new token if the existing token had not been expired', function (done) {
+            PrivilegedAPI.sendRequest('test', {});
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() + 3600 * 1000,
+            });
+            Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].url, '/privileged-api/test');
+                PrivilegedAPI.sendRequest('test2', {});
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 3);
+                assert.equal(requests[2].url, '/privileged-api/test2');
+                done();
+            }).catch(done);
+        });
+
+        it('should reject immediately when a token generation had failed', function (done) {
+            const request = PrivilegedAPI.sendRequest('test', {});
+            let caught = false;
+            request.catch(function () { caught = true; });
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].reject({status: 'FailedToGenerateToken'});
+            Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 1);
+                assert(caught);
+                done();
+            }).catch(done);
+        });
+
+        it('should re-generate token when it had become invalid', function (done) {
+            PrivilegedAPI.sendRequest('test', {});
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() + 3600 * 1000,
+            });
+            Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].data.token, 'abc');
+                assert.equal(requests[1].url, '/privileged-api/test');
+                requests[1].reject('InvalidToken');
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 3);
+                assert.equal(requests[2].url, '/privileged-api/generate-csrf-token');
+                requests[2].resolve({
+                    token: 'def',
+                    expiration: Date.now() + 3600 * 1000,
+                });
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 4);
+                assert.equal(requests[3].data.token, 'def');
+                assert.equal(requests[3].url, '/privileged-api/test');
+                done();
+            }).catch(done);
+        });
+
+        it('should not re-generate token when the re-fetched token was invalid', function (done) {
+            PrivilegedAPI.sendRequest('test', {});
+            assert.equal(requests.length, 1);
+            assert.equal(requests[0].url, '/privileged-api/generate-csrf-token');
+            requests[0].resolve({
+                token: 'abc',
+                expiration: Date.now() + 3600 * 1000,
+            });
+            Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 2);
+                assert.equal(requests[1].data.token, 'abc');
+                assert.equal(requests[1].url, '/privileged-api/test');
+                requests[1].reject('InvalidToken');
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 3);
+                assert.equal(requests[2].url, '/privileged-api/generate-csrf-token');
+                requests[2].resolve({
+                    token: 'def',
+                    expiration: Date.now() + 3600 * 1000,
+                });
+                return Promise.resolve();
+            }).then(function () {
+                assert.equal(requests.length, 4);
+                assert.equal(requests[3].data.token, 'def');
+                assert.equal(requests[3].url, '/privileged-api/test');
+                requests[3].reject('InvalidToken');
+            }).then(function () {
+                assert.equal(requests.length, 4);
+                done();
+            }).catch(done);
+        });
+
+    });
+
+});
index 5fb93c9ca61a6c97f0b1b32317acda1dd14fcb6c..3e8f8244202f2ea990114ca852b9febbe6bcb572 100644 (file)
@@ -16,6 +16,14 @@ var MockRemoteAPI = {
     {
         return this._addRequest(url, 'GET', null);
     },
+    postJSON: function (url, data)
+    {
+        return this._addRequest(url, 'POST', data);
+    },
+    postJSONWithStatus: function (url, data)
+    {
+        return this._addRequest(url, 'POST', data);
+    },
     postFormUrlencodedData: function (url, data)
     {
         return this._addRequest(url, 'POST', data);