| /** |
| * Each test is run twice: once using etag/If-None-Match and once with |
| * date/If-Modified-Since. Each test run gets its own URL and randomized |
| * content and operates independently. |
| * |
| * The test steps are run with request_cache.length fetch requests issued |
| * and their immediate results sanity-checked. The cache.py server script |
| * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma, |
| * and Cache-Control observed headers for each request it receives. When |
| * the test fetches have run, this state is retrieved from cache.py and the |
| * expected_* lists are checked, including their length. |
| * |
| * This means that if a request_* fetch is expected to hit the cache and not |
| * touch the network, then there will be no entry for it in the expect_* |
| * lists. AKA (request_cache.length - expected_validation_headers.length) |
| * should equal the number of cache hits that didn't touch the network. |
| * |
| * Test dictionary keys: |
| * - state: required string that determines whether the Expires response for |
| * the fetched document should be set in the future ("fresh") or past |
| * ("stale"). |
| * - vary: optional string to be passed to the server for it to quote back |
| * in a Vary header on the response to us. |
| * - cache_control: optional string to be passed to the server for it to |
| * quote back in a Cache-Control header on the response to us. |
| * - redirect: optional string "same-origin" or "cross-origin". If |
| * provided, the server will issue an absolute redirect to the script on |
| * the same or a different origin, as appropriate. The redirected |
| * location is the script with the redirect parameter removed, so the |
| * content/state/etc. will be as if you hadn't specified a redirect. |
| * - request_cache: required array of cache modes to use (via `cache`). |
| * - request_headers: optional array of explicit fetch `headers` arguments. |
| * If provided, the server will log an empty dictionary for each request |
| * instead of the request headers it would normally log. |
| * - response: optional array of specialized response handling. Right now, |
| * "error" array entries indicate a network error response is expected |
| * which will reject with a TypeError. |
| * - expected_validation_headers: required boolean array indicating whether |
| * the server should have seen an If-None-Match/If-Modified-Since header |
| * in the request. |
| * - expected_no_cache_headers: required boolean array indicating whether |
| * the server should have seen Pragma/Cache-control:no-cache headers in |
| * the request. |
| * - expected_max_age_headers: optional boolean array indicating whether |
| * the server should have seen a Cache-Control:max-age=0 header in the |
| * request. |
| */ |
| |
| var now = new Date(); |
| |
| function base_path() { |
| return location.pathname.replace(/\/[^\/]*$/, '/'); |
| } |
| function make_url(uuid, id, value, content, info) { |
| var dates = { |
| fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(), |
| stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(), |
| }; |
| var vary = ""; |
| if ("vary" in info) { |
| vary = "&vary=" + info.vary; |
| } |
| var cache_control = ""; |
| if ("cache_control" in info) { |
| cache_control = "&cache_control=" + info.cache_control; |
| } |
| var redirect = ""; |
| |
| var ignore_request_headers = ""; |
| if ("request_headers" in info) { |
| // Ignore the request headers that we send since they may be synthesized by the test. |
| ignore_request_headers = "&ignore"; |
| } |
| var url_sans_redirect = "resources/cache.py?token=" + uuid + |
| "&content=" + content + |
| "&" + id + "=" + value + |
| "&expires=" + dates[info.state] + |
| vary + cache_control + ignore_request_headers; |
| // If there's a redirect, the target is the script without any redirect at |
| // either the same domain or a different domain. |
| if ("redirect" in info) { |
| var host_info = get_host_info(); |
| var origin; |
| switch (info.redirect) { |
| case "same-origin": |
| origin = host_info['HTTP_ORIGIN']; |
| break; |
| case "cross-origin": |
| origin = host_info['HTTP_REMOTE_ORIGIN']; |
| break; |
| } |
| var redirected_url = origin + base_path() + url_sans_redirect; |
| return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url); |
| } else { |
| return url_sans_redirect; |
| } |
| } |
| function expected_status(type, identifier, init) { |
| if (type == "date" && |
| init.headers && |
| init.headers["If-Modified-Since"] == identifier) { |
| // The server will respond with a 304 in this case. |
| return [304, "Not Modified"]; |
| } |
| return [200, "OK"]; |
| } |
| function expected_response_text(type, identifier, init, content) { |
| if (type == "date" && |
| init.headers && |
| init.headers["If-Modified-Since"] == identifier) { |
| // The server will respond with a 304 in this case. |
| return ""; |
| } |
| return content; |
| } |
| function server_state(uuid) { |
| return fetch("resources/cache.py?querystate&token=" + uuid) |
| .then(function(response) { |
| return response.text(); |
| }).then(function(text) { |
| // null will be returned if the server never received any requests |
| // for the given uuid. Normalize that to an empty list consistent |
| // with our representation. |
| return JSON.parse(text) || []; |
| }); |
| } |
| function make_test(type, info) { |
| return function(test) { |
| var uuid = token(); |
| var identifier = (type == "tag" ? Math.random() : now.toGMTString()); |
| var content = Math.random().toString(); |
| var url = make_url(uuid, type, identifier, content, info); |
| var fetch_functions = []; |
| for (var i = 0; i < info.request_cache.length; ++i) { |
| fetch_functions.push(function(idx) { |
| var init = {cache: info.request_cache[idx]}; |
| if ("request_headers" in info) { |
| init.headers = info.request_headers[idx]; |
| } |
| if (init.cache === "only-if-cached") { |
| // only-if-cached requires we use same-origin mode. |
| init.mode = "same-origin"; |
| } |
| return fetch(url, init) |
| .then(function(response) { |
| if ("response" in info && info.response[idx] === "error") { |
| assert_true(false, "fetch should have been an error"); |
| return; |
| } |
| assert_array_equals([response.status, response.statusText], |
| expected_status(type, identifier, init)); |
| return response.text(); |
| }).then(function(text) { |
| assert_equals(text, expected_response_text(type, identifier, init, content)); |
| }, function(reason) { |
| if ("response" in info && info.response[idx] === "error") { |
| assert_throws(new TypeError(), function() { throw reason; }); |
| } else { |
| throw reason; |
| } |
| }); |
| }); |
| } |
| var i = 0; |
| function run_next_step() { |
| if (fetch_functions.length) { |
| return fetch_functions.shift()(i++) |
| .then(run_next_step); |
| } else { |
| return Promise.resolve(); |
| } |
| } |
| return run_next_step() |
| .then(function() { |
| // Now, query the server state |
| return server_state(uuid); |
| }).then(function(state) { |
| var expectedState = []; |
| info.expected_validation_headers.forEach(function (validate) { |
| if (validate) { |
| if (type == "tag") { |
| expectedState.push({"If-None-Match": '"' + identifier + '"'}); |
| } else { |
| expectedState.push({"If-Modified-Since": identifier}); |
| } |
| } else { |
| expectedState.push({}); |
| } |
| }); |
| for (var i = 0; i < info.expected_no_cache_headers.length; ++i) { |
| if (info.expected_no_cache_headers[i]) { |
| expectedState[i]["Pragma"] = "no-cache"; |
| expectedState[i]["Cache-Control"] = "no-cache"; |
| } |
| } |
| if ("expected_max_age_headers" in info) { |
| for (var i = 0; i < info.expected_max_age_headers.length; ++i) { |
| if (info.expected_max_age_headers[i]) { |
| expectedState[i]["Cache-Control"] = "max-age=0"; |
| } |
| } |
| } |
| assert_equals(state.length, expectedState.length); |
| for (var i = 0; i < state.length; ++i) { |
| for (var header in state[i]) { |
| assert_equals(state[i][header], expectedState[i][header]); |
| delete expectedState[i][header]; |
| } |
| for (var header in expectedState[i]) { |
| assert_false(header in state[i]); |
| } |
| } |
| }); |
| }; |
| } |
| |
| function run_tests(tests) |
| { |
| tests.forEach(function(info) { |
| promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response"); |
| promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response"); |
| }); |
| } |