<!DOCTYPE html>
<title>Service Worker: Redirected response</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<script>
// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests
// the resulting response against the expected values. It also adds the
// response to |cache| and checks the cached response matches the expected
// values.
//
// |options|: a dictionary of parameters for the test
// |options.url|: the URL to fetch
// |options.fetch_option|: the options passed to |fetch_method|
// |options.fetch_method|: the method used to fetch. Useful for testing an
//                         iframe's fetch() vs. this page's fetch().
// |options.expected_type|: The value of response.type
// |options.expected_redirected|: The value of response.redirected
// |options.expected_intercepted_urls|: The list of intercepted request URLs.
function redirected_test(options) {
  return options.fetch_method.call(null, options.url, options.fetch_option).then(response => {
        let cloned_response = response.clone();
        assert_equals(
            response.type, options.expected_type,
            'The response type of response must match. URL: ' + options.url);
        assert_equals(
            cloned_response.type, options.expected_type,
            'The response type of cloned response must match. URL: ' + options.url);
        assert_equals(
            response.redirected, options.expected_redirected,
            'The redirected flag of response must match. URL: ' + options.url);
        assert_equals(
            cloned_response.redirected, options.expected_redirected,
            'The redirected flag of cloned response must match. URL: ' + options.url);
        if (options.expected_response_url) {
            assert_equals(
                cloned_response.url, options.expected_response_url,
                'The URL does not meet expectation. URL: ' + options.url);
        }
        return cache.put(options.url, response);
      })
    .then(_ => cache.match(options.url))
    .then(response => {
        assert_equals(
            response.type, options.expected_type,
            'The response type of response in CacheStorage must match. ' +
            'URL: ' + options.url);
        assert_equals(
            response.redirected, options.expected_redirected,
            'The redirected flag of response in CacheStorage must match. ' +
            'URL: ' + options.url);
        return check_intercepted_urls(options.expected_intercepted_urls);
      });
}

async function take_intercepted_urls() {
  const message = new Promise((resolve) => {
    let channel = new MessageChannel();
    channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); };
    worker.postMessage({command: 'getRequestInfos', port: channel.port2},
                       [channel.port2]);
  });
  const request_infos = await message;
  return request_infos.map(info => { return info.url; });
}

function check_intercepted_urls(expected_urls) {
  return take_intercepted_urls().then((urls) => {
      assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.');
    });
}

function setup_and_clean() {
  // To prevent interference from previous tests, take the intercepted URLs from
  // the service worker.
  return setup.then(() => take_intercepted_urls());
}


let host_info = get_host_info();
const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() +
                     'resources/redirect.py?Redirect=';
const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() +
                   'resources/simple.txt?';
const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL);
let frame;
let cache;
let setup;
let worker;

promise_test(t => {
    const SCOPE = 'resources/blank.html?redirected-response';
    const SCRIPT = 'resources/redirect-worker.js';
    const CACHE_NAME = 'service-workers/service-worker/redirected-response';
    setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
      .then(registration => {
          promise_test(
              () => registration.unregister(),
              'restore global state (service worker registration)');
          worker = registration.installing;
          return wait_for_state(t, registration.installing, 'activated');
        })
      .then(_ => self.caches.open(CACHE_NAME))
      .then(c => {
          cache = c;
          promise_test(
            () => self.caches.delete(CACHE_NAME),
            'restore global state (caches)');
          return with_iframe(SCOPE);
        })
      .then(f => {
          frame = f;
          add_completion_callback(() => f.remove());
          return check_intercepted_urls(
              [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]);
        });
      return setup;
  }, 'initialize global state (service worker registration and caches)');

// ===============================================================
// Tests for requests that are out-of-scope of the service worker.
// ===============================================================
promise_test(t => setup_and_clean()
  .then(() => redirected_test({url: TARGET_URL,
                               fetch_option: {},
                               fetch_method: self.fetch,
                               expected_type: 'basic',
                               expected_redirected: false,
                               expected_intercepted_urls: []})),
  'mode: "follow", non-intercepted request, no server redirect');

promise_test(t => setup_and_clean()
  .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL,
                               fetch_option: {},
                               fetch_method: self.fetch,
                               expected_type: 'basic',
                               expected_redirected: true,
                               expected_intercepted_urls: []})),
  'mode: "follow", non-intercepted request');

promise_test(t => setup_and_clean()
  .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual',
                               fetch_option: {redirect: 'manual'},
                               fetch_method: self.fetch,
                               expected_type: 'opaqueredirect',
                               expected_redirected: false,
                               expected_intercepted_urls: []})),
  'mode: "manual", non-intercepted request');

