<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[202001] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/202001">202001</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-06-13 12:47:38 -0700 (Mon, 13 Jun 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>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 &quot;false&quot; 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 &quot;InvalidToken&quot; 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.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3remotejs">trunk/Websites/perf.webkit.org/public/v3/remote.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsapimeasurementsettestsjs">trunk/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsresourcestestserverjs">trunk/Websites/perf.webkit.org/server-tests/resources/test-server.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsremotejs">trunk/Websites/perf.webkit.org/tools/js/remote.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsv3modelsjs">trunk/Websites/perf.webkit.org/tools/js/v3-models.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsresourcesmockremoteapijs">trunk/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3privilegedapijs">trunk/Websites/perf.webkit.org/public/v3/privileged-api.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsprivilegedapiupaterunstatusjs">trunk/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsprivilegedapitestsjs">trunk/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -1,5 +1,45 @@
</span><span class="cx"> 2016-06-13  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><ins>+        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 &quot;false&quot; 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 &quot;InvalidToken&quot; 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  &lt;rniwa@webkit.org&gt;
+
</ins><span class="cx">         /admin/tests is very slow
</span><span class="cx">         https://bugs.webkit.org/show_bug.cgi?id=158682
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -140,7 +140,7 @@
</span><span class="cx">     run_mean_cache double precision,
</span><span class="cx">     run_sum_cache double precision,
</span><span class="cx">     run_square_sum_cache double precision,
</span><del>-    run_marked_outlier boolean,
</del><ins>+    run_marked_outlier boolean NOT NULL DEFAULT FALSE,
</ins><span class="cx">     CONSTRAINT test_config_build_must_be_unique UNIQUE(run_config, run_build));
</span><span class="cx"> CREATE INDEX run_config_index ON test_runs(run_config);
</span><span class="cx"> CREATE INDEX run_build_index ON test_runs(run_build);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -42,6 +42,7 @@
</span><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;instrumentation.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;remote.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;privileged-api.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;models/time-series.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;models/measurement-adaptor.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3privilegedapijs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/privileged-api.js (0 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/privileged-api.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/privileged-api.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -0,0 +1,45 @@
</span><ins>+&quot;use strict&quot;;
+
+// 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 &amp;&amp; this._expiration &gt; 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;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3remotejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/remote.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/remote.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/public/v3/remote.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -1,6 +1,18 @@
</span><ins>+&quot;use strict&quot;;
</ins><span class="cx"> 
</span><span class="cx"> var RemoteAPI = {};
</span><span class="cx"> 
</span><ins>+RemoteAPI.postJSON = function (path, data)
+{
+    return this.getJSON(path, data || {});
+}
+
+RemoteAPI.postJSONWithStatus = function (path, data)
+{
+    console.log(document.cookie);
+    return this.getJSONWithStatus(path, data || {});
+}
+
</ins><span class="cx"> RemoteAPI.getJSON = function(path, data)
</span><span class="cx"> {
</span><span class="cx">     console.assert(!path.startsWith('http:') &amp;&amp; !path.startsWith('https:') &amp;&amp; !path.startsWith('file:'));
</span><span class="lines">@@ -53,35 +65,3 @@
</span><span class="cx">         return content;
</span><span class="cx">     });
</span><span class="cx"> }
</span><del>-
-// 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 &amp;&amp; this._expiration &gt; 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;
</del></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsapimeasurementsettestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/server-tests/api-measurement-set-tests.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -1,7 +1,6 @@
</span><span class="cx"> 'use strict';
</span><span class="cx"> 
</span><span class="cx"> const assert = require('assert');
</span><del>-const crypto = require('crypto');
</del><span class="cx"> 
</span><span class="cx"> const TestServer = require('./resources/test-server.js');
</span><span class="cx"> const addBuilderForReport = require('./resources/common-operations.js').addBuilderForReport;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsprivilegedapiupaterunstatusjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js (0 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/server-tests/privileged-api-upate-run-status.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -0,0 +1,134 @@
</span><ins>+'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(&quot;/privileged-api/update-run-status&quot;, function () {
+    this.timeout(1000);
+    TestServer.inject();
+    connectToDatabaseInEveryTest();
+
+    const reportWithRevision = [{
+        &quot;buildNumber&quot;: &quot;124&quot;,
+        &quot;buildTime&quot;: &quot;2013-02-28T15:34:51&quot;,
+        &quot;revisions&quot;: {
+            &quot;WebKit&quot;: {
+                &quot;revision&quot;: &quot;191622&quot;,
+                &quot;timestamp&quot;: (new Date(1445945816878)).toISOString(),
+            },
+        },
+        &quot;builderName&quot;: &quot;someBuilder&quot;,
+        &quot;builderPassword&quot;: &quot;somePassword&quot;,
+        &quot;platform&quot;: &quot;some platform&quot;,
+        &quot;tests&quot;: {
+            &quot;Suite&quot;: {
+                &quot;tests&quot;: {
+                    &quot;test1&quot;: {
+                        &quot;metrics&quot;: {&quot;Time&quot;: { &quot;current&quot;: [11] }}
+                    }
+                }
+            },
+        }}];
+
+    it(&quot;should be able to mark a run as an outlier&quot;, 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(&quot;should reject when the token is not set in cookie&quot;, 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(&quot;should reject when the token in the request content is bad&quot;, 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(&quot;should be able to unmark a run as an outlier&quot;, 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);
+    });
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsresourcestestserverjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/resources/test-server.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/resources/test-server.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/server-tests/resources/test-server.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -233,6 +233,12 @@
</span><span class="cx">             self.cleanDataDirectory();
</span><span class="cx">             originalRemote = global.RemoteAPI;
</span><span class="cx">             global.RemoteAPI = self._remote;
</span><ins>+            self._remote.clearCookies();
+
+            if (global.PrivilegedAPI) {
+                global.PrivilegedAPI._token = null;
+                global.PrivilegedAPI._expiration = null;
+            }
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         after(function () {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsremotejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/remote.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/remote.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/tools/js/remote.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -9,6 +9,7 @@
</span><span class="cx">     constructor(server)
</span><span class="cx">     {
</span><span class="cx">         this._server = null;
</span><ins>+        this._cookies = new Map;
</ins><span class="cx">         if (server)
</span><span class="cx">             this.configure(server);
</span><span class="cx">     }
</span><span class="lines">@@ -50,6 +51,8 @@
</span><span class="cx">         };
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    clearCookies() { this._cookies = new Map; }
+
</ins><span class="cx">     getJSON(path)
</span><span class="cx">     {
</span><span class="cx">         return this.sendHttpRequest(path, 'GET', null, null).then(function (result) {
</span><span class="lines">@@ -69,7 +72,7 @@
</span><span class="cx">     postJSON(path, data)
</span><span class="cx">     {
</span><span class="cx">         const contentType = 'application/json';
</span><del>-        const payload = JSON.stringify(data);
</del><ins>+        const payload = JSON.stringify(data || {});
</ins><span class="cx">         return this.sendHttpRequest(path, 'POST', 'application/json', payload).then(function (result) {
</span><span class="cx">             try {
</span><span class="cx">                 return JSON.parse(result.responseText);
</span><span class="lines">@@ -80,6 +83,15 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    postJSONWithStatus(path, data)
+    {
+        return this.postJSON(path, data).then(function (result) {
+            if (result['status'] != 'OK')
+                return Promise.reject(result);
+            return result;
+        });
+    }
+
</ins><span class="cx">     postFormUrlencodedData(path, data)
</span><span class="cx">     {
</span><span class="cx">         const contentType = 'application/x-www-form-urlencoded';
</span><span class="lines">@@ -92,6 +104,7 @@
</span><span class="cx">     sendHttpRequest(path, method, contentType, content)
</span><span class="cx">     {
</span><span class="cx">         let server = this._server;
</span><ins>+        const self = this;
</ins><span class="cx">         return new Promise(function (resolve, reject) {
</span><span class="cx">             let options = {
</span><span class="cx">                 hostname: server.host,
</span><span class="lines">@@ -105,7 +118,15 @@
</span><span class="cx">                 let responseText = '';
</span><span class="cx">                 response.setEncoding('utf8');
</span><span class="cx">                 response.on('data', function (chunk) { responseText += chunk; });
</span><del>-                response.on('end', function () { resolve({statusCode: response.statusCode, responseText: responseText}); });
</del><ins>+                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});
+                });
</ins><span class="cx">             });
</span><span class="cx"> 
</span><span class="cx">             request.on('error', reject);
</span><span class="lines">@@ -113,6 +134,12 @@
</span><span class="cx">             if (contentType)
</span><span class="cx">                 request.setHeader('Content-Type', contentType);
</span><span class="cx"> 
</span><ins>+            if (self._cookies.size) {
+                request.setHeader('Cookie', Array.from(self._cookies.keys()).map(function (key) {
+                    return `${key}=${self._cookies.get(key)}`;
+                }).join('; '));
+            }
+
</ins><span class="cx">             if (content)
</span><span class="cx">                 request.write(content);
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsv3modelsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -27,6 +27,7 @@
</span><span class="cx"> importFromV3('models/test.js', 'Test');
</span><span class="cx"> importFromV3('models/test-group.js', 'TestGroup');
</span><span class="cx"> 
</span><ins>+importFromV3('privileged-api.js', 'PrivilegedAPI');
</ins><span class="cx"> importFromV3('instrumentation.js', 'Instrumentation');
</span><span class="cx"> 
</span><span class="cx"> global.Statistics = require('../../public/shared/statistics.js');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsprivilegedapitestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js (0 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/privileged-api-tests.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -0,0 +1,182 @@
</span><ins>+'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);
+        });
+
+    });
+
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsresourcesmockremoteapijs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js (202000 => 202001)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js        2016-06-13 19:40:01 UTC (rev 202000)
+++ trunk/Websites/perf.webkit.org/unit-tests/resources/mock-remote-api.js        2016-06-13 19:47:38 UTC (rev 202001)
</span><span class="lines">@@ -16,6 +16,14 @@
</span><span class="cx">     {
</span><span class="cx">         return this._addRequest(url, 'GET', null);
</span><span class="cx">     },
</span><ins>+    postJSON: function (url, data)
+    {
+        return this._addRequest(url, 'POST', data);
+    },
+    postJSONWithStatus: function (url, data)
+    {
+        return this._addRequest(url, 'POST', data);
+    },
</ins><span class="cx">     postFormUrlencodedData: function (url, data)
</span><span class="cx">     {
</span><span class="cx">         return this._addRequest(url, 'POST', data);
</span></span></pre>
</div>
</div>

</body>
</html>