promise_test(t => setup_and_clean()
    .then(() => promise_rejects_js(
                   t, TypeError,
                   self.fetch(REDIRECT_TO_TARGET_URL + '&error',
                              {redirect:'error'}),
                   'The redirect response from the server should be treated as' +
                   ' an error when the redirect flag of request was \'error\'.'))
    .then(() => check_intercepted_urls([])),
  'mode: "error", non-intercepted request');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = TARGET_URL + '&sw=fetch';
      return redirected_test({url: url,
                              fetch_option: {},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: false,
                              expected_intercepted_urls: [url]})
    }),
  'mode: "follow", no mode change, no server redirect');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns a redirected response.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=follow&sw=fetch';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'follow'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: true,
                              expected_intercepted_urls: [url]})
    }),
  'mode: "follow", no mode change');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=error&sw=follow';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'error'}),
          'The redirected response from the service worker should be ' +
          'treated as an error when the redirect flag of request was ' +
          '\'error\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "error", mode change: "follow"');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=manual&sw=follow';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'manual'}),
          'The redirected response from the service worker should be ' +
          'treated as an error when the redirect flag of request was ' +
          '\'manual\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "manual", mode change: "follow"');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns an opaqueredirect response.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=follow&sw=manual';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'follow'}),
          'The opaqueredirect response from the service worker should ' +
          'be treated as an error when the redirect flag of request was' +
          ' \'follow\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "follow", mode change: "manual"');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=error&sw=manual';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'error'}),
          'The opaqueredirect response from the service worker should ' +
          'be treated as an error when the redirect flag of request was' +
          ' \'error\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "error", mode change: "manual"');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = REDIRECT_TO_TARGET_URL +
                  '&original-redirect-mode=manual&sw=manual';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'manual'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'opaqueredirect',
                              expected_redirected: false,
                              expected_intercepted_urls: [url]});
    }),
  'mode: "manual", no mode change');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns a generated redirect response.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=follow&sw=gen';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'follow'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: true,
                              expected_intercepted_urls: [url, TARGET_URL]})
    }),
  'mode: "follow", generated redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=error&sw=gen';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'error'}),
          'The generated redirect response from the service worker should ' +
          'be treated as an error when the redirect flag of request was' +
          ' \'error\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "error", generated redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=manual&sw=gen';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'manual'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'opaqueredirect',
                              expected_redirected: false,
                              expected_intercepted_urls: [url]})
    }),
  'mode: "manual", generated redirect response');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns a generated redirect response manually with the Response
// constructor.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=follow&sw=gen-manual';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'follow'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: true,
                              expected_intercepted_urls: [url, TARGET_URL]})
    }),
  'mode: "follow", manually-generated redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=error&sw=gen-manual';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'error'}),
          'The generated redirect response from the service worker should ' +
          'be treated as an error when the redirect flag of request was' +
          ' \'error\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "error", manually-generated redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=' + encodeURIComponent(TARGET_URL) +
                  '&original-redirect-mode=manual&sw=gen-manual';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'manual'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'opaqueredirect',
                              expected_redirected: false,
                              expected_intercepted_urls: [url]})
    }),
  'mode: "manual", manually-generated redirect response');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns a generated redirect response with a relative location header.
// Generated responses do not have URLs, so this should fail to resolve.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=blank.html' +
                  '&original-redirect-mode=follow&sw=gen-manual';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'follow'}),
          'Following the generated redirect response from the service worker '+
          'should result fail.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "follow", generated relative redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=blank.html' +
                  '&original-redirect-mode=error&sw=gen-manual';
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(url, {redirect: 'error'}),
          'The generated redirect response from the service worker should ' +
          'be treated as an error when the redirect flag of request was' +
          ' \'error\'.')
        .then(() => check_intercepted_urls([url]));
    }),
  'mode: "error", generated relative redirect response');

promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() +
                  'sample?url=blank.html' +
                  '&original-redirect-mode=manual&sw=gen-manual';
      return redirected_test({url: url,
                              fetch_option: {redirect: 'manual'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'opaqueredirect',
                              expected_redirected: false,
                              expected_intercepted_urls: [url]})
    }),
  'mode: "manual", generated relative redirect response');

// =======================================================
// Tests for requests that are in-scope of the service worker. The service
// worker returns a generated redirect response. And the fetch follows the
// redirection multiple times.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      // The Fetch spec says: "If request’s redirect count is twenty, return a
      // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
      // So fetch can follow the redirect response 20 times.
      let urls = [TARGET_URL];
      for (let i = 0; i < 20; ++i) {
        urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
                    encodeURIComponent(urls[0]));

      }
      return redirected_test({url: urls[0],
                              fetch_option: {redirect: 'follow'},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: true,
                              expected_intercepted_urls: urls})
    }),
  'Fetch should follow the redirect response 20 times');

promise_test(t => setup_and_clean()
    .then(() => {
      let urls = [TARGET_URL];
      // The Fetch spec says: "If request’s redirect count is twenty, return a
      // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
      // So fetch can't follow the redirect response 21 times.
      for (let i = 0; i < 21; ++i) {
        urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
                    encodeURIComponent(urls[0]));

      }
      return promise_rejects_js(
          t, frame.contentWindow.TypeError,
          frame.contentWindow.fetch(urls[0], {redirect: 'follow'}),
          'Fetch should not follow the redirect response 21 times.')
        .then(() => {
          urls.pop();
          return check_intercepted_urls(urls)
        });
    }),
  'Fetch should not follow the redirect response 21 times.');

// =======================================================
// A test for verifying the url of a service-worker-redirected request is
// propagated to the outer response.
// =======================================================
promise_test(t => setup_and_clean()
    .then(() => {
      const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' +
                  encodeURIComponent(TARGET_URL) +'&sw=fetch-url';
      return redirected_test({url: url,
                              fetch_option: {},
                              fetch_method: frame.contentWindow.fetch,
                              expected_type: 'basic',
                              expected_redirected: false,
                              expected_intercepted_urls: [url],
                              expected_response_url: TARGET_URL});
    }),
  'The URL for the service worker redirected request should be propagated to ' +
  'response.');
</script>
