From 47e0894225361a25ac79624ae6b2ad6882982ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Tue, 12 Jan 2021 16:45:35 +0100 Subject: [PATCH 1/3] lib: implement fetch's Headers class Refs: https://github.com/nodejs/node/issues/19393 --- lib/http.js | 2 +- lib/internal/fetch/headers.js | 469 ++++++++++++++++ node.gyp | 1 + test/fixtures/wpt/README.md | 1 + test/fixtures/wpt/fetch/META.yml | 8 + test/fixtures/wpt/fetch/README.md | 6 + .../wpt/fetch/api/abort/cache.https.any.js | 47 ++ .../fetch/api/abort/destroyed-context.html | 27 + .../wpt/fetch/api/abort/general.any.js | 531 ++++++++++++++++++ .../wpt/fetch/api/abort/keepalive.html | 85 +++ .../serviceworker-intercepted.https.html | 105 ++++ .../wpt/fetch/api/basic/accept-header.any.js | 34 ++ .../fetch/api/basic/block-mime-as-script.html | 43 ++ .../fetch/api/basic/conditional-get.any.js | 38 ++ .../api/basic/error-after-response.any.js | 24 + .../api/basic/header-value-combining.any.js | 15 + .../api/basic/header-value-null-byte.any.js | 5 + .../wpt/fetch/api/basic/historical.any.js | 17 + .../wpt/fetch/api/basic/integrity.sub.any.js | 77 +++ .../wpt/fetch/api/basic/keepalive.html | 106 ++++ .../wpt/fetch/api/basic/mediasource.window.js | 5 + .../fetch/api/basic/mode-no-cors.sub.any.js | 29 + .../fetch/api/basic/mode-same-origin.any.js | 28 + .../wpt/fetch/api/basic/referrer.any.js | 29 + .../basic/request-forbidden-headers.any.js | 43 ++ .../wpt/fetch/api/basic/request-head.any.js | 6 + .../api/basic/request-headers-case.any.js | 13 + .../api/basic/request-headers-nonascii.any.js | 29 + .../fetch/api/basic/request-headers.any.js | 82 +++ .../request-referrer-redirected-worker.html | 17 + .../fetch/api/basic/request-referrer.any.js | 24 + .../wpt/fetch/api/basic/request-upload.any.js | 125 +++++ .../fetch/api/basic/response-url.sub.any.js | 16 + .../wpt/fetch/api/basic/scheme-about.any.js | 18 + .../fetch/api/basic/scheme-blob.sub.any.js | 45 ++ .../wpt/fetch/api/basic/scheme-data.any.js | 43 ++ .../fetch/api/basic/scheme-others.sub.any.js | 31 + .../fetch/api/basic/stream-response.any.js | 29 + .../api/basic/stream-safe-creation.any.js | 54 ++ .../wpt/fetch/api/basic/text-utf8.any.js | 74 +++ .../wpt/fetch/api/body/mime-type.any.js | 40 ++ .../wpt/fetch/api/cors/cors-basic.any.js | 37 ++ .../api/cors/cors-cookies-redirect.any.js | 49 ++ .../wpt/fetch/api/cors/cors-cookies.any.js | 56 ++ .../api/cors/cors-expose-star.sub.any.js | 41 ++ .../fetch/api/cors/cors-filtering.sub.any.js | 69 +++ .../api/cors/cors-multiple-origins.sub.any.js | 22 + .../fetch/api/cors/cors-no-preflight.any.js | 41 ++ .../wpt/fetch/api/cors/cors-origin.any.js | 51 ++ .../api/cors/cors-preflight-cache.any.js | 46 ++ .../cors-preflight-not-cors-safelisted.any.js | 19 + .../api/cors/cors-preflight-redirect.any.js | 37 ++ .../api/cors/cors-preflight-referrer.any.js | 51 ++ .../cors-preflight-response-validation.any.js | 33 ++ .../fetch/api/cors/cors-preflight-star.any.js | 49 ++ .../api/cors/cors-preflight-status.any.js | 37 ++ .../wpt/fetch/api/cors/cors-preflight.any.js | 42 ++ .../api/cors/cors-redirect-credentials.any.js | 52 ++ .../api/cors/cors-redirect-preflight.any.js | 46 ++ .../wpt/fetch/api/cors/cors-redirect.any.js | 42 ++ .../wpt/fetch/api/cors/data-url-iframe.html | 58 ++ .../api/cors/data-url-shared-worker.html | 53 ++ .../wpt/fetch/api/cors/data-url-worker.html | 50 ++ .../fetch/api/cors/resources/corspreflight.js | 58 ++ .../cors/resources/not-cors-safelisted.json | 13 + .../wpt/fetch/api/cors/sandboxed-iframe.html | 14 + .../credentials/authentication-basic.any.js | 17 + .../wpt/fetch/api/credentials/cookies.any.js | 49 ++ .../headers/header-values-normalize.any.js | 70 +++ .../fetch/api/headers/header-values.any.js | 61 ++ .../fetch/api/headers/headers-basic.any.js | 218 +++++++ .../fetch/api/headers/headers-casing.any.js | 52 ++ .../fetch/api/headers/headers-combine.any.js | 64 +++ .../fetch/api/headers/headers-errors.any.js | 94 ++++ .../fetch/api/headers/headers-no-cors.any.js | 55 ++ .../api/headers/headers-normalize.any.js | 35 ++ .../fetch/api/headers/headers-record.any.js | 355 ++++++++++++ .../api/headers/headers-structure.any.js | 18 + test/fixtures/wpt/fetch/api/idlharness.any.js | 21 + .../api/policies/csp-blocked-worker.html | 16 + .../wpt/fetch/api/policies/csp-blocked.html | 15 + .../api/policies/csp-blocked.html.headers | 1 + .../wpt/fetch/api/policies/csp-blocked.js | 13 + .../fetch/api/policies/csp-blocked.js.headers | 1 + .../wpt/fetch/api/policies/nested-policy.js | 1 + .../api/policies/nested-policy.js.headers | 1 + ...rrer-no-referrer-service-worker.https.html | 18 + .../policies/referrer-no-referrer-worker.html | 17 + .../api/policies/referrer-no-referrer.html | 15 + .../referrer-no-referrer.html.headers | 1 + .../api/policies/referrer-no-referrer.js | 19 + .../policies/referrer-no-referrer.js.headers | 1 + .../referrer-origin-service-worker.https.html | 18 + ...hen-cross-origin-service-worker.https.html | 17 + ...errer-origin-when-cross-origin-worker.html | 16 + .../referrer-origin-when-cross-origin.html | 16 + ...rrer-origin-when-cross-origin.html.headers | 1 + .../referrer-origin-when-cross-origin.js | 21 + ...ferrer-origin-when-cross-origin.js.headers | 1 + .../api/policies/referrer-origin-worker.html | 17 + .../fetch/api/policies/referrer-origin.html | 16 + .../api/policies/referrer-origin.html.headers | 1 + .../wpt/fetch/api/policies/referrer-origin.js | 30 + .../api/policies/referrer-origin.js.headers | 1 + ...errer-unsafe-url-service-worker.https.html | 18 + .../policies/referrer-unsafe-url-worker.html | 17 + .../api/policies/referrer-unsafe-url.html | 16 + .../policies/referrer-unsafe-url.html.headers | 1 + .../fetch/api/policies/referrer-unsafe-url.js | 21 + .../policies/referrer-unsafe-url.js.headers | 1 + .../redirect-back-to-original-origin.any.js | 38 ++ .../fetch/api/redirect/redirect-count.any.js | 42 ++ .../redirect/redirect-empty-location.any.js | 21 + .../redirect-location-escape.tentative.any.js | 46 ++ .../api/redirect/redirect-location.any.js | 48 ++ .../fetch/api/redirect/redirect-method.any.js | 112 ++++ .../fetch/api/redirect/redirect-mode.any.js | 50 ++ .../fetch/api/redirect/redirect-origin.any.js | 42 ++ .../redirect-referrer-override.any.js | 104 ++++ .../api/redirect/redirect-referrer.any.js | 66 +++ .../api/redirect/redirect-schemes.any.js | 19 + .../api/redirect/redirect-to-dataurl.any.js | 28 + .../fetch-destination-frame.https.html | 51 ++ .../fetch-destination-iframe.https.html | 51 ++ ...fetch-destination-no-load-event.https.html | 124 ++++ .../fetch-destination-prefetch.https.html | 46 ++ .../fetch-destination-worker.https.html | 60 ++ .../destination/fetch-destination.https.html | 435 ++++++++++++++ .../api/request/destination/resources/dummy | 0 .../request/destination/resources/dummy.es | 0 .../destination/resources/dummy.es.headers | 1 + .../request/destination/resources/dummy.html | 0 .../request/destination/resources/dummy.png | Bin 0 -> 20527 bytes .../request/destination/resources/dummy.ttf | Bin 0 -> 3009 bytes .../destination/resources/dummy_audio.mp3 | Bin 0 -> 37652 bytes .../destination/resources/dummy_audio.oga | Bin 0 -> 32132 bytes .../destination/resources/dummy_video.mp4 | Bin 0 -> 119549 bytes .../destination/resources/dummy_video.ogv | Bin 0 -> 157777 bytes .../destination/resources/empty.https.html | 0 .../fetch-destination-worker-frame.js | 20 + .../fetch-destination-worker-iframe.js | 20 + .../fetch-destination-worker-no-load-event.js | 20 + .../resources/fetch-destination-worker.js | 12 + .../request/destination/resources/importer.js | 1 + .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 14 + .../request/multi-globals/url-parsing.html | 27 + .../fetch/api/request/request-bad-port.any.js | 83 +++ .../request-cache-default-conditional.any.js | 170 ++++++ .../api/request/request-cache-default.any.js | 39 ++ .../request/request-cache-force-cache.any.js | 67 +++ .../api/request/request-cache-no-cache.any.js | 25 + .../api/request/request-cache-no-store.any.js | 37 ++ .../request-cache-only-if-cached.any.js | 66 +++ .../api/request/request-cache-reload.any.js | 51 ++ .../wpt/fetch/api/request/request-cache.js | 223 ++++++++ .../fetch/api/request/request-clone.sub.html | 63 +++ .../api/request/request-consume-empty.any.js | 101 ++++ .../fetch/api/request/request-consume.any.js | 145 +++++ .../api/request/request-disturbed.any.js | 109 ++++ .../fetch/api/request/request-error.any.js | 48 ++ .../wpt/fetch/api/request/request-error.js | 57 ++ .../fetch/api/request/request-headers.any.js | 173 ++++++ .../api/request/request-init-001.sub.html | 100 ++++ .../fetch/api/request/request-init-002.any.js | 60 ++ .../api/request/request-init-003.sub.html | 84 +++ .../api/request/request-init-stream.any.js | 57 ++ .../api/request/request-keepalive-quota.html | 97 ++++ .../api/request/request-keepalive.any.js | 17 + .../request-reset-attributes.https.html | 96 ++++ .../api/request/request-structure.any.js | 128 +++++ .../wpt/fetch/api/request/resources/hello.txt | 1 + .../request-reset-attributes-worker.js | 19 + .../wpt/fetch/api/request/url-encoding.html | 25 + .../wpt/fetch/api/resources/basic.html | 5 + .../wpt/fetch/api/resources/cors-top.txt | 1 + .../fetch/api/resources/cors-top.txt.headers | 1 + .../wpt/fetch/api/resources/data.json | 1 + .../wpt/fetch/api/resources/empty.txt | 0 .../fetch/api/resources/keepalive-iframe.html | 25 + .../fetch/api/resources/keepalive-window.html | 34 ++ .../fetch/api/resources/sandboxed-iframe.html | 34 ++ .../wpt/fetch/api/resources/sw-intercept.js | 10 + test/fixtures/wpt/fetch/api/resources/top.txt | 1 + .../fixtures/wpt/fetch/api/resources/utils.js | 103 ++++ .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 16 + .../multi-globals/relevant/relevant.html | 2 + .../response/multi-globals/url-parsing.html | 27 + .../response-body-read-task-handling.html | 54 ++ .../response/response-cancel-stream.any.js | 57 ++ .../response/response-clone-iframe.window.js | 32 ++ .../fetch/api/response/response-clone.any.js | 124 ++++ .../response/response-consume-empty.any.js | 99 ++++ .../response/response-consume-stream.any.js | 59 ++ .../fetch/api/response/response-consume.html | 317 +++++++++++ .../response-error-from-stream.any.js | 59 ++ .../fetch/api/response/response-error.any.js | 27 + .../api/response/response-from-stream.any.js | 23 + .../api/response/response-init-001.any.js | 64 +++ .../api/response/response-init-002.any.js | 61 ++ .../api/response/response-static-error.any.js | 22 + .../response/response-static-redirect.any.js | 40 ++ .../response-stream-disturbed-1.any.js | 42 ++ .../response-stream-disturbed-2.any.js | 33 ++ .../response-stream-disturbed-3.any.js | 34 ++ .../response-stream-disturbed-4.any.js | 33 ++ .../response-stream-disturbed-5.any.js | 34 ++ .../response-stream-disturbed-6.any.js | 76 +++ .../response-stream-disturbed-by-pipe.any.js | 17 + .../response-stream-with-broken-then.any.js | 117 ++++ .../network-partition-key.html | 264 +++++++++ ...network-partition-about-blank-checker.html | 35 ++ .../resources/network-partition-checker.html | 30 + .../network-partition-iframe-checker.html | 22 + .../resources/network-partition-key.js | 47 ++ .../network-partition-worker-checker.html | 24 + .../resources/network-partition-worker.js | 15 + .../content-encoding/bad-gzip-body.any.js | 22 + .../fetch/content-length/content-length.html | 14 + .../content-length.html.headers | 1 + .../fixtures/wpt/fetch/content-type/README.md | 20 + .../content-type/resources/content-types.json | 122 ++++ .../resources/script-content-types.json | 92 +++ .../wpt/fetch/content-type/response.window.js | 72 +++ .../wpt/fetch/content-type/script.window.js | 48 ++ test/fixtures/wpt/fetch/corb/README.md | 67 +++ .../img-html-correctly-labeled.sub-ref.html | 4 + .../corb/img-html-correctly-labeled.sub.html | 11 + ...img-mime-types-coverage.tentative.sub.html | 85 +++ ...led-as-html-nosniff.tentative.sub-ref.html | 4 + ...labeled-as-html-nosniff.tentative.sub.html | 11 + .../img-png-mislabeled-as-html.sub-ref.html | 4 + .../corb/img-png-mislabeled-as-html.sub.html | 10 + ...labeled-as-html-nosniff.tentative.sub.html | 24 + .../css-mislabeled-as-html-nosniff.css | 1 + ...css-mislabeled-as-html-nosniff.css.headers | 2 + .../corb/resources/css-mislabeled-as-html.css | 1 + .../css-mislabeled-as-html.css.headers | 1 + .../css-with-json-parser-breaker.css | 3 + .../corb/resources/empty-labeled-as-png.png | 0 .../empty-labeled-as-png.png.headers | 1 + .../resources/html-correctly-labeled.html | 10 + .../html-correctly-labeled.html.headers | 1 + .../fetch/corb/resources/html-js-polyglot.js | 9 + .../resources/html-js-polyglot.js.headers | 1 + .../fetch/corb/resources/html-js-polyglot2.js | 10 + .../resources/html-js-polyglot2.js.headers | 1 + .../js-mislabeled-as-html-nosniff.js | 1 + .../js-mislabeled-as-html-nosniff.js.headers | 2 + .../corb/resources/js-mislabeled-as-html.js | 1 + .../js-mislabeled-as-html.js.headers | 1 + .../corb/resources/png-correctly-labeled.png | Bin 0 -> 1092 bytes .../png-correctly-labeled.png.headers | 1 + .../png-mislabeled-as-html-nosniff.png | Bin 0 -> 1092 bytes ...png-mislabeled-as-html-nosniff.png.headers | 2 + .../corb/resources/png-mislabeled-as-html.png | Bin 0 -> 1092 bytes .../png-mislabeled-as-html.png.headers | 1 + ...ts-html-containing-blob-url-to-parent.html | 16 + ...-html-correctly-labeled.tentative.sub.html | 30 + .../corb/script-html-js-polyglot.sub.html | 32 ++ ...pt-html-via-cross-origin-blob-url.sub.html | 38 ++ ...ipt-js-mislabeled-as-html-nosniff.sub.html | 33 ++ .../script-js-mislabeled-as-html.sub.html | 25 + ...ith-json-parser-breaker.tentative.sub.html | 83 +++ ...with-nonsniffable-types.tentative.sub.html | 84 +++ ...le-css-mislabeled-as-html-nosniff.sub.html | 42 ++ .../style-css-mislabeled-as-html.sub.html | 36 ++ ...tyle-css-with-json-parser-breaker.sub.html | 38 ++ .../style-html-correctly-labeled.sub.html | 41 ++ .../cors-rfc1918/idlharness.tentative.any.js | 24 + .../cors-rfc1918/non-secure-context.window.js | 31 + .../fetch/cors-rfc1918/resources/support.js | 34 ++ .../resources/treat-as-public-address.html | 8 + .../treat-as-public-address.html.headers | 1 + .../treat-as-public-address.https.html | 8 + ...treat-as-public-address.https.html.headers | 1 + .../secure-context.https.window.js | 31 + .../fetch-in-iframe.html | 67 +++ .../cross-origin-resource-policy/fetch.any.js | 76 +++ .../fetch.https.any.js | 56 ++ .../iframe-loads.html | 46 ++ .../image-loads.html | 54 ++ .../resources/green.png | Bin 0 -> 114 bytes .../resources/iframeFetch.html | 19 + .../scheme-restriction.any.js | 7 + .../scheme-restriction.https.window.js | 13 + .../script-loads.html | 52 ++ .../syntax.any.js | 19 + test/fixtures/wpt/fetch/data-urls/README.md | 11 + .../wpt/fetch/data-urls/base64.any.js | 18 + .../wpt/fetch/data-urls/processing.any.js | 22 + .../wpt/fetch/data-urls/resources/base64.json | 82 +++ .../fetch/data-urls/resources/data-urls.json | 208 +++++++ test/fixtures/wpt/fetch/h1-parsing/README.md | 5 + .../wpt/fetch/h1-parsing/lone-cr.window.js | 23 + .../resources-with-0x00-in-header.window.js | 31 + .../wpt/fetch/h1-parsing/resources/README.md | 6 + .../resources/blue-with-0x00-in-a-header.asis | Bin 0 -> 890 bytes .../fetch/h1-parsing/status-code.window.js | 98 ++++ .../wpt/fetch/http-cache/304-update.any.js | 146 +++++ test/fixtures/wpt/fetch/http-cache/README.md | 71 +++ .../http-cache/basic-auth-cache-test-ref.html | 6 + .../http-cache/basic-auth-cache-test.html | 27 + .../wpt/fetch/http-cache/cache-mode.any.js | 61 ++ .../wpt/fetch/http-cache/cc-request.any.js | 202 +++++++ .../wpt/fetch/http-cache/freshness.any.js | 215 +++++++ .../wpt/fetch/http-cache/heuristic.any.js | 93 +++ .../wpt/fetch/http-cache/http-cache.js | 272 +++++++++ .../wpt/fetch/http-cache/invalidate.any.js | 235 ++++++++ .../wpt/fetch/http-cache/partial.any.js | 187 ++++++ .../wpt/fetch/http-cache/post-patch.any.js | 46 ++ .../split-cache-popup-with-iframe.html | 34 ++ .../resources/split-cache-popup.html | 28 + .../wpt/fetch/http-cache/split-cache.html | 142 +++++ .../wpt/fetch/http-cache/status.any.js | 60 ++ .../fixtures/wpt/fetch/http-cache/vary.any.js | 313 +++++++++++ ...vas-remote-read-remote-image-redirect.html | 28 + test/fixtures/wpt/fetch/metadata/META.yml | 4 + test/fixtures/wpt/fetch/metadata/README.md | 9 + .../fetch/metadata/download.https.sub.html | 37 ++ .../metadata/embed.https.sub.tentative.html | 63 +++ .../wpt/fetch/metadata/favicon.https.sub.html | 67 +++ .../metadata/fetch-preflight.https.sub.any.js | 29 + ...via-serviceworker--fallback.https.sub.html | 50 ++ ...-serviceworker--respondWith.https.sub.html | 51 ++ .../wpt/fetch/metadata/fetch.https.sub.any.js | 58 ++ .../wpt/fetch/metadata/fetch.sub.html | 29 + .../wpt/fetch/metadata/font.https.sub.html | 78 +++ .../wpt/fetch/metadata/form.https.sub.html | 85 +++ .../wpt/fetch/metadata/history.https.sub.html | 79 +++ .../wpt/fetch/metadata/iframe.https.sub.html | 85 +++ .../wpt/fetch/metadata/iframe.sub.html | 82 +++ .../wpt/fetch/metadata/img.https.sub.html | 75 +++ .../fetch/metadata/navigation.https.sub.html | 23 + .../wpt/fetch/metadata/object.https.sub.html | 62 ++ .../wpt/fetch/metadata/portal.https.sub.html | 50 ++ .../fetch/metadata/prefetch.https.sub.html | 36 ++ .../wpt/fetch/metadata/preload.https.sub.html | 50 ++ .../cross-site-redirect.https.sub.html | 77 +++ ...ultiple-redirect-cross-site.https.sub.html | 36 ++ ...wngrade-upgrade-prefetch.optional.sub.html | 15 + ...-redirect-https-downgrade-upgrade.sub.html | 73 +++ ...multiple-redirect-same-site.https.sub.html | 36 ++ ...ct-http-upgrade-prefetch.optional.sub.html | 15 + .../redirect/redirect-http-upgrade.sub.html | 67 +++ ...https-downgrade-prefetch.optional.sub.html | 15 + .../redirect-https-downgrade.sub.html | 69 +++ .../same-origin-redirect.https.sub.html | 80 +++ .../same-site-redirect.https.sub.html | 80 +++ .../wpt/fetch/metadata/report.https.sub.html | 33 ++ .../report.https.sub.html.sub.headers | 3 + .../metadata/resources/dedicatedWorker.js | 1 + ...ch-via-serviceworker--fallback--frame.html | 3 + .../fetch-via-serviceworker--fallback--sw.js | 3 + ...via-serviceworker--respondWith--frame.html | 3 + ...etch-via-serviceworker--respondWith--sw.js | 3 + .../wpt/fetch/metadata/resources/go-back.html | 3 + .../wpt/fetch/metadata/resources/helper.js | 59 ++ .../resources/redirectTestHelper.sub.js | 226 ++++++++ .../fetch/metadata/resources/sharedWorker.js | 9 + .../resources/unload-with-beacon.html | 12 + .../metadata/resources/xslt-test.sub.xml | 12 + .../wpt/fetch/metadata/script.https.sub.html | 64 +++ .../wpt/fetch/metadata/script.sub.html | 49 ++ .../metadata/serviceworker.https.sub.html | 36 ++ .../metadata/sharedworker.https.sub.html | 40 ++ .../wpt/fetch/metadata/style.https.sub.html | 86 +++ .../wpt/fetch/metadata/track.https.sub.html | 119 ++++ .../metadata/trailing-dot.https.sub.any.js | 30 + .../wpt/fetch/metadata/unload.https.sub.html | 64 +++ .../fetch/metadata/window-open.https.sub.html | 199 +++++++ .../wpt/fetch/metadata/worker.https.sub.html | 24 + .../wpt/fetch/metadata/xslt.https.sub.html | 25 + test/fixtures/wpt/fetch/nosniff/image.html | 39 ++ .../wpt/fetch/nosniff/importscripts.html | 14 + .../wpt/fetch/nosniff/importscripts.js | 28 + .../fetch/nosniff/parsing-nosniff.window.js | 27 + .../resources/x-content-type-options.json | 58 ++ test/fixtures/wpt/fetch/nosniff/script.html | 43 ++ .../wpt/fetch/nosniff/stylesheet.html | 60 ++ test/fixtures/wpt/fetch/nosniff/worker.html | 28 + .../wpt/fetch/origin/assorted.window.js | 211 +++++++ test/fixtures/wpt/fetch/range/general.any.js | 91 +++ .../wpt/fetch/range/general.window.js | 29 + .../wpt/fetch/range/resources/basic.html | 1 + .../wpt/fetch/range/resources/range-sw.js | 159 ++++++ .../wpt/fetch/range/resources/utils.js | 24 + .../wpt/fetch/range/sw.https.window.js | 151 +++++ .../redirect-navigate/302-found-post.html | 20 + .../redirect-navigate/preserve-fragment.html | 202 +++++++ .../resources/destination.html | 28 + .../wpt/fetch/redirects/data.window.js | 25 + ...kup-mitigation-data-url.tentative.sub.html | 229 ++++++++ .../dangling-markup-mitigation.tentative.html | 147 +++++ .../embedded-credentials.tentative.sub.html | 89 +++ ...edirect-to-url-with-credentials.https.html | 68 +++ .../embedded-credential-window.sub.html | 19 + .../fetch-sw.https.html | 65 +++ .../fetch/stale-while-revalidate/fetch.any.js | 32 ++ .../revalidate-not-blocked-by-csp.html | 67 +++ .../stale-while-revalidate/stale-css.html | 51 ++ .../stale-while-revalidate/stale-image.html | 55 ++ .../stale-while-revalidate/stale-script.html | 59 ++ .../stale-while-revalidate/sw-intercept.js | 14 + test/fixtures/wpt/versions.json | 4 + test/wpt/status/fetch/api/headers.json | 1 + test/wpt/test-fetch-headers.js | 15 + 408 files changed, 20009 insertions(+), 1 deletion(-) create mode 100644 lib/internal/fetch/headers.js create mode 100644 test/fixtures/wpt/fetch/META.yml create mode 100644 test/fixtures/wpt/fetch/README.md create mode 100644 test/fixtures/wpt/fetch/api/abort/cache.https.any.js create mode 100644 test/fixtures/wpt/fetch/api/abort/destroyed-context.html create mode 100644 test/fixtures/wpt/fetch/api/abort/general.any.js create mode 100644 test/fixtures/wpt/fetch/api/abort/keepalive.html create mode 100644 test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html create mode 100644 test/fixtures/wpt/fetch/api/basic/accept-header.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html create mode 100644 test/fixtures/wpt/fetch/api/basic/conditional-get.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/error-after-response.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/historical.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/keepalive.html create mode 100644 test/fixtures/wpt/fetch/api/basic/mediasource.window.js create mode 100644 test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-head.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html create mode 100644 test/fixtures/wpt/fetch/api/basic/request-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-upload.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-about.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-data.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/stream-response.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/text-utf8.any.js create mode 100644 test/fixtures/wpt/fetch/api/body/mime-type.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-worker.html create mode 100644 test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js create mode 100644 test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json create mode 100644 test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/credentials/cookies.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/header-values.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-casing.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-combine.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-errors.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-record.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-structure.any.js create mode 100644 test/fixtures/wpt/fetch/api/idlharness.any.js create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.html create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.js create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/nested-policy.js create mode 100644 test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.ogv create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/importer.js create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-bad-port.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-default.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-clone.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-consume.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-disturbed.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-error.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-001.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-002.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-003.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-keepalive.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-structure.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/resources/hello.txt create mode 100644 test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js create mode 100644 test/fixtures/wpt/fetch/api/request/url-encoding.html create mode 100644 test/fixtures/wpt/fetch/api/resources/basic.html create mode 100644 test/fixtures/wpt/fetch/api/resources/cors-top.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers create mode 100644 test/fixtures/wpt/fetch/api/resources/data.json create mode 100644 test/fixtures/wpt/fetch/api/resources/empty.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-window.html create mode 100644 test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/resources/sw-intercept.js create mode 100644 test/fixtures/wpt/fetch/api/resources/top.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/utils.js create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-clone.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-from-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-init-001.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-init-002.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-static-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js create mode 100644 test/fixtures/wpt/fetch/connection-pool/network-partition-key.html create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html create mode 100644 test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js create mode 100644 test/fixtures/wpt/fetch/content-encoding/bad-gzip-body.any.js create mode 100644 test/fixtures/wpt/fetch/content-length/content-length.html create mode 100644 test/fixtures/wpt/fetch/content-length/content-length.html.headers create mode 100644 test/fixtures/wpt/fetch/content-type/README.md create mode 100644 test/fixtures/wpt/fetch/content-type/resources/content-types.json create mode 100644 test/fixtures/wpt/fetch/content-type/resources/script-content-types.json create mode 100644 test/fixtures/wpt/fetch/content-type/response.window.js create mode 100644 test/fixtures/wpt/fetch/content-type/script.window.js create mode 100644 test/fixtures/wpt/fetch/corb/README.md create mode 100644 test/fixtures/wpt/fetch/corb/img-html-correctly-labeled.sub-ref.html create mode 100644 test/fixtures/wpt/fetch/corb/img-html-correctly-labeled.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/img-mime-types-coverage.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html create mode 100644 test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html create mode 100644 test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css create mode 100644 test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css create mode 100644 test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css create mode 100644 test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png create mode 100644 test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js create mode 100644 test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js create mode 100644 test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js create mode 100644 test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png create mode 100644 test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers create mode 100644 test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html create mode 100644 test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html create mode 100644 test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/idlharness.tentative.any.js create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/non-secure-context.window.js create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/resources/support.js create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html.headers create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html.headers create mode 100644 test/fixtures/wpt/fetch/cors-rfc1918/secure-context.https.window.js create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html create mode 100644 test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js create mode 100644 test/fixtures/wpt/fetch/data-urls/README.md create mode 100644 test/fixtures/wpt/fetch/data-urls/base64.any.js create mode 100644 test/fixtures/wpt/fetch/data-urls/processing.any.js create mode 100644 test/fixtures/wpt/fetch/data-urls/resources/base64.json create mode 100644 test/fixtures/wpt/fetch/data-urls/resources/data-urls.json create mode 100644 test/fixtures/wpt/fetch/h1-parsing/README.md create mode 100644 test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js create mode 100644 test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js create mode 100644 test/fixtures/wpt/fetch/h1-parsing/resources/README.md create mode 100644 test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis create mode 100644 test/fixtures/wpt/fetch/h1-parsing/status-code.window.js create mode 100644 test/fixtures/wpt/fetch/http-cache/304-update.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/README.md create mode 100644 test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html create mode 100644 test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html create mode 100644 test/fixtures/wpt/fetch/http-cache/cache-mode.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/cc-request.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/freshness.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/heuristic.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/http-cache.js create mode 100644 test/fixtures/wpt/fetch/http-cache/invalidate.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/partial.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/post-patch.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html create mode 100644 test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html create mode 100644 test/fixtures/wpt/fetch/http-cache/split-cache.html create mode 100644 test/fixtures/wpt/fetch/http-cache/status.any.js create mode 100644 test/fixtures/wpt/fetch/http-cache/vary.any.js create mode 100644 test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html create mode 100644 test/fixtures/wpt/fetch/metadata/META.yml create mode 100644 test/fixtures/wpt/fetch/metadata/README.md create mode 100644 test/fixtures/wpt/fetch/metadata/download.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html create mode 100644 test/fixtures/wpt/fetch/metadata/favicon.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js create mode 100644 test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--fallback.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--respondWith.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js create mode 100644 test/fixtures/wpt/fetch/metadata/fetch.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/font.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/form.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/history.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/iframe.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/iframe.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/img.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/navigation.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/object.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/portal.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/prefetch.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/preload.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/cross-site-redirect.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-cross-site.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade-prefetch.optional.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-same-site.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade-prefetch.optional.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade-prefetch.optional.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/same-origin-redirect.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/redirect/same-site-redirect.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/report.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers create mode 100644 test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--frame.html create mode 100644 test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--frame.html create mode 100644 test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/go-back.html create mode 100644 test/fixtures/wpt/fetch/metadata/resources/helper.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js create mode 100644 test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html create mode 100644 test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml create mode 100644 test/fixtures/wpt/fetch/metadata/script.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/script.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/serviceworker.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/style.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/track.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js create mode 100644 test/fixtures/wpt/fetch/metadata/unload.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/window-open.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/worker.https.sub.html create mode 100644 test/fixtures/wpt/fetch/metadata/xslt.https.sub.html create mode 100644 test/fixtures/wpt/fetch/nosniff/image.html create mode 100644 test/fixtures/wpt/fetch/nosniff/importscripts.html create mode 100644 test/fixtures/wpt/fetch/nosniff/importscripts.js create mode 100644 test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js create mode 100644 test/fixtures/wpt/fetch/nosniff/resources/x-content-type-options.json create mode 100644 test/fixtures/wpt/fetch/nosniff/script.html create mode 100644 test/fixtures/wpt/fetch/nosniff/stylesheet.html create mode 100644 test/fixtures/wpt/fetch/nosniff/worker.html create mode 100644 test/fixtures/wpt/fetch/origin/assorted.window.js create mode 100644 test/fixtures/wpt/fetch/range/general.any.js create mode 100644 test/fixtures/wpt/fetch/range/general.window.js create mode 100644 test/fixtures/wpt/fetch/range/resources/basic.html create mode 100644 test/fixtures/wpt/fetch/range/resources/range-sw.js create mode 100644 test/fixtures/wpt/fetch/range/resources/utils.js create mode 100644 test/fixtures/wpt/fetch/range/sw.https.window.js create mode 100644 test/fixtures/wpt/fetch/redirect-navigate/302-found-post.html create mode 100644 test/fixtures/wpt/fetch/redirect-navigate/preserve-fragment.html create mode 100644 test/fixtures/wpt/fetch/redirect-navigate/resources/destination.html create mode 100644 test/fixtures/wpt/fetch/redirects/data.window.js create mode 100644 test/fixtures/wpt/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/security/dangling-markup-mitigation.tentative.html create mode 100644 test/fixtures/wpt/fetch/security/embedded-credentials.tentative.sub.html create mode 100644 test/fixtures/wpt/fetch/security/redirect-to-url-with-credentials.https.html create mode 100644 test/fixtures/wpt/fetch/security/support/embedded-credential-window.sub.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/fetch-sw.https.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/fetch.any.js create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/stale-css.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/stale-image.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/stale-script.html create mode 100644 test/fixtures/wpt/fetch/stale-while-revalidate/sw-intercept.js create mode 100644 test/wpt/status/fetch/api/headers.json create mode 100644 test/wpt/test-fetch-headers.js diff --git a/lib/http.js b/lib/http.js index 491162f9c4a172..ee360a852ba4f4 100644 --- a/lib/http.js +++ b/lib/http.js @@ -72,7 +72,7 @@ module.exports = { validateHeaderName, validateHeaderValue, get, - request + request, }; ObjectDefineProperty(module.exports, 'maxHeaderSize', { diff --git a/lib/internal/fetch/headers.js b/lib/internal/fetch/headers.js new file mode 100644 index 00000000000000..eee41b808de3d2 --- /dev/null +++ b/lib/internal/fetch/headers.js @@ -0,0 +1,469 @@ +'use strict'; + +const { + ArrayFrom, + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypeSort, + ObjectEntries, + RegExpPrototypeTest, + SafeMap, + SafeSet, + StringPrototypeToLowerCase, + SymbolIterator, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + }, +} = require('internal/errors'); +const { + validateFunction, +} = require('internal/validators'); + +// https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name +const CORSNonWildcardRequestHeaderNames = new SafeSet([ + 'authorization', +]); + +// https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name +const privilegedNoCORSRequestHeaderNames = new SafeSet([ + 'range', +]); + +// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name +const CORSSafelistedResponseHeaderNames = new SafeSet([ + 'cache-control', + 'content-language', + 'content-length', + 'content-type', + 'expires', + 'last-modified', + 'pragma', +]); + +// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-header-name +const noCORSSafelistedRequestHeaderNames = new SafeSet([ + 'accept', + 'accept-language', + 'content-language', + 'content-type', +]); + +// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-header +function isNoCORSSafelistedRequestHeader(name, value) { + if (noCORSSafelistedRequestHeaderNames.has(name)) + return false; + return isCORSSafelistedRequestHeader(name, value); +} + +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header +function isCORSSafelistedRequestHeader(name, value) { + switch (StringPrototypeToLowerCase(name)) { + case 'accept': + if (hasCORSUnsafeRequestHeaderByte(value)) + return false; + break; + case 'accept-language': + case 'content-language': + if (!RegExpPrototypeTest(/^[0-9A-Za-z\x20\x2A\x2C\x2D\x2E\x3B\x3D]*$/, )) + return false; + break; + case 'content-type': { + if (hasCORSUnsafeRequestHeaderByte(value)) + return false; + const mimeType = parseMIMEType(value); + if (mimeType === null) + return false; + // If mimeType’s essence is not "application/x-www-form-urlencoded", "multipart/form-data", or "text/plain", then return false. + break; + } + default: + return false; + } + + if (value.length > 128) + return false; + + return true; +} + +// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte +const CORSUnsafeHeaderByte = /[\x00-\x08\x10-\x19\x22\x28\x29\x3A\x3C\x3E\x3F\x40\x5B\x5C\x5D\x7B\x7D\x7F]/; +function hasCORSUnsafeRequestHeaderByte(value) { + return RegExpPrototypeTest(CORSUnsafeHeaderByte, value); +} + +// https://fetch.spec.whatwg.org/#forbidden-header-name +const forbiddenHeaderNames = new SafeSet([ + 'accept-charset', + 'accept-encoding', + 'access-control-request-headers', + 'access-control-request-method', + 'connection', + 'content-length', + 'cookie', + 'cookie2', + 'date', + 'dnt', + 'expect', + 'host', + 'keep-alive', + 'origin', + 'referer', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', + 'via', +]); + +// https://fetch.spec.whatwg.org/#forbidden-response-header-name +const forbiddenResponseHeaderNames = new SafeSet([ + 'set-cookie', + 'set-cookie2', +]); + +// https://fetch.spec.whatwg.org/#request-body-header-name +const requestBodyHeaderNames = new SafeSet([ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type', +]); + +// https://fetch.spec.whatwg.org/#concept-header-list +class HeaderListItem { + constructor(name, value) { + this.name = name; + this.values = [value]; + } +} + +class HeaderList { + constructor() { + this.headers = new SafeMap(); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get-structured-header + getStructuredFieldValue(name, type) { + const value = this.get(name); + if (value === null) { + return null; + } + return parseStructuredFields(value, type); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set-structured-header + setStructuredFieldValue(name, structuredValue) { + const serializedValue = serializeStructuredFields(structuredValue); + this.set(name, serializedValue); + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains(name) { + name = StringPrototypeToLowerCase(name); + return this.headers.has(name); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get(name) { + name = StringPrototypeToLowerCase(name); + const header = this.headers.get(name); + if (header === undefined) { + return null; + } else { + return ArrayPrototypeJoin(header.values, ', '); + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split + getDecodeAndSplit(name) { + const initialValue = this.get(name); + if (initialValue === null) { + return null; + } + // No need to decode, as it is already a string. + const input = initialValue; + // TODO + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append(name, value) { + const lowerName = StringPrototypeToLowerCase(name); + const header = this.headers.get(lowerName); + if (header === undefined) { + this.headers.set(lowerName, new HeaderListItem(name, value)) + } else { + header.values.push(value); + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete(name) { + this.headers.delete(StringPrototypeToLowerCase(name)); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set(name, value) { + this.headers.set( + StringPrototypeToLowerCase(name), + new HeaderListItem(name, value) + ); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-combine + combine(name, value) { + const lowerName = StringPrototypeToLowerCase(name); + const header = this.headers.get(lowerName); + if (header === undefined) { + this.headers.set(lowerName, new HeaderListItem(name, value)) + } else { + header.values[0] += `, ${value}`; + } + } + + // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set + convertHeaderNamesToSortedLowercaseSet() { + const headerNames = ArrayFrom(this.headers.keys()); + const lowerHeaderNames = ArrayPrototypeMap(headerNames, (headerName) => { + return StringPrototypeToLowerCase(headerName); + }); + return ArrayPrototypeSort(lowerHeaderNames); + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + sortAndCombine() { + const headers = []; + const names = this.convertHeaderNamesToSortedLowercaseSet(); + ArrayPrototypeForEach(names, (name) => { + headers.push([name, this.get(name)]); + }); + return headers; + } + + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names + CORSUnsafeRequestHeaderNames() { + // TODO + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + #headerList; + #guard; + + // https://fetch.spec.whatwg.org/#concept-headers-append + #append(name, value) { + name = normalizeName(name); + value = normalizeValue(value); + + const lowerName = StringPrototypeToLowerCase(name); + + if (this.#guard === 'immutable') { + throw new TypeError('this Headers object is immutable'); + } else if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { + return; + } else if (this.#guard === 'request-no-cors') { + let temporaryValue = this.#headerList.get(name); + if (temporaryValue === null) { + temporaryValue = value; + } else { + temporaryValue = `${temporaryValue}, ${value}`; + } + if (!isNoCORSSafelistedRequestHeader(name, value)) { + return; + } + } else if (this.#guard === 'response' && forbiddenResponseHeaderNames.has(name)) { + return; + } + + this.#headerList.append(name, value); + + if (this.#guard === 'request-no-cors') { + this.#removePrivilegedNoCORSRequestHeaders(); + } + } + + // https://fetch.spec.whatwg.org/#concept-headers-fill + #fill(init) { + if (typeof init !== 'object' || init === null) { + throw new ERR_INVALID_ARG_TYPE('init', 'object', init); + } + if (typeof init[SymbolIterator] === 'function') { + for (const header of init) { + if (header === null || + typeof header[SymbolIterator] !== 'function' || + typeof header === 'string') { + throw new ERR_INVALID_ARG_TYPE('init.header', 'Iterable', header); + } + const pair = ArrayFrom(header); + if (pair.length !== 2) { + throw new ERR_INVALID_ARG_TYPE('init.header', 'of length two', pair); + } + this.#append(header[0], header[1]); + } + } else { + ArrayPrototypeForEach(ObjectEntries(init), (header) => { + this.#append(header[0], header[1]); + }) + } + } + + // https://fetch.spec.whatwg.org/#concept-headers-remove-privileged-no-cors-request-headers + #removePrivilegedNoCORSRequestHeaders() { + for (const headerName of privilegedNoCORSRequestHeaderNames) { + this.#headerList.delete(headerName); + } + } + + // https://fetch.spec.whatwg.org/#dom-headers + constructor(init) { + this.#headerList = new HeaderList(); + this.#guard = 'none'; + if (init !== undefined){ + this.#fill(init); + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append(name, value) { + this.#append(name, value); + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete(name) { + name = normalizeName(name); + const lowerName = StringPrototypeToLowerCase(name); + if (this.#guard === 'immutable') { + throw new TypeError('this Headers object is immutable'); + } + if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { + return; + } + if (this.#guard === 'request-no-cors' && + !noCORSSafelistedRequestHeaderNames.has(lowerName) && + !privilegedNoCORSRequestHeaderNames.has(lowerName)) { + return; + } + if (this.#guard === 'response' && + forbiddenResponseHeaderNames.has(lowerName)) { + return; + } + if (!this.#headerList.contains(lowerName)) { + return; + } + this.#headerList.delete(lowerName); + if (this.#guard === 'request-no-cors') { + this.#removePrivilegedNoCORSRequestHeaders(); + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get(name) { + name = normalizeName(name); + return this.#headerList.get(name); + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has(name) { + name = normalizeName(name); + return this.#headerList.contains(name); + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set(name, value) { + name = normalizeName(name); + value = normalizeValue(value); + const lowerName = StringPrototypeToLowerCase(name); + if (this.#guard === 'immutable') { + throw new TypeError('this Headers object is immutable'); + } + if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { + return; + } + if (this.#guard === 'request-no-cors' && + !noCORSSafelistedRequestHeaderNames.has(lowerName)) { + return; + } + if (this.#guard === 'response' && + forbiddenResponseHeaderNames.has(lowerName)) { + return; + } + this.#headerList.set(name, value); + if (this.#guard === 'request-no-cors') { + this.#removePrivilegedNoCORSRequestHeaders(); + } + } + + *entries() { + const headers = this.#headerList.sortAndCombine(); + yield* headers; + } + + *keys() { + for (const entry of this.entries()) { + yield entry[0]; + } + } + + *values() { + for (const entry of this.entries()) { + yield entry[1]; + } + } + + [SymbolIterator]() { + return this.entries(); + } + + forEach(callback) { + validateFunction(callback, 'callback'); + for (const entry of this.entries()) { + callback(entry[1], entry[0], this); + } + } +} + +function normalizeName(name) { + name = `${name}`; + if (!/^[!#$%&'*+\-.^_`|~0-9a-z]+$/i.test(name)) { + throw new TypeError('invalid characters in name'); + }; + return name; +} + +function normalizeValue(potentialValue) { + potentialValue = `${potentialValue}`; + const value = potentialValue + .replace(/^[\n\r\t ]+/, '') + .replace(/[\n\r\t ]+$/, ''); + if (/[\0\n\r]/.test(value)) { + throw new TypeError('invalid characters in value'); + } + return value; +} + +// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-19#section-4.1 +function serializeStructuredFields(structuredValue) { + // TODO + return null; +} + +// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-19#section-4.2 +function parseStructuredFields(inputString, headerType) { + // TODO + return null; +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +function parseMIMEType(input) { + // TODO + return null; +} + +module.exports = { + Headers, +}; diff --git a/node.gyp b/node.gyp index dbcbf4d8ca2f1d..14f688aea14966 100644 --- a/node.gyp +++ b/node.gyp @@ -151,6 +151,7 @@ 'lib/internal/errors.js', 'lib/internal/error_serdes.js', 'lib/internal/event_target.js', + 'lib/internal/fetch/headers.js', 'lib/internal/fixed_queue.js', 'lib/internal/freelist.js', 'lib/internal/freeze_intrinsics.js', diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 1bcebbec49ff7d..c26f77b8d345fd 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -21,6 +21,7 @@ Last update: - common: https://github.com/web-platform-tests/wpt/tree/841a51412f/common - dom/abort: https://github.com/web-platform-tests/wpt/tree/7caa3de747/dom/abort - FileAPI: https://github.com/web-platform-tests/wpt/tree/d9d921b8f9/FileAPI +- fetch: https://github.com/web-platform-tests/wpt/tree/5c46bbe8d0/fetch [Web Platform Tests]: https://github.com/web-platform-tests/wpt [`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/master/docs/git-node.md#git-node-wpt diff --git a/test/fixtures/wpt/fetch/META.yml b/test/fixtures/wpt/fetch/META.yml new file mode 100644 index 00000000000000..43f9dc51cfd31e --- /dev/null +++ b/test/fixtures/wpt/fetch/META.yml @@ -0,0 +1,8 @@ +spec: https://fetch.spec.whatwg.org/ +suggested_reviewers: + - jdm + - youennf + - annevk + - mnot + - yutakahirano + - domfarolino diff --git a/test/fixtures/wpt/fetch/README.md b/test/fixtures/wpt/fetch/README.md new file mode 100644 index 00000000000000..dcaad0219d5a53 --- /dev/null +++ b/test/fixtures/wpt/fetch/README.md @@ -0,0 +1,6 @@ +Tests for the [Fetch Standard](https://fetch.spec.whatwg.org/). + +More Fetch tests can be found in + +* /cors +* /xhr diff --git a/test/fixtures/wpt/fetch/api/abort/cache.https.any.js b/test/fixtures/wpt/fetch/api/abort/cache.https.any.js new file mode 100644 index 00000000000000..bdaf0e69e58010 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/abort/cache.https.any.js @@ -0,0 +1,47 @@ +// META: title=Request signals & the cache API +// META: global=window,worker + +promise_test(async () => { + await caches.delete('test'); + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/data.json', { signal }); + + const cache = await caches.open('test'); + await cache.put(request, new Response('')); + + const requests = await cache.keys(); + + assert_equals(requests.length, 1, 'Ensuring cleanup worked'); + + const [cachedRequest] = requests; + + controller.abort(); + + assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); + + const data = await fetch(cachedRequest).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signals are not stored in the cache API"); + +promise_test(async () => { + await caches.delete('test'); + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/data.json', { signal }); + controller.abort(); + + const cache = await caches.open('test'); + await cache.put(request, new Response('')); + + const requests = await cache.keys(); + + assert_equals(requests.length, 1, 'Ensuring cleanup worked'); + + const [cachedRequest] = requests; + + assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); + + const data = await fetch(cachedRequest).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signals are not stored in the cache API, even if they're already aborted"); diff --git a/test/fixtures/wpt/fetch/api/abort/destroyed-context.html b/test/fixtures/wpt/fetch/api/abort/destroyed-context.html new file mode 100644 index 00000000000000..161d39bd9ce3db --- /dev/null +++ b/test/fixtures/wpt/fetch/api/abort/destroyed-context.html @@ -0,0 +1,27 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/api/abort/general.any.js b/test/fixtures/wpt/fetch/api/abort/general.any.js new file mode 100644 index 00000000000000..7c8d1b51cd4b07 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/abort/general.any.js @@ -0,0 +1,531 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../request/request-error.js + +const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text']; + +// This is used to close connections that weren't correctly closed during the tests, +// otherwise you can end up running out of HTTP connections. +let requestAbortKeys = []; + +function abortRequests() { + const keys = requestAbortKeys; + requestAbortKeys = []; + return Promise.all( + keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`)) + ); +} + +const hostInfo = get_host_info(); +const urlHostname = hostInfo.REMOTE_HOST; + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const fetchPromise = fetch('../resources/data.json', { signal }); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Aborting rejects with AbortError"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const url = new URL('../resources/data.json', location); + url.hostname = urlHostname; + + const fetchPromise = fetch(url, { + signal, + mode: 'no-cors' + }); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Aborting rejects with AbortError - no-cors"); + +// Test that errors thrown from the request constructor take priority over abort errors. +// badRequestArgTests is from response-error.js +for (const { args, testName } of badRequestArgTests) { + promise_test(async t => { + try { + // If this doesn't throw, we'll effectively skip the test. + // It'll fail properly in ../request/request-error.html + new Request(...args); + } + catch (err) { + const controller = new AbortController(); + controller.abort(); + + // Add signal to 2nd arg + args[1] = args[1] || {}; + args[1].signal = controller.signal; + await promise_rejects_js(t, TypeError, fetch(...args)); + } + }, `TypeError from request constructor takes priority - ${testName}`); +} + +test(() => { + const request = new Request(''); + assert_true(Boolean(request.signal), "Signal member is present & truthy"); + assert_equals(request.signal.constructor, AbortSignal); +}, "Request objects have a signal property"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + assert_true(Boolean(request.signal), "Signal member is present & truthy"); + assert_equals(request.signal.constructor, AbortSignal); + assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); + assert_true(request.signal.aborted, `Request's signal has aborted`); + + const fetchPromise = fetch(request); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + const requestFromRequest = new Request(request); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json'); + const requestFromRequest = new Request(request, { signal }); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object, with signal on second request"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal: new AbortController().signal }); + const requestFromRequest = new Request(request, { signal }); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object, with signal on second request overriding another"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + const fetchPromise = fetch(request, {method: 'POST'}); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal retained after unrelated properties are overridden by fetch"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + const data = await fetch(request, { signal: null }).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signal removed by setting to null"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const log = []; + + await Promise.all([ + fetch('../resources/data.json', { signal }).then( + () => assert_unreached("Fetch must not resolve"), + () => log.push('fetch-reject') + ), + Promise.resolve().then(() => log.push('next-microtask')) + ]); + + assert_array_equals(log, ['fetch-reject', 'next-microtask']); +}, "Already aborted signal rejects immediately"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { + signal, + method: 'POST', + body: 'foo', + headers: { 'Content-Type': 'text/plain' } + }); + + await fetch(request).catch(() => {}); + + assert_true(request.bodyUsed, "Body has been used"); +}, "Request is still 'used' if signal is aborted before fetching"); + +for (const bodyMethod of BODY_METHODS) { + promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + + const log = []; + const response = await fetch('../resources/data.json', { signal }); + + controller.abort(); + + const bodyPromise = response[bodyMethod](); + + await Promise.all([ + bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)), + Promise.resolve().then(() => log.push('next-microtask')) + ]); + + await promise_rejects_dom(t, "AbortError", bodyPromise); + + assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']); + }, `response.${bodyMethod}() rejects if already aborted`); +} + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + controller.abort(); + + await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {}); + + // I'm hoping this will give the browser enough time to (incorrectly) make the request + // above, if it intends to. + await fetch('../resources/data.json').then(r => r.json()); + + const response = await fetch(`../resources/stash-take.py?key=${stateKey}`); + const data = await response.json(); + + assert_equals(data, null, "Request hasn't been made to the server"); +}, "Already aborted signal does not make request"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const fetches = []; + + for (let i = 0; i < 3; i++) { + const abortKey = token(); + requestAbortKeys.push(abortKey); + + fetches.push( + fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) + ); + } + + for (const fetchPromise of fetches) { + await promise_rejects_dom(t, "AbortError", fetchPromise); + } +}, "Already aborted signal can be used for many fetches"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + + await fetch('../resources/data.json', { signal }).then(r => r.json()); + + controller.abort(); + + const fetches = []; + + for (let i = 0; i < 3; i++) { + const abortKey = token(); + requestAbortKeys.push(abortKey); + + fetches.push( + fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) + ); + } + + for (const fetchPromise of fetches) { + await promise_rejects_dom(t, "AbortError", fetchPromise); + } +}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + + const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + controller.abort(); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Underlying connection is closed when aborting after receiving response"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location); + url.hostname = urlHostname; + + await fetch(url, { + signal, + mode: 'no-cors' + }); + + const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location); + stashTakeURL.hostname = urlHostname; + + const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + controller.abort(); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(stashTakeURL).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Underlying connection is closed when aborting after receiving response - no-cors"); + +for (const bodyMethod of BODY_METHODS) { + promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + + const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + const bodyPromise = response[bodyMethod](); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", bodyPromise); + + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } + }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`); +} + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + const reader = response.body.getReader(); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", reader.read()); + await promise_rejects_dom(t, "AbortError", reader.closed); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Stream errors once aborted. Underlying connection closed."); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + const reader = response.body.getReader(); + + await reader.read(); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", reader.read()); + await promise_rejects_dom(t, "AbortError", reader.closed); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Stream errors once aborted, after reading. Underlying connection closed."); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + + const response = await fetch(`../resources/empty.txt`, { signal }); + + // Read whole response to ensure close signal has sent. + await response.clone().text(); + + const reader = response.body.getReader(); + + controller.abort(); + + const item = await reader.read(); + + assert_true(item.done, "Stream is done"); +}, "Stream will not error if body is empty. It's closed with an empty queue before it errors."); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + let cancelReason; + + const body = new ReadableStream({ + pull(controller) { + controller.enqueue(new Uint8Array([42])); + }, + cancel(reason) { + cancelReason = reason; + } + }); + + const fetchPromise = fetch('../resources/empty.txt', { + body, signal, + method: 'POST', + headers: { + 'Content-Type': 'text/plain' + } + }); + + assert_true(!!cancelReason, 'Cancel called sync'); + assert_equals(cancelReason.constructor, DOMException); + assert_equals(cancelReason.name, 'AbortError'); + + await promise_rejects_dom(t, "AbortError", fetchPromise); + + const fetchErr = await fetchPromise.catch(e => e); + + assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance"); +}, "Readable stream synchronously cancels with AbortError if aborted before reading"); + +test(() => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('.', { signal }); + const requestSignal = request.signal; + + const clonedRequest = request.clone(); + + assert_equals(requestSignal, request.signal, "Original request signal the same after cloning"); + assert_true(request.signal.aborted, "Original request signal aborted"); + assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal"); + assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted"); +}, "Signal state is cloned"); + +test(() => { + const controller = new AbortController(); + const signal = controller.signal; + + const request = new Request('.', { signal }); + const clonedRequest = request.clone(); + + const log = []; + + request.signal.addEventListener('abort', () => log.push('original-aborted')); + clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted')); + + controller.abort(); + + assert_array_equals(log, ['clone-aborted', 'original-aborted'], "Abort events fired in correct order"); + assert_true(request.signal.aborted, 'Signal aborted'); + assert_true(clonedRequest.signal.aborted, 'Signal aborted'); +}, "Clone aborts with original controller"); diff --git a/test/fixtures/wpt/fetch/api/abort/keepalive.html b/test/fixtures/wpt/fetch/api/abort/keepalive.html new file mode 100644 index 00000000000000..db12df0d289be9 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/abort/keepalive.html @@ -0,0 +1,85 @@ + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html b/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html new file mode 100644 index 00000000000000..603f29eac981c7 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html @@ -0,0 +1,105 @@ + + + + + Aborting fetch when intercepted by a service worker + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/basic/accept-header.any.js b/test/fixtures/wpt/fetch/api/basic/accept-header.any.js new file mode 100644 index 00000000000000..cd54cf2a03e8a9 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/accept-header.any.js @@ -0,0 +1,34 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'"); + }); +}, "Request through fetch should have 'accept' header with value '*/*'"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'"); + }); +}, "Request through fetch should have 'accept' header with value 'custom/*'"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_true(response.headers.has("x-request-accept-language")); + }); +}, "Request through fetch should have a 'accept-language' header"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'"); + }); +}, "Request through fetch should have 'accept-language' header with value 'bzh'"); diff --git a/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html b/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html new file mode 100644 index 00000000000000..afc2bbbafb0942 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html @@ -0,0 +1,43 @@ + + +Block mime type as script + + +
+ diff --git a/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js b/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js new file mode 100644 index 00000000000000..2f9fa81c02b18b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js @@ -0,0 +1,38 @@ +// META: title=Request ETag +// META: global=window,worker +// META: script=/common/utils.js + +promise_test(function() { + var cacheBuster = token(); // ensures first request is uncached + var url = "../resources/cache.py?v=" + cacheBuster; + var etag; + + // make the first request + return fetch(url).then(function(response) { + // ensure we're getting the regular, uncached response + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), null) + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a second request + return fetch(url); + }).then(function(response) { + // while the server responds with 304 if our browser sent the correct + // If-None-Match request header, at the JavaScript level this surfaces + // as 200 + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), "304") + + etag = response.headers.get("ETag") + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a third request, explicitly setting If-None-Match request header + var headers = { "If-None-Match": etag } + return fetch(url, { headers: headers }) + }).then(function(response) { + // 304 now surfaces thanks to the explicit If-None-Match request header + assert_equals(response.status, 304); + }); +}, "Testing conditional GET with ETags"); diff --git a/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js b/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js new file mode 100644 index 00000000000000..f7114425f95504 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js @@ -0,0 +1,24 @@ +// META: title=Fetch: network timeout after receiving the HTTP response headers +// META: global=window,worker +// META: timeout=long +// META: script=../resources/utils.js + +function checkReader(test, reader, promiseToTest) +{ + return reader.read().then((value) => { + validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk"); + return promise_rejects_js(test, TypeError, promiseToTest(reader)); + }); +} + +promise_test((test) => { + return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { + return checkReader(test, response.body.getReader(), reader => reader.read()); + }); +}, "Response reader read() promise should reject after a network error happening after resolving fetch promise"); + +promise_test((test) => { + return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { + return checkReader(test, response.body.getReader(), reader => reader.closed); + }); +}, "Response reader closed promise should reject after a network error happening after resolving fetch promise"); diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js new file mode 100644 index 00000000000000..bb70d87d250cda --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker + +[ + ["content-length", "0", "header-content-length"], + ["content-length", "0, 0", "header-content-length-twice"], + ["double-trouble", ", ", "headers-double-empty"], + ["foo-test", "1, 2, 3", "headers-basic"], + ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"], + ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"], +].forEach(testValues => { + promise_test(async t => { + const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis"); + assert_equals(response.headers.get(testValues[0]), testValues[1]); + }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]); +}); diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js new file mode 100644 index 00000000000000..741d83bf7aaa55 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js @@ -0,0 +1,5 @@ +// META: global=window,worker + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x"))); +}, "Ensure fetch() rejects null bytes in headers"); diff --git a/test/fixtures/wpt/fetch/api/basic/historical.any.js b/test/fixtures/wpt/fetch/api/basic/historical.any.js new file mode 100644 index 00000000000000..c8081262168e36 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/historical.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +test(() => { + assert_false("getAll" in new Headers()); + assert_false("getAll" in Headers.prototype); +}, "Headers object no longer has a getAll() method"); + +test(() => { + assert_false("type" in new Request("about:blank")); + assert_false("type" in Request.prototype); +}, "'type' getter should not exist on Request objects"); + +// See https://github.com/whatwg/fetch/pull/979 for the removal +test(() => { + assert_false("trailer" in new Response()); + assert_false("trailer" in Response.prototype); +}, "Response object no longer has a trailer getter"); diff --git a/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js b/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js new file mode 100644 index 00000000000000..56dbd4909f6a43 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js @@ -0,0 +1,77 @@ +// META: global=window,dedicatedworker,sharedworker +// META: script=../resources/utils.js + +function integrity(desc, url, integrity, initRequestMode, shouldPass) { + var fetchRequestInit = {'integrity': integrity} + if (!!initRequestMode && initRequestMode !== "") { + fetchRequestInit.mode = initRequestMode; + } + + if (shouldPass) { + promise_test(function(test) { + return fetch(url, fetchRequestInit).then(function(resp) { + if (initRequestMode !== "no-cors") { + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.status, 0, "Opaque response's status is 0"); + assert_equals(resp.type, "opaque"); + } + }); + }, desc); + } else { + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit)); + }, desc); + } +} + +const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk="; +const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL"; +const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I="; +const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg=="; + +const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +const url = path; +const corsUrl = + `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`; +const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}` + +integrity("Empty string integrity", url, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Invalid integrity", url, invalidSha256, + /* initRequestMode */ undefined, /* shouldPass */ false); +integrity("Multiple integrities: valid stronger than invalid", url, + invalidSha256 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: invalid stronger than valid", + url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("Multiple integrities: invalid as strong as valid", url, + invalidSha512 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are valid", url, + topSha384 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are invalid", url, + invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("CORS SHA-512 integrity", corsUrl, topSha512, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("CORS invalid integrity", corsUrl, invalidSha512, + /* initRequestMode */ undefined, /* shouldPass */ false); + +integrity("Empty string integrity for opaque response", corsUrl2, "", + /* initRequestMode */ "no-cors", /* shouldPass */ true); +integrity("SHA-* integrity for opaque response", corsUrl2, topSha512, + /* initRequestMode */ "no-cors", /* shouldPass */ false); + +done(); diff --git a/test/fixtures/wpt/fetch/api/basic/keepalive.html b/test/fixtures/wpt/fetch/api/basic/keepalive.html new file mode 100644 index 00000000000000..36d156bba43606 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/keepalive.html @@ -0,0 +1,106 @@ + + + +Fetch API: keepalive handling + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/basic/mediasource.window.js b/test/fixtures/wpt/fetch/api/basic/mediasource.window.js new file mode 100644 index 00000000000000..1f89595393da41 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/mediasource.window.js @@ -0,0 +1,5 @@ +promise_test(t => { + const mediaSource = new MediaSource(), + mediaSourceURL = URL.createObjectURL(mediaSource); + return promise_rejects_js(t, TypeError, fetch(mediaSourceURL)); +}, "Cannot fetch blob: URL from a MediaSource"); diff --git a/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js b/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js new file mode 100644 index 00000000000000..a4abcac55f39a9 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js + +function fetchNoCors(url, isOpaqueFiltered) { + var urlQuery = "?pipe=header(x-is-filtered,value)" + promise_test(function(test) { + if (isOpaqueFiltered) + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 0, "Opaque filter: status is 0"); + assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); + assert_equals(resp.url, "", "Opaque filter: url is \"\""); + assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); + assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered"); + }); + else + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered"); + }); + }, "Fetch "+ url + " with no-cors mode"); +} + +fetchNoCors(RESOURCES_DIR + "top.txt", false); +fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false); +fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true); +fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true); + +done(); + diff --git a/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js b/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js new file mode 100644 index 00000000000000..1457702f1b163b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js @@ -0,0 +1,28 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function fetchSameOrigin(url, shouldPass) { + promise_test(function(test) { + if (shouldPass) + return fetch(url , {"mode": "same-origin"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + }); + else + return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"})); + }, "Fetch "+ url + " with same-origin mode"); +} + +var host_info = get_host_info(); + +fetchSameOrigin(RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); diff --git a/test/fixtures/wpt/fetch/api/basic/referrer.any.js b/test/fixtures/wpt/fetch/api/basic/referrer.any.js new file mode 100644 index 00000000000000..85745e692a2fe0 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/referrer.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function runTest(url, init, expectedReferrer, title) { + promise_test(function(test) { + url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors"; + + return fetch(url , init).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct"); + }); + }, title); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py"; +var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py"; +var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ; +var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL"); +runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL"); +runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection"); +runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection"); + + +var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@"); +runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped"); +var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier"; +runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js new file mode 100644 index 00000000000000..5d85c4e62d32b0 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js @@ -0,0 +1,43 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function requestForbiddenHeaders(desc, forbiddenHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": forbiddenHeaders} + var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in forbiddenHeaders) + assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined"); + }); + }, desc); +} + +requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"}); +requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""}); + +requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""}); +requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""}); +requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"}); +requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"}); +requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"}); +requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"}); +requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"}); +requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"}); +requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"}); +requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"}); +requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"}); +requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"}); +requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"}); +requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"}); +requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"}); +requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"}); +requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"}); +requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"}); +requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"}); +requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"}); +requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"}); +requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"}); diff --git a/test/fixtures/wpt/fetch/api/basic/request-head.any.js b/test/fixtures/wpt/fetch/api/basic/request-head.any.js new file mode 100644 index 00000000000000..e0b6afa079a400 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-head.any.js @@ -0,0 +1,6 @@ +// META: global=window,worker + +promise_test(function(test) { + var requestInit = {"method": "HEAD", "body": "test"}; + return promise_rejects_js(test, TypeError, fetch(".", requestInit)); +}, "Fetch with HEAD with body"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js new file mode 100644 index 00000000000000..4c10e717f8c2e5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js @@ -0,0 +1,13 @@ +// META: global=window,worker + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-is-A-test: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-is-A-test first)") + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)") diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js new file mode 100644 index 00000000000000..4a9a8011385351 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker + +// This tests characters that are not +// https://infra.spec.whatwg.org/#ascii-code-point +// but are still +// https://infra.spec.whatwg.org/#byte-value +// in request header values. +// Such request header values are valid and thus sent to servers. +// Characters outside the #byte-value range are tested e.g. in +// fetch/api/headers/headers-errors.html. + +promise_test(() => { + return fetch( + "../resources/inspect-headers.py?headers=accept|x-test", + {headers: { + "Accept": "before-æøå-after", + "X-Test": "before-ß-after" + }}) + .then(res => { + assert_equals( + res.headers.get("x-request-accept"), + "before-æøå-after", + "Accept Header"); + assert_equals( + res.headers.get("x-request-x-test"), + "before-ß-after", + "X-Test Header"); + }); +}, "Non-ascii bytes in request headers"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers.any.js new file mode 100644 index 00000000000000..ac54256e4c6a63 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-headers.any.js @@ -0,0 +1,82 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkContentType(contentType, body) +{ + if (self.FormData && body instanceof self.FormData) { + assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType); + return; + } + + var expectedContentType = "text/plain;charset=UTF-8"; + if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer) + expectedContentType = null; + else if (body instanceof Blob) + expectedContentType = body.type ? body.type : null; + else if (body instanceof URLSearchParams) + expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8"; + + assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType); +} + +function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) { + var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type"; + var requestInit = {"method": method} + promise_test(function(test){ + if (typeof body === "function") + body = body(); + if (body) + requestInit["body"] = body; + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent"); + assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset"); + assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin); + if (expectedContentLength !== undefined) + assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength); + checkContentType(resp.headers.get("x-request-content-type"), body); + }); + }, desc); +} + +var url = RESOURCES_DIR + "inspect-headers.py" + +requestHeaders("Fetch with GET", url, "GET", null, null, null); +requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null); +requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin); +requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10"); +requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4"); +requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4"); +requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4"); +requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8"); +requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4"); +requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4"); +requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null); +requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14"); + +function requestOriginHeader(method, mode, needsOrigin) { + promise_test(function(test){ + return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + if(needsOrigin) + assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin); + else + assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header") + }); + }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header"); +} + +requestOriginHeader("GET", "cors", false); +requestOriginHeader("POST", "same-origin", true); +requestOriginHeader("POST", "no-cors", true); +requestOriginHeader("PUT", "same-origin", true); +requestOriginHeader("TacO", "same-origin", true); +requestOriginHeader("TacO", "cors", true); diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html b/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html new file mode 100644 index 00000000000000..bdea1e185314aa --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer header + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js b/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js new file mode 100644 index 00000000000000..0c3357642d674b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function testReferrer(referrer, expected, desc) { + promise_test(function(test) { + var url = RESOURCES_DIR + "inspect-headers.py?headers=referer" + var req = new Request(url, { referrer: referrer }); + return fetch(req).then(function(resp) { + var actual = resp.headers.get("x-request-referer"); + if (expected) { + assert_equals(actual, expected, "request's referer should be: " + expected); + return; + } + if (actual) { + assert_equals(actual, "", "request's referer should be empty"); + } + }); + }, desc); +} + +testReferrer("about:client", self.location.href, 'about:client referrer'); + +var fooURL = new URL("./foo", self.location).href; +testReferrer(fooURL, fooURL, 'url referrer'); diff --git a/test/fixtures/wpt/fetch/api/basic/request-upload.any.js b/test/fixtures/wpt/fetch/api/basic/request-upload.any.js new file mode 100644 index 00000000000000..1412816a3c220d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/request-upload.any.js @@ -0,0 +1,125 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function testUpload(desc, url, method, createBody, expectedBody) { + const requestInit = {"method": method} + promise_test(function(test){ + const body = createBody(); + if (body) { + requestInit["body"] = body; + } + return fetch(url, requestInit).then(function(resp) { + return resp.text().then((text)=> { + assert_equals(text, expectedBody); + }); + }); + }, desc); +} + +function testUploadFailure(desc, url, method, createBody) { + const requestInit = {"method": method}; + promise_test(t => { + const body = createBody(); + if (body) { + requestInit["body"] = body; + } + return promise_rejects_js(t, TypeError, fetch(url, requestInit)); + }, desc); +} + +const url = RESOURCES_DIR + "echo-content.py" + +testUpload("Fetch with PUT with body", url, + "PUT", + () => "Request's body", + "Request's body"); +testUpload("Fetch with POST with text body", url, + "POST", + () => "Request's body", + "Request's body"); +testUpload("Fetch with POST with URLSearchParams body", url, + "POST", + () => new URLSearchParams("name=value"), + "name=value"); +testUpload("Fetch with POST with Blob body", url, + "POST", + () => new Blob(["Test"]), + "Test"); +testUpload("Fetch with POST with ArrayBuffer body", url, + "POST", + () => new ArrayBuffer(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Uint8Array body", url, + "POST", + () => new Uint8Array(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Int8Array body", url, + "POST", + () => new Int8Array(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Float32Array body", url, + "POST", + () => new Float32Array(1), + "\0\0\0\0"); +testUpload("Fetch with POST with Float64Array body", url, + "POST", + () => new Float64Array(1), + "\0\0\0\0\0\0\0\0"); +testUpload("Fetch with POST with DataView body", url, + "POST", + () => new DataView(new ArrayBuffer(8), 0, 4), + "\0\0\0\0"); +testUpload("Fetch with POST with Blob body with mime type", url, + "POST", + () => new Blob(["Test"], { type: "text/maybe" }), + "Test"); +testUpload("Fetch with POST with ReadableStream", url, + "POST", + () => { + return new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}) + }, + "Test"); +testUploadFailure("Fetch with POST with ReadableStream containing String", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue("Test"); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing null", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(null); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing number", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(99); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(new ArrayBuffer()); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing Blob", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(new Blob()); + controller.close(); + }}) + }); diff --git a/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js b/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js new file mode 100644 index 00000000000000..0d123c429445f1 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js @@ -0,0 +1,16 @@ +function checkResponseURL(fetchedURL, expectedURL) +{ + promise_test(function() { + return fetch(fetchedURL).then(function(response) { + assert_equals(response.url, expectedURL); + }); + }, "Testing response url getter with " +fetchedURL); +} + +var baseURL = "http://{{host}}:{{ports[http][0]}}"; +checkResponseURL(baseURL + "/ada", baseURL + "/ada"); +checkResponseURL(baseURL + "/#", baseURL + "/"); +checkResponseURL(baseURL + "/#ada", baseURL + "/"); +checkResponseURL(baseURL + "#ada", baseURL + "/"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js new file mode 100644 index 00000000000000..4329bd070320dd --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js @@ -0,0 +1,18 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkNetworkError(url, method) { + method = method || "GET"; + const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO" + promise_test(function(test) { + var promise = fetch(url, { method: method }); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +checkNetworkError("about:blank", "GET"); +checkNetworkError("about:blank", "PUT"); +checkNetworkError("about:blank", "POST"); +checkNetworkError("about:invalid.com"); +checkNetworkError("about:config"); +checkNetworkError("about:unicorn"); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js new file mode 100644 index 00000000000000..6e63cbecaa52c5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js @@ -0,0 +1,45 @@ +// META: script=../resources/utils.js + +function checkFetchResponse(url, data, mime, size, desc) { + promise_test(function(test) { + size = size.toString(); + return fetch(url).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); + assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length")); + return resp.text(); + }).then(function(bodyAsText) { + assert_equals(bodyAsText, data, "Response's body is " + data); + }); + }, desc); +} + +var blob = new Blob(["Blob's data"], { "type" : "text/plain" }); +checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size, + "Fetching [GET] URL.createObjectURL(blob) is OK"); + +function checkKoUrl(url, method, desc) { + promise_test(function(test) { + var promise = fetch(url, {"method": method}); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" }); +checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET", + "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO"); + +var invalidRequestMethods = [ + "POST", + "OPTIONS", + "HEAD", + "PUT", + "DELETE", + "INVALID", +]; +invalidRequestMethods.forEach(function(method) { + checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO"); +}); + +done(); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js new file mode 100644 index 00000000000000..1c3b5dc816c88b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js @@ -0,0 +1,43 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkFetchResponse(url, data, mime, fetchMode, method) { + var cut = (url.length >= 40) ? "[...]" : ""; + desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK"; + var init = {"method": method || "GET"}; + if (fetchMode) { + init.mode = fetchMode; + desc += " (" + fetchMode + ")"; + } + promise_test(function(test) { + return fetch(url, init).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.statusText, "OK", "HTTP statusText is OK"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(body) { + assert_equals(body, data, "Response's body is correct"); + }); + }, desc); +} + +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors"); +checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain"); +checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5", + "response's body", + "image/png"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST"); +checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD"); + +function checkKoUrl(url, method, desc) { + var cut = (url.length >= 40) ? "[...]" : ""; + desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO" + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, {"method": method})); + }, desc); +} + +checkKoUrl("data:notAdataUrl.com", "GET"); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js new file mode 100644 index 00000000000000..550f69c41b5a43 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js @@ -0,0 +1,31 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkKoUrl(url, desc) { + if (!desc) + desc = "Fetching " + url.substring(0, 45) + " is KO" + promise_test(function(test) { + var promise = fetch(url); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/"; +checkKoUrl("aaa" + urlWithoutScheme); +checkKoUrl("cap" + urlWithoutScheme); +checkKoUrl("cid" + urlWithoutScheme); +checkKoUrl("dav" + urlWithoutScheme); +checkKoUrl("dict" + urlWithoutScheme); +checkKoUrl("dns" + urlWithoutScheme); +checkKoUrl("geo" + urlWithoutScheme); +checkKoUrl("im" + urlWithoutScheme); +checkKoUrl("imap" + urlWithoutScheme); +checkKoUrl("ipp" + urlWithoutScheme); +checkKoUrl("ldap" + urlWithoutScheme); +checkKoUrl("mailto" + urlWithoutScheme); +checkKoUrl("nfs" + urlWithoutScheme); +checkKoUrl("pop" + urlWithoutScheme); +checkKoUrl("rtsp" + urlWithoutScheme); +checkKoUrl("snmp" + urlWithoutScheme); + +done(); diff --git a/test/fixtures/wpt/fetch/api/basic/stream-response.any.js b/test/fixtures/wpt/fetch/api/basic/stream-response.any.js new file mode 100644 index 00000000000000..4a8855a62d7b0d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/stream-response.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function streamBody(reader, test, count) { + return reader.read().then(function(data) { + if (!data.done && count < 2) { + count += 1; + return streamBody(reader, test, count); + } else { + test.step(function() { + assert_true(count >= 2, "Retrieve body progressively"); + }); + } + }); +} + +//simulate streaming: +//count is large enough to let the UA deliver the body before it is completely retrieved +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) { + var count = 0; + if (resp.body) + return streamBody(resp.body.getReader(), test, count); + else + test.step(function() { + assert_unreached( "Body does not exist in response"); + }); + }); +}, "Stream response's body"); diff --git a/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js b/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js new file mode 100644 index 00000000000000..37c821ce73f25c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js @@ -0,0 +1,54 @@ +// META: global=window,worker + +// These tests verify that stream creation is not affected by changes to +// Object.prototype. + +const creationCases = { + fetch: async () => fetch(location.href), + request: () => new Request(location.href, {method: 'POST', body: 'hi'}), + response: () => new Response('bye'), + consumeEmptyResponse: () => new Response().text(), + consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(), + consumeEmptyRequest: () => new Request(location.href).text(), + consumeNonEmptyRequest: () => new Request(location.href, + {method: 'POST', body: 'yes'}).arrayBuffer(), +}; + +for (creationCase of Object.keys(creationCases)) { + for (accessorName of ['start', 'type', 'size', 'highWaterMark']) { + promise_test(async t => { + Object.defineProperty(Object.prototype, accessorName, { + get() { throw Error(`Object.prototype.${accessorName} was accessed`); }, + configurable: true + }); + t.add_cleanup(() => { + delete Object.prototype[accessorName]; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `throwing Object.prototype.${accessorName} accessor should not affect ` + + `stream creation by '${creationCase}'`); + + promise_test(async t => { + // -1 is a convenient value which is invalid, and should cause the + // constructor to throw, for all four fields. + Object.prototype[accessorName] = -1; + t.add_cleanup(() => { + delete Object.prototype[accessorName]; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `Object.prototype.${accessorName} accessor returning invalid value ` + + `should not affect stream creation by '${creationCase}'`); + } + + promise_test(async t => { + Object.prototype.start = controller => controller.error(new Error('start')); + t.add_cleanup(() => { + delete Object.prototype.start; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `Object.prototype.start function which errors the stream should not ` + + `affect stream creation by '${creationCase}'`); +} diff --git a/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js b/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js new file mode 100644 index 00000000000000..05c8c88825d37b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js @@ -0,0 +1,74 @@ +// META: title=Fetch: Request and Response text() should decode as UTF-8 +// META: global=window,worker +// META: script=../resources/utils.js + +function testTextDecoding(body, expectedText, urlParameter, title) +{ + var arrayBuffer = stringToArray(body); + + promise_test(function(test) { + var request = new Request("", {method: "POST", body: arrayBuffer}); + return request.text().then(function(value) { + assert_equals(value, expectedText, "Request.text() should decode data as UTF-8"); + }); + }, title + " with Request.text()"); + + promise_test(function(test) { + var response = new Response(arrayBuffer); + return response.text().then(function(value) { + assert_equals(value, expectedText, "Response.text() should decode data as UTF-8"); + }); + }, title + " with Response.text()"); + + promise_test(function(test) { + return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) { + return response.text().then(function(value) { + assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); + }); + }); + }, title + " with fetched data (UTF-8 charset)"); + + promise_test(function(test) { + return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) { + return response.text().then(function(value) { + assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); + }); + }); + }, title + " with fetched data (UTF-16 charset)"); + + promise_test(function(test) { + return new Response(body).arrayBuffer().then(function(buffer) { + assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8"); + }); + }, title + " (Response object)"); + + promise_test(function(test) { + return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) { + assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8"); + }); + }, title + " (Request object)"); + +} + +var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; +var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; +var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; +var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; +var utf8Decoded = "三村かな子"; +testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM"); +testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM"); + +var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50"; +var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50"; +var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P"; +testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8"); + +var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b"; +var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b"; +var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P["; +testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8"); + +var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00"; +var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00"; +var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000"; +testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8"); diff --git a/test/fixtures/wpt/fetch/api/body/mime-type.any.js b/test/fixtures/wpt/fetch/api/body/mime-type.any.js new file mode 100644 index 00000000000000..a0f90a0abdfc3e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/body/mime-type.any.js @@ -0,0 +1,40 @@ +[ + () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }), + () => new Response("", { headers: { "Content-Type": "text/plain" } }) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + const newMIMEType = "test/test"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }), + () => new Response(new URLSearchParams()), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8"); + bodyContainer.headers.delete("Content-Type"); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + }, `${bodyContainer.constructor.name}: removing implicit Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }), + () => new Response(new ArrayBuffer()), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), null); + const newMIMEType = "test/test"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: setting missing Content-Type`); +}); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js b/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js new file mode 100644 index 00000000000000..23f5f91c87d49f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js @@ -0,0 +1,37 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function cors(desc, origin) { + var url = origin + dirname(location.pathname); + var urlParameters = "?pipe=header(Access-Control-Allow-Origin,*)"; + + promise_test(function(test) { + return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "no-cors"} ).then(function(resp) { + assert_equals(resp.status, 0, "Opaque filter: status is 0"); + assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); + assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); + return resp.text().then(function(value) { + assert_equals(value, "", "Opaque response should have an empty body"); + }); + }); + }, desc + " [no-cors mode]"); + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url + RESOURCES_DIR + "top.txt", {"mode": "cors"})); + }, desc + " [server forbid CORS]"); + + promise_test(function(test) { + return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "cors"} ).then(function(resp) { + assert_equals(resp.status, 200, "Fetch's response's status is 200"); + assert_equals(resp.type , "cors", "CORS response's type is cors"); + }); + }, desc + " [cors mode]"); +} + +var host_info = get_host_info(); + +cors("Same domain different port", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT); +cors("Same domain different protocol different port", host_info.HTTPS_ORIGIN); +cors("Cross domain basic usage", host_info.HTTP_REMOTE_ORIGIN); +cors("Cross domain different port", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); +cors("Cross domain different protocol", host_info.HTTPS_REMOTE_ORIGIN); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js new file mode 100644 index 00000000000000..f5217b42460a57 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js @@ -0,0 +1,49 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + +var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; +urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)"; + +urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)"; +urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)"; + +urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)"; +urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)"; + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Set cookies"); + +function doTest(usePreflight) { + promise_test(async (test) => { + var url = redirectUrl; + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=301"; + urlParameters += "&location=" + encodeURIComponent(urlCheckCookies); + urlParameters += "&allow_headers=a&headers=Cookie"; + headers = []; + if (usePreflight) + headers.push(["a", "b"]); + + var requestInit = {"credentials": "include", "mode": "cors", "headers": headers}; + var response = await fetch(url + urlParameters, requestInit); + + assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)"); + }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight"); +} + +doTest(false); +doTest(true); + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Clean cookies"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js new file mode 100644 index 00000000000000..8c666e4782f4c8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js @@ -0,0 +1,56 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) { + var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + //enable cors with credentials + var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlParameters += "|header(Access-Control-Allow-Credentials,true)"; + + var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)"; + if (cookies) { + urlParameters += "|header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters += "|header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentialsMode, "mode": "cors"}; + + promise_test(function(test){ + return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + //check cookies sent + return fetch(urlCheckCookies, requestInit); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentialsMode === "include" && baseURL1 === baseURL2) { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie"); + } + //clean cookies + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) { + throw e; + }) + }); + }, desc); +} + +var local = get_host_info().HTTP_ORIGIN; +var remote = get_host_info().HTTP_REMOTE_ORIGIN; +// FIXME: otherRemote might not be accessible on some test environments. +var otherRemote = local.replace("http://", "http://www."); + +corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]); +corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]); +corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]); +corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]); +corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]); +corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js new file mode 100644 index 00000000000000..340e99ab5f99d7 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js @@ -0,0 +1,41 @@ +// META: script=../resources/utils.js + +const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt", + sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|" + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "Basic Access-Control-Expose-Headers: * support") + +promise_test(() => { + const origin = location.origin, // assuming an ASCII origin + headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)" + return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted + assert_equals(resp.headers.get("test"), null) + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* for credentialed fetches only matches literally") + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* can be one of several values") + +done(); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js new file mode 100644 index 00000000000000..a26eaccf2a5c79 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js @@ -0,0 +1,69 @@ +// META: script=../resources/utils.js + +function corsFilter(corsUrl, headerName, headerValue, isFiltered) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)"; + promise_test(function(test) { + return fetch(url).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isFiltered) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, "CORS filter on " + headerName + " header"); +} + +function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" + + "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" + + "header(Access-Control-Allow-Credentials, true)" + + "header(Access-Control-Expose-Headers," + headerName + ")"; + + var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed"); + if (withCredentials) + title+= "(credentials = include)"; + promise_test(function(test) { + return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isForbidden) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, title); +} + +var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + +corsFilter(url, "Cache-Control", "no-cache", false); +corsFilter(url, "Content-Language", "fr", false); +corsFilter(url, "Content-Type", "text/html", false); +corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false); +corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false); +corsFilter(url, "Pragma", "no-cache", false); +corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top" + +corsFilter(url, "Age", "27", true); +corsFilter(url, "Server", "wptServe" , true); +corsFilter(url, "Warning", "Mind the gap" , true); +corsFilter(url, "Set-Cookie", "name=value" , true); +corsFilter(url, "Set-Cookie2", "name=value" , true); + +corsExposeFilter(url, "Age", "27", false); +corsExposeFilter(url, "Server", "wptServe" , false); +corsExposeFilter(url, "Warning", "Mind the gap" , false); + +corsExposeFilter(url, "Set-Cookie", "name=value" , true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true); +corsExposeFilter(url, "Set-Cookie", "name=value" , true, true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true, true); + +done(); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js new file mode 100644 index 00000000000000..b3abb922841c63 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function corsMultipleOrigins(originList) { + var urlParameters = "?origin=" + encodeURIComponent(originList.join(", ")); + var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters)); + }, "Listing multiple origins is illegal: " + originList); +} +/* Actual origin */ +var origin = "http://{{host}}:{{ports[http][0]}}"; + +corsMultipleOrigins(["\"\"", "http://example.com", origin]); +corsMultipleOrigins(["\"\"", "http://example.com", "*"]); +corsMultipleOrigins(["\"\"", origin, origin]); +corsMultipleOrigins(["*", "http://example.com", "*"]); +corsMultipleOrigins(["*", "http://example.com", origin]); +corsMultipleOrigins(["", "http://example.com", "https://example2.com"]); + +done(); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js new file mode 100644 index 00000000000000..7a0269aae4ec3d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js @@ -0,0 +1,41 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsNoPreflight(desc, baseURL, method, headerName, headerValue) { + + var uuid_token = token(); + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method, "headers":{}}; + if (headerName) + requestInit["headers"][headerName] = headerValue; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + }); + }); + }, desc); +} + +var host_info = get_host_info(); + +corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET"); +corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST"); +corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD"); +corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*"); +corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded"); +corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8"); +corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js b/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js new file mode 100644 index 00000000000000..30a02d910fdad5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* If origin is undefined, it is set to fetched url's origin*/ +function corsOrigin(desc, baseURL, method, origin, shouldPass) { + if (!origin) + origin = baseURL; + + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method; + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var requestInit = {"mode": "cors", "method": method}; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + if (shouldPass) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); + +} + +var host_info = get_host_info(); + +/* Actual origin */ +var origin = host_info.HTTP_ORIGIN; + +corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true); +corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false); +corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true); +corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false); +corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true); +corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false); +corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true); +corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false); +corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js new file mode 100644 index 00000000000000..ce6a169d814675 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js @@ -0,0 +1,46 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var cors_url = get_host_info().HTTP_REMOTE_ORIGIN + + dirname(location.pathname) + + RESOURCES_DIR + + "preflight.py"; + +promise_test((test) => { + var uuid_token = token(); + var request_url = + cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" + + "&allow_headers=x-test-header"; + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash") + .then(() => { + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test1"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }) + .then((res) => res.text()) + .then((txt) => { + assert_equals(txt, "1", "Server stash must be cleared."); + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test2"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }); +}); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js new file mode 100644 index 00000000000000..b2747ccd5bc09e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js @@ -0,0 +1,19 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +function runTests(testArray) { + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header", + corsURL, + "GET", + true, + [[headerName, headerValue]]); + }); +} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js new file mode 100644 index 00000000000000..15f7659abd2156 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + if (redirectPreflight) + urlParameters += "&redirect_preflight"; + var requestInit = {"mode": "cors", "redirect": "follow"}; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + }); + }, desc); +} + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +for (var code of [301, 302, 303, 307, 308]) { + /* preflight should not follow the redirection */ + corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true); + /* preflight is done before redirection: preflight force redirect to error */ + corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false); +} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js new file mode 100644 index 00000000000000..5df9fcf1429a7a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) { + var uuid_token = token(); + var url = corsUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy}; + + if (referrer) + requestInit.referrer = referrer; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct"); + assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct"); + assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value"); + }); + }); + }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default")); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +var origin = get_host_info().HTTP_ORIGIN + "/"; + +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, ""); +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", ""); + +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin); +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString()) +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString()); + +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString()); +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString()); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js new file mode 100644 index 00000000000000..718e351c1d3f09 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js @@ -0,0 +1,33 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + if (allowHeaders) + urlParameters += "," + allowHeaders; + if (allowMethods) + urlParameters += "&allow_methods="+ allowMethods; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + + return fetch(url + urlParameters).then(function(resp) { + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null); +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js new file mode 100644 index 00000000000000..d76e9a21fd4b94 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js @@ -0,0 +1,49 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py", + origin = location.origin // assuming an ASCII origin + +function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) { + return promise_test(t => { + let testURL = url + "?", + requestInit = {} + if (withCredentials) { + testURL += "origin=" + origin + "&" + testURL += "credentials&" + requestInit.credentials = "include" + } + if (useMethod) { + requestInit.method = useMethod + } + if (useHeader.length > 0) { + requestInit.headers = [useHeader] + } + testURL += "allow_methods=" + allowMethod + "&" + testURL += "allow_headers=" + allowHeader + "&" + + if (succeeds) { + return fetch(testURL, requestInit).then(resp => { + assert_equals(resp.headers.get("x-origin"), origin) + }) + } else { + return promise_rejects_js(t, TypeError, fetch(testURL, requestInit)) + } + }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")") +} + +// "GET" does not pass the case-sensitive method check, but in the safe list. +preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"]) +// Headers check is case-insensitive, and "*" works as any for method. +preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"]) +// "*" works as any only without credentials. +preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "", "PUT", []) +preflightTest(true, true, "PUT", "*", "PUT", []) +preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"]) +// Exact character match works even for "*" with credentials. +preflightTest(true, true, "*", "*", "*", ["*", "1"]) +// "PUT" does not pass the case-sensitive method check, and not in the safe list. +preflightTest(false, true, "put", "*", "PUT", []) diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js new file mode 100644 index 00000000000000..a4467a6087b0a3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* Check preflight is ok if status is ok status (200 to 299)*/ +function corsPreflightStatus(desc, corsUrl, preflightStatus) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + urlParameters += "&preflight_status=" + preflightStatus; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + if (200 <= preflightStatus && 299 >= preflightStatus) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +for (status of [200, 201, 202, 203, 204, 205, 206, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + 400, 401, 402, 403, 404, 405, + 501, 502, 503, 504, 505]) + corsPreflightStatus("Preflight answered with status " + status, corsUrl, status); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js new file mode 100644 index 00000000000000..7455b9774031c8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true); +corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false); +corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true); +corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true); +corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false); +corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true); +corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false); +corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true); +corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false); + +corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]); +corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]); + +var headers = [ + ["x-test-header1", "allowedOrRefused"], + ["x-test-header2", "allowedOrRefused"], + ["X-test-header3", "allowedOrRefused"], + ["x-test-header-b", "allowedOrRefused"], + ["x-test-header-D", "allowedOrRefused"], + ["x-test-header-C", "allowedOrRefused"], + ["x-test-header-a", "allowedOrRefused"], + ["Content-Type", "allowedOrRefused"], +]; +var safeHeaders= [ + ["Accept", "*"], + ["Accept-Language", "bzh"], + ["Content-Language", "eu"], +]; + +corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders); +corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders); + +corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js new file mode 100644 index 00000000000000..2aff3134063c35 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js @@ -0,0 +1,52 @@ +// META: timeout=long +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) { + var url = redirectUrl + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@"); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + promise_test(t => { + const result = fetch(url + urlParameters, requestInit) + if(locationCredentials === "") { + return result; + } else { + return promise_rejects_js(t, TypeError, result); + } + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; +var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, ""); + + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password"); +} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js new file mode 100644 index 00000000000000..50848170d0d415 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js @@ -0,0 +1,46 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) { + var urlBaseParameters = "&redirect_status=" + redirectStatus; + var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c"); + var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]}; + + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc + " (preflight after redirection success case)"); + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit)); + }); + }, desc + " (preflight after redirection failure case)"); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code); +} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js new file mode 100644 index 00000000000000..cdf4097d566924 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + return promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect"); + }); + }); + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null"); + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null"); +} diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html b/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html new file mode 100644 index 00000000000000..217baa3c46b631 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html @@ -0,0 +1,58 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html new file mode 100644 index 00000000000000..d69748ab261b90 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html @@ -0,0 +1,53 @@ + + + + + diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-worker.html new file mode 100644 index 00000000000000..13113e62621ac8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/data-url-worker.html @@ -0,0 +1,50 @@ + + + + + diff --git a/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js b/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js new file mode 100644 index 00000000000000..18b8f6dfa28a84 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js @@ -0,0 +1,58 @@ +function headerNames(headers) { + let names = []; + for (let header of headers) { + names.push(header[0].toLowerCase()); + } + return names; +} + +/* + Check preflight is done + Control if server allows method and headers and check accordingly + Check control access headers added by UA (for method and headers) +*/ +function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) { + return promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) { + var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&"); + var urlParameters = "token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method}; + var requestHeaders = []; + if (headers) + requestHeaders.push.apply(requestHeaders, headers); + if (safeHeaders) + requestHeaders.push.apply(requestHeaders, safeHeaders); + requestInit["headers"] = requestHeaders; + + if (allowed) { + urlParameters += "&allow_methods=" + method + "&control_request_headers"; + if (headers) { + //Make the server allow the headers + urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C"); + } + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + if (headers) { + var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(","); + for (var i in actualHeaders) + actualHeaders[i] = actualHeaders[i].trim(); + for (var header of headers) + assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header); + + let accessControlAllowHeaders = headerNames(headers).sort().join(","); + assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value"); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + } else { + assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted") + } + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){ + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + }); + } + }); + }, desc); +} diff --git a/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json b/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json new file mode 100644 index 00000000000000..945dc0f93ba4a3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json @@ -0,0 +1,13 @@ +[ + ["accept", "\""], + ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"], + ["accept-language", "\u0001"], + ["accept-language", "@"], + ["authorization", "basics"], + ["content-language", "\u0001"], + ["content-language", "@"], + ["content-type", "text/html"], + ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"], + ["range", "bytes 0-"], + ["test", "hi"] +] diff --git a/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html new file mode 100644 index 00000000000000..feb9f1f2e5bd3e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html @@ -0,0 +1,14 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js b/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js new file mode 100644 index 00000000000000..31ccc3869775fe --- /dev/null +++ b/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +function basicAuth(desc, user, pass, mode, status) { + promise_test(function(test) { + var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)}; + var requestInit = {"credentials": mode, "headers": headers}; + return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) { + assert_equals(resp.status, status, "HTTP status is " + status); + assert_equals(resp.type , "basic", "Response's type is basic"); + }); + }, desc); +} + +basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200); +basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200); +basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200); +basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401); diff --git a/test/fixtures/wpt/fetch/api/credentials/cookies.any.js b/test/fixtures/wpt/fetch/api/credentials/cookies.any.js new file mode 100644 index 00000000000000..de30e477655c28 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/credentials/cookies.any.js @@ -0,0 +1,49 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function cookies(desc, credentials1, credentials2 ,cookies) { + var url = RESOURCES_DIR + "top.txt" + var urlParameters = ""; + var urlCleanParameters = ""; + if (cookies) { + urlParameters +="?pipe=header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters +="?pipe=header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentials1} + promise_test(function(test){ + var requestInit = {"credentials": credentials1} + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + //check cookies sent + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2}); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentials1 != "omit" && credentials2 != "omit") { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)"); + } + //clean cookies + return fetch(url + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() { + return Promise.reject(e); + }); + }); + }, desc); +} + +cookies("Include mode: 1 cookie", "include", "include", ["a=1"]); +cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]); +cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]); +cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]); +cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]); +cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]); +cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]); diff --git a/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js new file mode 100644 index 00000000000000..814789c5b0b578 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js @@ -0,0 +1,70 @@ +// META: title=Header value normalizing test +// META: global=window,worker +// META: timeout=long + +for(let i = 0; i < 0x21; i++) { + let fail = false, + strip = false + + // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed + if(i === 0x0B || i === 0x0C) + continue + + if(i === 0) { + fail = true + } + + if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) { + strip = true + } + + let url = "../resources/inspect-headers.py?headers=val1|val2|val3", + val = String.fromCharCode(i), + expectedVal = strip ? "" : val, + val1 = val, + expectedVal1 = expectedVal, + val2 = "x" + val, + expectedVal2 = "x" + expectedVal, + val3 = val + "x", + expectedVal3 = expectedVal + "x" + + // XMLHttpRequest is not available in service workers + if (!self.GLOBAL.isWorker()) { + async_test((t) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", url) + if(fail) { + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1)) + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2)) + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3)) + t.done() + } else { + xhr.setRequestHeader("val1", val1) + xhr.setRequestHeader("val2", val2) + xhr.setRequestHeader("val3", val3) + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1) + assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2) + assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3) + }) + xhr.send() + } + }, "XMLHttpRequest with value " + encodeURI(val)) + } + + promise_test((t) => { + if(fail) { + return Promise.all([ + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })), + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })), + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} })) + ]) + } else { + return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => { + assert_equals(res.headers.get("x-request-val1"), expectedVal1) + assert_equals(res.headers.get("x-request-val2"), expectedVal2) + assert_equals(res.headers.get("x-request-val3"), expectedVal3) + }) + } + }, "fetch() with value " + encodeURI(val)) +} diff --git a/test/fixtures/wpt/fetch/api/headers/header-values.any.js b/test/fixtures/wpt/fetch/api/headers/header-values.any.js new file mode 100644 index 00000000000000..9a829d8d4ed621 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/header-values.any.js @@ -0,0 +1,61 @@ +// META: title=Header value test +// META: global=window,worker +// META: timeout=long + +// Invalid values +[0, 0x0A, 0x0D].forEach(val => { + val = "x" + String.fromCharCode(val) + "x" + + // XMLHttpRequest is not available in service workers + if (!self.GLOBAL.isWorker()) { + test(() => { + let xhr = new XMLHttpRequest() + xhr.open("POST", "/") + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val)) + }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw") + } + + promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw") +}) + +// Valid values +let headerValues =[] +for(let i = 0; i < 0x100; i++) { + if(i === 0 || i === 0x0A || i === 0x0D) { + continue + } + headerValues.push("x" + String.fromCharCode(i) + "x") +} +var url = "../resources/inspect-headers.py?headers=" +headerValues.forEach((_, i) => { + url += "val" + i + "|" +}) + +// XMLHttpRequest is not available in service workers +if (!self.GLOBAL.isWorker()) { + async_test((t) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", url) + headerValues.forEach((val, i) => { + xhr.setRequestHeader("val" + i, val) + }) + xhr.onload = t.step_func_done(() => { + headerValues.forEach((val, i) => { + assert_equals(xhr.getResponseHeader("x-request-val" + i), val) + }) + }) + xhr.send() + }, "XMLHttpRequest with all valid values") +} + +promise_test((t) => { + const headers = new Headers + headerValues.forEach((val, i) => { + headers.append("val" + i, val) + }) + return fetch(url, { headers }).then((res) => { + headerValues.forEach((val, i) => { + assert_equals(res.headers.get("x-request-val" + i), val) + }) + }) +}, "fetch() with all valid values") diff --git a/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js b/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js new file mode 100644 index 00000000000000..5de71f43bb4fc3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js @@ -0,0 +1,218 @@ +// META: title=Headers structure +// META: global=window,worker + +test(function() { + new Headers(); +}, "Create headers from no parameter"); + +test(function() { + new Headers(undefined); +}, "Create headers from undefined parameter"); + +test(function() { + new Headers({}); +}, "Create headers from empty object"); + +var parameters = [null, 1]; +parameters.forEach(function(parameter) { + test(function() { + assert_throws_js(TypeError, function() { new Headers(parameter) }); + }, "Create headers with " + parameter + " should throw"); +}); + +var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3", + "name4": null, + "name5": undefined, + "name6": 1, + "Content-Type": "value4" +}; + +var headerSeq = []; +for (var name in headerDict) + headerSeq.push([name, headerDict[name]]); + +test(function() { + var headers = new Headers(headerSeq); + for (name in headerDict) { + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } + assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary"); +}, "Create headers with sequence"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) { + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Create headers with record"); + +test(function() { + var headers = new Headers(headerDict); + var headers2 = new Headers(headers); + for (name in headerDict) { + assert_equals(headers2.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Create headers with existing headers"); + +test(function() { + var headers = new Headers() + headers[Symbol.iterator] = function *() { + yield ["test", "test"] + } + var headers2 = new Headers(headers) + assert_equals(headers2.get("test"), "test") +}, "Create headers with existing headers with custom iterator"); + +test(function() { + var headers = new Headers(); + for (name in headerDict) { + headers.append(name, headerDict[name]); + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Check append method"); + +test(function() { + var headers = new Headers(); + for (name in headerDict) { + headers.set(name, headerDict[name]); + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Check set method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) + assert_true(headers.has(name),"headers has name " + name); + + assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders"); +}, "Check has method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) { + assert_true(headers.has(name),"headers have a header: " + name); + headers.delete(name) + assert_true(!headers.has(name),"headers do not have anymore a header: " + name); + } +}, "Check delete method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + + assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value"); +}, "Check get method"); + +var headerEntriesDict = {"name1": "value1", + "Name2": "value2", + "name": "value3", + "content-Type": "value4", + "Content-Typ": "value5", + "Content-Types": "value6" +}; +var sortedHeaderDict = {}; +var headerValues = []; +var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) { + sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value]; + headerValues.push(headerEntriesDict[value]); + return value.toLowerCase(); +}).sort(); + +var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())); +function checkIteratorProperties(iterator) { + var prototype = Object.getPrototypeOf(iterator); + assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype); + + var descriptor = Object.getOwnPropertyDescriptor(prototype, "next"); + assert_true(descriptor.configurable, "configurable"); + assert_true(descriptor.enumerable, "enumerable"); + assert_true(descriptor.writable, "writable"); +} + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.keys(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value, key); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (key of headers.keys()) + assert_true(sortedHeaderKeys.indexOf(key) != -1); +}, "Check keys method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.values(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value, sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (value of headers.values()) + assert_true(headerValues.indexOf(value) != -1); +}, "Check values method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.entries(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value[0], key); + assert_equals(entry.value[1], sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (entry of headers.entries()) + assert_equals(entry[1], sortedHeaderDict[entry[0]]); +}, "Check entries method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers[Symbol.iterator](); + + sortedHeaderKeys.forEach(function(key) { + entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value[0], key); + assert_equals(entry.value[1], sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); +}, "Check Symbol.iterator method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var reference = sortedHeaderKeys[Symbol.iterator](); + headers.forEach(function(value, key, container) { + assert_equals(headers, container); + entry = reference.next(); + assert_false(entry.done); + assert_equals(key, entry.value); + assert_equals(value, sortedHeaderDict[entry.value]); + }); + assert_true(reference.next().done); +}, "Check forEach method"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js b/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js new file mode 100644 index 00000000000000..57bec0164ed9ab --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js @@ -0,0 +1,52 @@ +// META: title=Headers case management +// META: global=window,worker + +var headerDictCase = {"UPPERCASE": "value1", + "lowercase": "value2", + "mixedCase": "value3", + "Content-TYPE": "value4" + }; + +function checkHeadersCase(originalName, headersToCheck, expectedDict) { + var lowCaseName = originalName.toLowerCase(); + var upCaseName = originalName.toUpperCase(); + var expectedValue = expectedDict[originalName]; + assert_equals(headersToCheck.get(originalName), expectedValue, + "name: " + originalName + " has value: " + expectedValue); + assert_equals(headersToCheck.get(lowCaseName), expectedValue, + "name: " + lowCaseName + " has value: " + expectedValue); + assert_equals(headersToCheck.get(upCaseName), expectedValue, + "name: " + upCaseName + " has value: " + expectedValue); +} + +test(function() { + var headers = new Headers(headerDictCase); + for (name in headerDictCase) + checkHeadersCase(name, headers, headerDictCase) +}, "Create headers, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (name in headerDictCase) { + headers.append(name, headerDictCase[name]); + checkHeadersCase(name, headers, headerDictCase); + } +}, "Check append method, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (name in headerDictCase) { + headers.set(name, headerDictCase[name]); + checkHeadersCase(name, headers, headerDictCase); + } +}, "Check set method, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (name in headerDictCase) + headers.set(name, headerDictCase[name]); + for (name in headerDictCase) + headers.delete(name.toLowerCase()); + for (name in headerDictCase) + assert_false(headers.has(name), "header " + name + " should have been deleted"); +}, "Check delete method, names use characters with different case"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js b/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js new file mode 100644 index 00000000000000..dab47889c45ef3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js @@ -0,0 +1,64 @@ +// META: title=Headers have combined (and sorted) values +// META: global=window,worker + +var headerSeqCombine = [["single", "singleValue"], + ["double", "doubleValue1"], + ["double", "doubleValue2"], + ["triple", "tripleValue1"], + ["triple", "tripleValue2"], + ["triple", "tripleValue3"] +]; +var expectedDict = {"single": "singleValue", + "double": "doubleValue1, doubleValue2", + "triple": "tripleValue1, tripleValue2, tripleValue3" +}; + +test(function() { + var headers = new Headers(headerSeqCombine); + for (name in expectedDict) + assert_equals(headers.get(name), expectedDict[name]); +}, "Create headers using same name for different values"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (name in expectedDict) { + assert_true(headers.has(name), "name: " + name + " has value(s)"); + headers.delete(name); + assert_false(headers.has(name), "name: " + name + " has no value(s) anymore"); + } +}, "Check delete and has methods when using same name for different values"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (name in expectedDict) { + headers.set(name,"newSingleValue"); + assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue"); + } +}, "Check set methods when called with already used name"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (name in expectedDict) { + var value = headers.get(name); + headers.append(name,"newSingleValue"); + assert_equals(headers.get(name), (value + ", " + "newSingleValue")); + } +}, "Check append methods when called with already used name"); + +test(() => { + const headers = new Headers([["1", "a"],["1", "b"]]); + for(let header of headers) { + assert_array_equals(header, ["1", "a, b"]); + } +}, "Iterate combined values"); + +test(() => { + const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]), + expected = [["1", "b"], ["2", "a, b"]]; + let i = 0; + for(let header of headers) { + assert_array_equals(header, expected[i]); + i++; + } + assert_equals(i, 2); +}, "Iterate combined values in sorted order") diff --git a/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js new file mode 100644 index 00000000000000..ab8a118e255f3a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js @@ -0,0 +1,94 @@ +// META: title=Headers errors +// META: global=window,worker + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["name"]]); }); +}, "Create headers giving an array having one string as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); }); +}, "Create headers giving an array having three strings as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); }); +}, "Create headers giving bad header name as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); }); +}, "Create headers giving bad header value as init argument"); + +var badNames = ["invalidĀ", {}]; +var badValues = ["invalidĀ"]; + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.get(name); }); + }, "Check headers get with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.delete(name); }); + }, "Check headers delete with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.has(name); }); + }, "Check headers has with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.set(name, "Value1"); }); + }, "Check headers set with an invalid name " + name); +}); + +badValues.forEach(function(value) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.set("name", value); }); + }, "Check headers set with an invalid value " + value); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); }); + }, "Check headers append with an invalid name " + name); +}); + +badValues.forEach(function(value) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.append("name", value); }); + }, "Check headers append with an invalid value " + value); +}); + +test(function() { + var headers = new Headers([["name", "value"]]); + assert_throws_js(TypeError, function() { headers.forEach(); }); + assert_throws_js(TypeError, function() { headers.forEach(undefined); }); + assert_throws_js(TypeError, function() { headers.forEach(1); }); +}, "Headers forEach throws if argument is not callable"); + +test(function() { + var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]); + var counter = 0; + try { + headers.forEach(function(value, name) { + counter++; + if (name == "name2") + throw "error"; + }); + } catch (e) { + assert_equals(counter, 2); + assert_equals(e, "error"); + return; + } + assert_unreached(); +}, "Headers forEach loop should stop if callback is throwing exception"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js new file mode 100644 index 00000000000000..c09658e641ecdf --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker + +promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +const longValue = "s".repeat(127); + +[ + { + "headers": ["accept", "accept-language", "content-language"], + "values": [longValue, "", longValue] + }, + { + "headers": ["accept", "accept-language", "content-language"], + "values": ["", longValue] + }, + { + "headers": ["content-type"], + "values": ["text/plain;" + "s".repeat(116), "text/plain"] + } +].forEach(testItem => { + testItem.headers.forEach(header => { + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + testItem.values.forEach((value) => { + noCorsHeaders.append(header, value); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '1'); + }); + noCorsHeaders.set(header, testItem.values.join(", ")); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '2'); + noCorsHeaders.delete(header); + assert_false(noCorsHeaders.has(header)); + }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", ")); + }); +}); + +function runTests(testArray) { + testArray = testArray.concat([ + ["dpr", "2"], + ["downlink", "1"], + ["save-data", "on"], + ["viewport-width", "100"], + ["width", "100"], + ["unknown", "doesitmatter"] + ]); + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + noCorsHeaders.append(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + noCorsHeaders.set(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header"); + }); +} diff --git a/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js new file mode 100644 index 00000000000000..5ebd7ae9ea9898 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js @@ -0,0 +1,35 @@ +// META: title=Headers normalize values +// META: global=window,worker + +var headerDictWS = {"name1": " space ", + "name2": "\ttab\t", + "name3": " spaceAndTab\t", + "name4": "\r\n newLine", //obs-fold cases + "name5": "newLine\r\n ", + "name6": "\r\n\tnewLine", + }; + +test(function() { + var headers = new Headers(headerDictWS); + for (name in headerDictWS) + assert_equals(headers.get(name), headerDictWS[name].trim(), + "name: " + name + " has normalized value: " + headerDictWS[name].trim()); +}, "Create headers with not normalized values"); + +test(function() { + var headers = new Headers(); + for (name in headerDictWS) { + headers.append(name, headerDictWS[name]); + assert_equals(headers.get(name), headerDictWS[name].trim(), + "name: " + name + " has value: " + headerDictWS[name].trim()); + } +}, "Check append method with not normalized values"); + +test(function() { + var headers = new Headers(); + for (name in headerDictWS) { + headers.set(name, headerDictWS[name]); + assert_equals(headers.get(name), headerDictWS[name].trim(), + "name: " + name + " has value: " + headerDictWS[name].trim()); + } +}, "Check set method with not normalized values"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-record.any.js b/test/fixtures/wpt/fetch/api/headers/headers-record.any.js new file mode 100644 index 00000000000000..55036c2da4c076 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-record.any.js @@ -0,0 +1,355 @@ +// META: global=window,worker + +var log = []; +function clearLog() { + log = []; +} +function addLogEntry(name, args) { + log.push([ name, ...args ]); +} + +var loggingHandler = { +}; + +setup(function() { + for (let prop of Object.getOwnPropertyNames(Reflect)) { + loggingHandler[prop] = function(...args) { + addLogEntry(prop, args); + return Reflect[prop](...args); + } + } +}); + +test(function() { + var h = new Headers(); + assert_equals([...h].length, 0); +}, "Passing nothing to Headers constructor"); + +test(function() { + var h = new Headers(undefined); + assert_equals([...h].length, 0); +}, "Passing undefined to Headers constructor"); + +test(function() { + assert_throws_js(TypeError, function() { + var h = new Headers(null); + }); +}, "Passing null to Headers constructor"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b" }; + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["a"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); +}, "Basic operation with one property"); + +test(function() { + this.add_cleanup(clearLog); + var recordProto = { c: "d" }; + var record = Object.create(recordProto, { a: { value: "b", enumerable: true } }); + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["a"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); +}, "Basic operation with one property and a proto"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b", c: "d" }; + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[5], ["get", record, "c", proxy]); + + // Check the results. + assert_equals([...h].length, 2); + assert_array_equals([...h.keys()], ["a", "c"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with two properties"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b", "\uFFFF": "d" }; + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, function() { + var h = new Headers(proxy); + }); + + assert_equals(log.length, 5); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]); + // The second [[Get]] never happens, because we convert the invalid name to a + // ByteString first and throw. +}, "Correct operation ordering with two properties one of which has an invalid name"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "\uFFFF", c: "d" } + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, function() { + var h = new Headers(proxy); + }); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Nothing else after this, because converting the result of that [[Get]] to a + // ByteString throws. +}, "Correct operation ordering with two properties one of which has an invalid value"); + +test(function() { + this.add_cleanup(clearLog); + var record = {}; + Object.defineProperty(record, "a", { value: "b", enumerable: false }); + Object.defineProperty(record, "c", { value: "d", enumerable: true }); + Object.defineProperty(record, "e", { value: "f", enumerable: false }); + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // No [[Get]] because not enumerable + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[4], ["get", record, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]); + // No [[Get]] because not enumerable + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["c"]); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with non-enumerable properties"); + +test(function() { + this.add_cleanup(clearLog); + var record = {a: "b", c: "d", e: "f"}; + var lyingHandler = { + getOwnPropertyDescriptor: function(target, name) { + if (name == "a" || name == "e") { + return undefined; + } + return Reflect.getOwnPropertyDescriptor(target, name); + } + }; + var lyingProxy = new Proxy(record, lyingHandler); + var proxy = new Proxy(lyingProxy, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", lyingProxy]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]); + // No [[Get]] because no descriptor + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]); + // No [[Get]] because no descriptor + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["c"]); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with undefined descriptors"); + +test(function() { + this.add_cleanup(clearLog); + var record = {a: "b", c: "d"}; + var lyingHandler = { + ownKeys: function() { + return [ "a", "c", "a", "c" ]; + }, + }; + var lyingProxy = new Proxy(record, lyingHandler); + var proxy = new Proxy(lyingProxy, loggingHandler); + + // Returning duplicate keys from ownKeys() throws a TypeError. + assert_throws_js(TypeError, + function() { var h = new Headers(proxy); }); + + assert_equals(log.length, 2); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", lyingProxy]); +}, "Correct operation ordering with repeated keys"); + +test(function() { + this.add_cleanup(clearLog); + var record = { + a: "b", + [Symbol.toStringTag]: { + // Make sure the ToString conversion of the value happens + // after the ToString conversion of the key. + toString: function () { addLogEntry("toString", [this]); return "nope"; } + }, + c: "d" }; + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, + function() { var h = new Headers(proxy); }); + + assert_equals(log.length, 7); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[5], ["get", record, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[6], ["getOwnPropertyDescriptor", record, + Symbol.toStringTag]); + // Then we throw an exception converting the Symbol to a string, before we do + // the third [[Get]]. +}, "Basic operation with Symbol keys"); + +test(function() { + this.add_cleanup(clearLog); + var record = { + a: { + toString: function() { addLogEntry("toString", [this]); return "b"; } + }, + [Symbol.toStringTag]: { + toString: function () { addLogEntry("toString", [this]); return "nope"; } + }, + c: { + toString: function() { addLogEntry("toString", [this]); return "d"; } + } + }; + // Now make that Symbol-named property not enumerable. + Object.defineProperty(record, Symbol.toStringTag, { enumerable: false }); + assert_array_equals(Reflect.ownKeys(record), + ["a", "c", Symbol.toStringTag]); + + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 9); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://heycam.github.io/webidl/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the ToString on the value. + assert_array_equals(log[4], ["toString", record.a]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[6], ["get", record, "c", proxy]); + // Then the ToString on the value. + assert_array_equals(log[7], ["toString", record.c]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[8], ["getOwnPropertyDescriptor", record, + Symbol.toStringTag]); + // No [[Get]] because not enumerable. + + // Check the results. + assert_equals([...h].length, 2); + assert_array_equals([...h.keys()], ["a", "c"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Operation with non-enumerable Symbol keys"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js b/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js new file mode 100644 index 00000000000000..c47290eb2098b3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js @@ -0,0 +1,18 @@ +// META: title=Headers basic +// META: global=window,worker + +var headers = new Headers(); +var methods = ["append", + "delete", + "get", + "has", + "set", + //Headers is iterable + "entries", + "keys", + "values" + ]; +for (var idx in methods) + test(function() { + assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method"); + }, "Headers has " + methods[idx] + " method"); diff --git a/test/fixtures/wpt/fetch/api/idlharness.any.js b/test/fixtures/wpt/fetch/api/idlharness.any.js new file mode 100644 index 00000000000000..7b3c694e16ac3e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/idlharness.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +idl_test( + ['fetch'], + ['referrer-policy', 'html', 'dom'], + idl_array => { + idl_array.add_objects({ + Headers: ["new Headers()"], + Request: ["new Request('about:blank')"], + Response: ["new Response()"], + }); + if (self.GLOBAL.isWindow()) { + idl_array.add_objects({ Window: ['window'] }); + } else if (self.GLOBAL.isWorker()) { + idl_array.add_objects({ WorkerGlobalScope: ['self'] }); + } + } +); diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html new file mode 100644 index 00000000000000..e8660dffa9496d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html @@ -0,0 +1,16 @@ + + + + + Fetch in worker: blocked by CSP + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html new file mode 100644 index 00000000000000..99e90dfcd8fdd7 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html @@ -0,0 +1,15 @@ + + + + + Fetch: blocked by CSP + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers new file mode 100644 index 00000000000000..c8c1e9ffbd9b1c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js new file mode 100644 index 00000000000000..28653fff85cf1d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js @@ -0,0 +1,13 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +//Content-Security-Policy: connect-src 'none'; cf .headers file +cspViolationUrl = RESOURCES_DIR + "top.txt"; + +promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(cspViolationUrl)); +}, "Fetch is blocked by CSP, got a TypeError"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers new file mode 100644 index 00000000000000..c8c1e9ffbd9b1c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js b/test/fixtures/wpt/fetch/api/policies/nested-policy.js new file mode 100644 index 00000000000000..b0d17696c3379c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/nested-policy.js @@ -0,0 +1 @@ +// empty, but referrer-policy set on this file diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers b/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers new file mode 100644 index 00000000000000..7ffbf17d6be5a5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html new file mode 100644 index 00000000000000..af898aa29f5f6e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in service worker: referrer with no-referrer policy + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html new file mode 100644 index 00000000000000..dbef9bb658fa67 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with no-referrer policy + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html new file mode 100644 index 00000000000000..22a6f34c525bad --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html @@ -0,0 +1,15 @@ + + + + + Fetch: referrer with no-referrer policy + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers new file mode 100644 index 00000000000000..7ffbf17d6be5a5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js new file mode 100644 index 00000000000000..60600bf081c71b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js @@ -0,0 +1,19 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + var referrer = resp.headers.get("x-request-referer"); + //Either no referrer header is sent or it is empty + if (referrer) + assert_equals(referrer, "", "request's referrer is empty"); + }); +}, "Request's referrer is empty"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers new file mode 100644 index 00000000000000..7ffbf17d6be5a5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html new file mode 100644 index 00000000000000..4018b837816e66 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in service worker: referrer with no-referrer policy + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html new file mode 100644 index 00000000000000..d87192e227119a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html @@ -0,0 +1,17 @@ + + + + + Fetch in service worker: referrer with origin-when-cross-origin policy + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html new file mode 100644 index 00000000000000..f95ae8cf081d13 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html @@ -0,0 +1,16 @@ + + + + + Fetch in worker: referrer with origin-when-cross-origin policy + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html new file mode 100644 index 00000000000000..5cd79e4b536159 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with origin-when-cross-origin policy + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers new file mode 100644 index 00000000000000..ad768e63294149 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js new file mode 100644 index 00000000000000..0adadbc55081f0 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + importScripts("/common/get-host-info.sub.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = location.origin + '/'; +var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers new file mode 100644 index 00000000000000..ad768e63294149 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html new file mode 100644 index 00000000000000..bb80dd54fbf450 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with origin policy + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html new file mode 100644 index 00000000000000..b164afe01de9bb --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with origin policy + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers new file mode 100644 index 00000000000000..5b29739bbdde3a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js new file mode 100644 index 00000000000000..918f8f207c3914 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js @@ -0,0 +1,30 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = (new URL("/", location.href)).href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +promise_test(function(test) { + var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/"; + return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Cross-origin referrer is overridden by client origin"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers new file mode 100644 index 00000000000000..5b29739bbdde3a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html new file mode 100644 index 00000000000000..634877edae8764 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in worker: referrer with unsafe-url policy + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html new file mode 100644 index 00000000000000..42045776b12027 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with unsafe-url policy + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html new file mode 100644 index 00000000000000..10dd79e3d358b1 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with unsafe-url policy + + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers new file mode 100644 index 00000000000000..8e23770bd60404 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js new file mode 100644 index 00000000000000..4d61172613ee58 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerUrl = location.href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl); + }); +}, "Request's referrer is the full url of current document/worker"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers new file mode 100644 index 00000000000000..8e23770bd60404 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js new file mode 100644 index 00000000000000..74d731f24251c1 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js @@ -0,0 +1,38 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const BASE = location.href; +const IS_HTTPS = new URL(BASE).protocol === 'https:'; +const REMOTE_HOST = get_host_info()['REMOTE_HOST']; +const REMOTE_PORT = + IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT']; + +const REMOTE_ORIGIN = + new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin; +const DESTINATION = new URL('../resources/cors-top.txt', BASE); + +function CreateURL(url, BASE, params) { + const u = new URL(url, BASE); + for (const {name, value} of params) { + u.searchParams.append(name, value); + } + return u; +} + +const redirect = + CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN, + [{name: 'redirect_status', value: 303}, + {name: 'location', value: DESTINATION.href}]); + +promise_test(async (test) => { + const res = await fetch(redirect.href, {mode: 'no-cors'}); + // This is discussed at https://github.com/whatwg/fetch/issues/737. + assert_equals(res.type, 'opaque'); +}, 'original => remote => original with mode: "no-cors"'); + +promise_test(async (test) => { + const res = await fetch(redirect.href, {mode: 'cors'}); + assert_equals(res.type, 'cors'); +}, 'original => remote => original with mode: "cors"'); + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js new file mode 100644 index 00000000000000..dda5d7f52901ae --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js @@ -0,0 +1,42 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: timeout=long + +function redirectCount(desc, redirectUrl, redirectLocation, redirectStatus, maxCount, shouldPass) { + var uuid_token = token(); + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&max_count=" + maxCount; + if (redirectLocation) + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var url = redirectUrl; + var requestInit = {"redirect": "follow"}; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + + if (!shouldPass) + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + return resp.text(); + }).then(function(body) { + assert_equals(body, maxCount.toString(), "Redirected " + maxCount + " times"); + }); + }); + }, desc); +} + +var redirUrl = RESOURCES_DIR + "redirect.py"; + +for (var statusCode of [301, 302, 303, 307, 308]) { + redirectCount("Redirect " + statusCode + " 20 times", redirUrl, redirUrl, statusCode, 20, true); + redirectCount("Redirect " + statusCode + " 21 times", redirUrl, redirUrl, statusCode, 21, false); +} + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js new file mode 100644 index 00000000000000..487f4d42e9239f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// Tests receiving a redirect response with a Location header with an empty +// value. + +const url = RESOURCES_DIR + 'redirect-empty-location.py'; + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'})); +}, 'redirect response with empty Location, follow mode'); + +promise_test(t => { + return fetch(url, {redirect:'manual'}) + .then(resp => { + assert_equals(resp.type, 'opaqueredirect'); + assert_equals(resp.status, 0); + }); +}, 'redirect response with empty Location, manual mode'); + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js new file mode 100644 index 00000000000000..779ad7057937f6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by +// this test. As of writing, the Fetch spec has not been updated to cover these. + +// redirectLocation tests that a Location header of |locationHeader| is resolved +// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted +// as a byte sequence via isomorphic encode, as described in [INFRA]. This +// allows the caller to specify byte sequences which are not valid UTF-8. +// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its +// UTF-8 encoding, not "\u2603". +// +// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode +function redirectLocation( + desc, redirectUrl, locationHeader, expectedUrlSuffix) { + promise_test(function(test) { + // Note we use escape() instead of encodeURIComponent(), so that characters + // are escaped as bytes in the isomorphic encoding. + var url = redirectUrl + '?simple=1&location=' + escape(locationHeader); + + return fetch(url, {'redirect': 'follow'}).then(function(resp) { + assert_true( + resp.url.endsWith(expectedUrlSuffix), + resp.url + ' ends with ' + expectedUrlSuffix); + }); + }, desc); +} + +var redirUrl = RESOURCES_DIR + 'redirect.py'; +redirectLocation( + 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83', + 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83', + 'top.txt?%E2%98%83'); +redirectLocation( + 'Redirect to escaped and unescaped UTF-8', redirUrl, + 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83', + 'top.txt?%%E2%98%83'); +redirectLocation( + 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF'); + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js new file mode 100644 index 00000000000000..5cb6cc280c4cd5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js @@ -0,0 +1,48 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function redirectLocation(desc, redirectUrl, redirectLocation, redirectStatus, redirectMode, shouldPass) { + var url = redirectUrl; + var urlParameters = "?redirect_status=" + redirectStatus; + if (redirectLocation) + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"redirect": redirectMode}; + + promise_test(function(test) { + if (redirectMode === "error" || !shouldPass) + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + if (redirectMode === "manual") + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 0, "Response's status is 0"); + assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); + assert_equals(resp.statusText, "", "Response's statusText is \"\""); + assert_true(resp.headers.entries().next().done, "Headers should be empty"); + }); + + if (redirectMode === "follow") + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, redirectStatus, "Response's status is " + redirectStatus); + }); + assert_unreached(redirectMode + " is not a valid redirect mode"); + }, desc); +} + +var redirUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = "top.txt"; +var invalidLocationUrl = "invalidurl:"; +var dataLocationUrl = "data:,data%20url"; +// FIXME: We may want to mix redirect-mode and cors-mode. +// FIXME: Add tests for "error" redirect-mode. +for (var statusCode of [301, 302, 303, 307, 308]) { + redirectLocation("Redirect " + statusCode + " in \"follow\" mode without location", redirUrl, undefined, statusCode, "follow", true); + redirectLocation("Redirect " + statusCode + " in \"manual\" mode without location", redirUrl, undefined, statusCode, "manual", true); + + redirectLocation("Redirect " + statusCode + " in \"follow\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "follow", false); + redirectLocation("Redirect " + statusCode + " in \"manual\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "manual", true); + + redirectLocation("Redirect " + statusCode + " in \"follow\" mode with data location", redirUrl, dataLocationUrl, statusCode, "follow", false); + redirectLocation("Redirect " + statusCode + " in \"manual\" mode with data location", redirUrl, dataLocationUrl, statusCode, "manual", true); +} + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js new file mode 100644 index 00000000000000..9fe086a9db718a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js @@ -0,0 +1,112 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// Creates a promise_test that fetches a URL that returns a redirect response. +// +// |opts| has additional options: +// |opts.body|: the request body as a string or blob (default is empty body) +// |opts.expectedBodyAsString|: the expected response body as a string. The +// server is expected to echo the request body. The default is the empty string +// if the request after redirection isn't POST; otherwise it's |opts.body|. +// |opts.expectedRequestContentType|: the expected Content-Type of redirected +// request. +function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) { + let url = redirectUrl; + let urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + let requestHeaders = { + "Content-Encoding": "Identity", + "Content-Language": "en-US", + "Content-Location": "foo", + }; + let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders}; + opts = opts || {}; + if (opts.body) { + requestInit.body = opts.body; + } + + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + let expectedRequestContentType = "NO"; + if (opts.expectedRequestContentType) { + expectedRequestContentType = opts.expectedRequestContentType; + } + + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.type, "basic", "Response's type basic"); + assert_equals( + resp.headers.get("x-request-method"), + expectedMethod, + "Request method after redirection is " + expectedMethod); + let hasRequestBodyHeader = true; + if (opts.expectedStripRequestBodyHeader) { + hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader; + } + assert_equals( + resp.headers.get("x-request-content-type"), + expectedRequestContentType, + "Request Content-Type after redirection is " + expectedRequestContentType); + [ + "Content-Encoding", + "Content-Language", + "Content-Location" + ].forEach(header => { + let xHeader = "x-request-" + header.toLowerCase(); + let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO"; + assert_equals( + resp.headers.get(xHeader), + expectedValue, + "Request " + header + " after redirection is " + expectedValue); + }); + assert_true(resp.redirected); + return resp.text().then(function(text) { + let expectedBody = ""; + if (expectedMethod == "POST") { + expectedBody = opts.expectedBodyAsString || requestInit.body; + } + let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO"; + assert_equals(text, expectedBody, "request body"); + assert_equals( + resp.headers.get("x-request-content-length"), + expectedContentLength, + "Request Content-Length after redirection is " + expectedContentLength); + }); + }); + }, desc); +} + +promise_test(function(test) { + assert_false(new Response().redirected); + return fetch(RESOURCES_DIR + "method.py").then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_false(resp.redirected); + }); +}, "Response.redirected should be false on not-redirected responses"); + +var redirUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = "method.py"; + +const stringBody = "this is my body"; +const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]); +const blobBodyAsString = "it's me the blob! and more blob!"; + +redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET"); +redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD"); + +redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET"); +redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD"); + +redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET"); +redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD"); +redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true }); + +redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET"); +redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"}); +redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString }); +redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js new file mode 100644 index 00000000000000..eed44e0414cb89 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js @@ -0,0 +1,50 @@ +// META: script=/common/get-host-info.sub.js + +var redirectLocation = "cors-top.txt"; + +function testRedirect(origin, redirectStatus, redirectMode, corsMode) { + var url = new URL("../resources/redirect.py", self.location); + if (origin === "cross-origin") { + url.host = get_host_info().REMOTE_HOST; + url.port = get_host_info().HTTP_PORT; + } + + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {redirect: redirectMode, mode: corsMode}; + + promise_test(function(test) { + if (redirectMode === "error" || + (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin")) + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + if (redirectMode === "manual") + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 0, "Response's status is 0"); + assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); + assert_equals(resp.statusText, "", "Response's statusText is \"\""); + assert_equals(resp.url, url + urlParameters, "Response URL should be the original one"); + }); + if (redirectMode === "follow") + return fetch(url + urlParameters, requestInit).then(function(resp) { + if (corsMode !== "no-cors" || origin === "same-origin") { + assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one"); + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.type, "opaque", "Response is opaque"); + } + }); + assert_unreached(redirectMode + " is no a valid redirect mode"); + }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode"); +} + +for (var origin of ["same-origin", "cross-origin"]) { + for (var statusCode of [301, 302, 303, 307, 308]) { + for (var redirect of ["error", "manual", "follow"]) { + for (var mode of ["cors", "no-cors"]) + testRedirect(origin, statusCode, redirect, mode); + } + } +} + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js new file mode 100644 index 00000000000000..b81b91601a8e34 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function testOriginAfterRedirection(desc, method, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return fetch(url + urlParameters, requestInit).then(function(response) { + assert_equals(response.status, 200, "Inspect header response's status is 200"); + assert_equals(response.headers.get("x-request-origin"), expectedOrigin, "Check origin header"); + }); + }); + }, desc); +} + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=origin"; +var corsLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=origin"; + +for (var code of [301, 302, 303, 307, 308]) { + testOriginAfterRedirection("Same origin to same origin redirection " + code, 'GET', redirectUrl, locationUrl, code, null); + testOriginAfterRedirection("Same origin to other origin redirection " + code, 'GET', redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); + testOriginAfterRedirection("Other origin to other origin redirection " + code, 'GET', corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); + testOriginAfterRedirection("Other origin to same origin redirection " + code, 'GET', corsRedirectUrl, locationUrl + "&cors", code, "null"); + + testOriginAfterRedirection("Same origin to same origin redirection[POST] " + code, 'POST', redirectUrl, locationUrl, code, null); + testOriginAfterRedirection("Same origin to other origin redirection[POST] " + code, 'POST', redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); + testOriginAfterRedirection("Other origin to other origin redirection[POST] " + code, 'POST', corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); + testOriginAfterRedirection("Other origin to same origin redirection[POST] " + code, 'POST', corsRedirectUrl, locationUrl + "&cors", code, "null"); +} + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js new file mode 100644 index 00000000000000..56e55d79e141fd --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js @@ -0,0 +1,104 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function getExpectation(expectations, init, initScenario, redirectPolicy, redirectScenario) { + let policies = [ + expectations[initPolicy][initScenario], + expectations[redirectPolicy][redirectScenario] + ]; + + if (policies.includes("omitted")) { + return null; + } else if (policies.includes("origin")) { + return referrerOrigin; + } else { + // "stripped-referrer" + return referrerUrl; + } +} + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header "; + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(response) { + assert_equals(response.status, 200, "Inspect header response's status is 200"); + assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, description); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +var expectations = { + "no-referrer": { + "same-origin": "omitted", + "cross-origin": "omitted" + }, + "no-referrer-when-downgrade": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + }, + "origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin", + }, + "same-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "omitted" + }, + "strict-origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "strict-origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin" + }, + "unsafe-url": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + } +}; + +for (var initPolicy in expectations) { + for (var redirectPolicy in expectations) { + + // Redirect to same-origin URL + testReferrerAfterRedirection( + "Same origin redirection", + redirectUrl, + locationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin")); + + // Redirect to cross-origin URL + testReferrerAfterRedirection( + "Cross origin redirection", + redirectUrl, + crossLocationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin")); + } +} + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js new file mode 100644 index 00000000000000..99fda42e69b29f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js @@ -0,0 +1,66 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(response) { + assert_equals(response.status, 200, "Inspect header response's status is 200"); + assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, desc); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl); + +testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl); + +testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin); + +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin); + +done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js new file mode 100644 index 00000000000000..31ec124fd6a3ed --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js @@ -0,0 +1,19 @@ +// META: title=Fetch: handling different schemes in redirects +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +// All non-HTTP(S) schemes cannot survive redirects +var url = "../resources/redirect.py?location="; +var tests = [ + url + "mailto:a@a.com", + url + "data:,HI", + url + "facetime:a@a.org", + url + "about:blank", + url + "about:unicorn", + url + "blob:djfksfjs" +]; +tests.forEach(function(url) { + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url)) + }) +}) diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js new file mode 100644 index 00000000000000..9d0f147349c488 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js @@ -0,0 +1,28 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5"; +var body = "response's body"; +var contentType = "text/plain"; + +function redirectDataURL(desc, redirectUrl, mode) { + var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL); + + var requestInit = {"mode": mode}; + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + }, desc); +} + +var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py"; +var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py"; + +redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors"); +redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors"); +redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin"); + +redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors"); +redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors"); + +done(); diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html new file mode 100644 index 00000000000000..f3f9f7856d5d90 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html @@ -0,0 +1,51 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html new file mode 100644 index 00000000000000..1aa5a5613b1c6e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html @@ -0,0 +1,51 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html new file mode 100644 index 00000000000000..1778bf2581a29e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html @@ -0,0 +1,124 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html new file mode 100644 index 00000000000000..db99202df87af6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html @@ -0,0 +1,46 @@ + +Fetch destination test for prefetching + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html new file mode 100644 index 00000000000000..5935c1ff31ec4b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html @@ -0,0 +1,60 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html new file mode 100644 index 00000000000000..0094b0b6fe8eac --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html @@ -0,0 +1,435 @@ + +Fetch destination tests + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers new file mode 100644 index 00000000000000..9bb8badcad45ab --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers @@ -0,0 +1 @@ +Content-Type: text/event-stream diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png new file mode 100644 index 0000000000000000000000000000000000000000..08302f5422fb0749bc34f674de9a61df4e50affd GIT binary patch literal 20527 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7cf4e1ef861d4455229df6de07e8d61f68a708c3 GIT binary patch literal 37652 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga new file mode 100644 index 0000000000000000000000000000000000000000..01e9c4c18f290e9bfee1cd0492e8d7613810db3c GIT binary patch literal 32132 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..123449c9081d52f5318ac46c7b308426b395af29 GIT binary patch literal 119549 zcmbTf_jjb#nJ!w&Ip-W(IpUtf3>X78$u`EAY#R(?Y*-7L z?uI6q)DfCBGiS_fQ)tx>czh-grp z40m%1@}kqDCs)=@(b;)L(d#*d#re@jqp{d%EW$)%1`fz({r-23t^ny+*~cm%-q@8$=IHoGOa8cmorw@XEJ%0jM>elMI5uT zx;eM9yipQ8X`C=l>a(NQP1E|^=qc00;>zTLKD#8VBrDpuY+T&iFzK^)b4#*zv$J!f zmrVLu({A*})&#V|=+%uqoC-d+KBdpj$ihir(Mxl?rl~PrF&k%$ts9qTO#1AC=*ijj zl_ldCPR)+qTsJK)&TT*|+AW%z+=Mc@wxrL(5{y&(E6XN*PIg*$cJ#DyV{>eEV_|NU zN5}=%R>xMRr#DQS`t+RW&DnLFv4PbruB0T5cG!qLqfeC-Lx@_)7K}*wC!?!eNsQUJ{`R@0l#uv z80d3yGqR%BxEXy`Mgi1SUf3dyyZVA+7&bOdtNOg?xm5%S34s{mTE;cRpV4Ga>vW#r zCdpG>p-6B$kUwa>d~zIOB7iz*>C7*{3l6~?ncY<*RlzzJQ?cF5Y-AtCUZB$;ojq7R z$ZZvj1cVO^vobW~r9(aQQ*GR|`Z))KX|d6UgFtBg+LS1MTtaRJSPE!Mj2paW0cekg z4}LCI;j6`v@uJsi6WKMr;o6|m7wVY@v@v(y1>)Lf9KmWpZXjSbvaoRtG87cRgMhF- zuz7Dah=H6tz0ff+2qhC#2@0&PUMZM6s7M;y2&^;10C-osgP#Hohygq#cytz`Lo+~$ z%|42(8G+J2FXq5cpMnxPmayXM8RDD}$W7bKd8y8W!Iqi#6`T%&oUI+;9#3=6eg|po zG6fzMp{Y>vK|Y|GJ_cTp8)$g<4^S79G!v8~bzbpw&$%BD8&6Ih9@AC%bb}y`S;7nM zF+J!Nz@~IZ6xt3%RhO}&I|K8;JU|Td63F4|GvK^*JK^c=tP}%@lDEjj0 zY5yF0YcA9ERO{t?oV%ejrUsd39u!T0)f|B(E=}z5=9A2bK@gLedMIOD0*bl#L^gzg z*8-wuJ;7Z)revrhF{j#46|AG6Ihkd(^qU)$tIf;#80RYLZOUn<-vFj6`=m+%F}8Az z0&bDLW2XYFuZR;Re|zX>J7y@KKCcq5t^lLzO|S@%iB(VznyOVbAIfTB8UZsP6BDwB z9SCTl_|`otT%|D)=N^!W5C|1gJ0=%N5W4HH>7ulWD1~4$2;4=y^+o~|cUQ2WG#(KE zVut1?f<=J1c~?4TlJVyr55xTb^N;s)`E-yy2#68^-cEI=%n^LNYe>10=~!&b9Iyp3 znt)f_tSGx8z-P^xnnIre-0Iw6LNr9HSdyI9!oq4|)jVt~*^Fo}mwLc;@jk!;h)eVa zF@Y<&>GjgT-14SR7_SO&_jF2FC^sDgVnLiTfH5vse_@bR40i2DrY$0s*q8qrF?t7lSu;PsZ6hI z#-NJsPv9y6-U*mVI~0Yv!jnT~sM3$_ju*>jJ>BgKDoE@CA3w`v(t`vmt&lyS7~7op zz6V)-OD!F#`fVNoXYASU0v7yP&<{ZZ;T>WF zSJ~1?rzA9jeef_CTXz*`iU}oPQ{lmbppTxh3xse30m{cRm|Clf&0us5AQkQ_Z$f7b z1x#(#)S#7F+W(VMGwi@Z8?Vu-j#*l%TAXL0BnHUz|N6nRPNI#80N)plxi>T7 znOY86EGF=2P?@Q6IB1r871){8)e^^dvrGQ>ypj&V$%4!XqE4MOO%9FZAz5tPUZ-}&d6+Sk|Fp(qN+;h$zrX8V0NZ+ z#eNK7b%{y8H*Vc-oay}CFrR0sWPuL)%-}^KbooxD9ZxYg8)*~J6D>!277TFOpLNb{ z&bBjP%uiDp%(LDeL_cp17SA0d<6My@}7wKHHL$dvNH4+ zZi7EA5Mn06@X*GD&;aCNKXa!~@z;7UKwf_|h8qfDFK=L5tdm9mTue7{NvZ zsB^k=_$(I8i0HI-3mE?NQ!j9}Rc^Z?t8UwF?4750YoGyCxHv3>aPIab^eenAFlrF# z3{jT7Q>=2|1w#Wbo*Nd+yz_No%XLWwW2y@LBe?|T9Kb>cHb6sW(S@r}r|)P+)vQ@P zJaT_Gw75CSk!rf0>(KDQD^7zxj7~`G9`7G1GhF%y-+%uhhy27MU&G4z%ayaT!c0L3Iyg z#aQK6?1SXy71C_`b$)>V{ku}eN?ZpcAHs*aiona4}98M470!UE~ zld(Ji(2H1ym}DAvPaKVB(z!qos1MX!n4luCoXRQxFCGD>-`twq3=r&-0GE|E$QB21 zcTJ+Ua>M`f=Y^ z1{pFsGX6jQ`7yezYd;ViI92Fd-!Np21snNnOv!SDfs-nC$DyzA!ITWrz&`r!!>ThK z&9T<$jDV`N2stKS1bB|Erb2PwOfanvn>()k{Nl@_YWXtHlDJ0SF)M*;USbwhVW5TR4dD-}gXMLFBt$uI%~Aa2*N9mX1JFZ1!RgB&WHXl97o zKy=ca{oyu*dleWkJF_ARI6eOP1B<+f;A5^`@YPF@Z~YQXJ(RBBClhgez}EY1>S8$Y zjs6n~gGyCs%XR(WUwk(QT;S!i$flC&6$Sq8cfgK9%jwz;6$P%hExLp~G`5QgR#QX= zBp)?3ZbAb8b=%_y5NuubAj>oHllwRX4x`=DA?RKKw;Hx}{l%K4kwMq9; z=*eTHG`VO;@K|p)51<=#@a4lfRgF+w#U?z4DYXoWWvMje`h}}SmKvDnu^_VI;E92g>oV|xNC8F2@Z24cGZlQohl>TAd)TKu-5nMm z!{W*V?SX(#@45K>Fi6%g{hJHw1kF`|3HCpKMbn{-v%T1e#Q>o)Rt(^*ORkQIw(O?X z2_0~n^A&|&X5N~&z<4cYHsHWWZ;KAjtpXqY^Qg5H7*m!`0he(SbYMQH$(0^0TPg?V zf$6*_JK;@;NP?kZMMXC>kr~wsiUXxCa9Zcm&r}ZIln8#SvJAiowCA%#^ z23rq=1_V|ITzP`2SX!xE2t7T}XFn8jk%0wNsOdRiO@yCEExXY~v<$(IPPhBCXroj% zz8p~9rdx8aKkp3WO@JMXzOkbZHjB#eaItJKz_r%`hYZ}Z`O~ME27qrN7jyP!VB4$$ z;IA*-_ujo5P*OpI+{D?3n5VqO*i7aU$KS{X!uX6HafBRK04>qRA!o$GSD95oEV(6WRoh65HMdb(D?S#84%xUry^ z$Er*{c7hJfFj44+ttBxCFJg)Xt3seh0iE%KyG2uXpW*(kr)Bx`<&$fl93wn}%brs0 zhqs?OIa}kKjiJgY`_SCYbPTUOA%~>wGCS0AxkBK}pT&ZI{`@CAW>;+11zwyScB{j)uYhm6ExUz`(HT35U{`+$Rsv!#6fg_{5k3Pl8ZknS z>8%&WaDujJ7K2}e+ozu;F1!H20@ z9JG_Dm4V4lWKrqYKl(OQ50JC7*o0sIJ><|7of`>S{I9#;+*%0ntEc}{nK=d(Atq~A zlRbDP4+c*0;Tjz3eG0;#(*iCG$N+_k3eFn?X~yFT1|a#XSHUm*1dK7c{>PfUVn~45 z1_bl|AAbuhgjZ&_5m0;kXKjoy5CdYayMS6j0%mQL+xxeMzWWd~Uf7#)2Fk}og4ghc zfN$Ouct`f6m^+vXbhL)kygK^{z$X14zqsqmKbm>?94Le}Yi+CCies-spLbRG64coC zfmsM(Swu4g*Ot!Z>Omje{ozv(|MJVn!2$2lJ#Z&gz07ds6=>mhe30<2Y7dhU&K@il z_Ji|pf&mR8qD0JebIFXBN(UxZN7}{TK_~}qK^3e(FmzkfTQoW|+nJ{+8;l*-A{Z7? z!mii?;_V~Dxo%*)Mn!>+wEUVjZWP+SV+D8*XgOCscE>hd{CcA2IhDIvqTQ$Dst( za$PfKrno5qEQhJRlA6fe1?)tad5@5Vo-L1sK4|@MeVP7kjD85yGNAzM0L0BCf&{Dr z`gtMCB?4lswU1jUaxZ6{u`jYuIPsCiv`M4CfN2$@jFuw$mrKrKd#GCDMY#7EP+CKV z7pDZ?`;blj@iN-ymV5%krr3CZ8*fQB9C=~H^#i)gKLIbN1&D4Ms?Q#T!ezD1aOJ+G z!QdJ`eH`kc9xS-YZZXfcI^0GD3?|jSw1YLpz-s+RL|C&6lk}QkWGkOL(O}jzC|gV& zz`Y<@ZOl^))*ir$yZHLLn1$~yoE$;t-qIQFhOHg`pE3enox(cIU^aGvHdR2NRxz;z zha%|vNwsiK#!;}tc=M24le=bwqIZ)KYYCud9lm;928rWrq-F7wKFN^YtX#p)X-IAf zI2Hk}f-GaErGVSic}L5jXI`^2S!|fh%+`1BoVt1sZ@{#600BdwUdI3m4FLmRzsh*8 ztM(uhju7Sk+CfleiC+65Sd0Smri+0GAEnQ^l)eNHK#NX71#f^!X6|=*|u;;1%(ndeof`D3;xM%O&Whg3ns6A++K_$%c z8b(}M4U2ug8%)4G04`&5oM7J(@J8GzSJ@0=4e{7E=CO)_Gjl4{2~04~gU`HL*X<|c zwyv50Xs#f(qCuva^$r+Jg8;gwl3gXX(>KJ0Td8HT<-|D9uXKM_c|17KI2EfC4csk7 zQ}dLgGQW7!wiZ^)`+v?Z&b8ICl`e7GVV0YCTYlt-iI3IH+@OZ0Z~nHzR)5;-v`k)o zQ)E-OR|2#sZ!i_NCYcegs#XT|j(YCEx2ekCDJB#e&uCLkV%#r_l>iKY0#ek&aml_I z!xEXHdMs-5Od^e?WQnY{Vzmc)aF2<^$P^EOF)F;8Ix{GCHlrzZXkp>p{V+0aCt=3m z0K~p9%ge+%75fE2Y8cftP4S6_s;iJOo4`qY@f~ec9qSWgk5O%-HF9GzyQUI_vo-NP z5Z*5K@%6D)j$5ir%W*s%utcvLN|KDv+?5l}l1|Nwih#%->9*&rJU97FoS3gZ=1tu| zr69X9Faf=H*mUmJS{IQ7`Flt zz|byVe%Z0FXULNd1=K}k08W>&$aJ3VyAUcBt!3QpoeVReVvrvp!-X7?|6e?bVd|0D zKi~RGC|n{d1!$*>9GJLjpU{rt8a!;c)pY+_aFt_6htPq25#Sd%1Wcgbg~5JrPi~mo zIGO?Dxjma;WnA|>NWkqgQwHK$5Q4M6il63dg@Qaj@2YymI#lYcwtVp+p0TI$voxP2 zE^u(@IH9Y~1q;1v0MVlY?zHGHKYy{#EvL&hm7~1%7WRM)nFn`>D4q}ONj1qwY0DmDQaH14T{E~= z9zl=S#!y%zDym{$m=NigMt`3IeR7Y=s>y%>3#tcukQ?F?+25**;LZKYeXiUJ$@>g8dh|g6t!-e&?L%%6U=9J6 zU#E9$C&~!i)`#0y4gKnCM)nux#PRuWfdedSdX@223uEi>b9UmPeJ)@jAfQo%Fq~(RuXL_wvz+WeK9!r`8p@SZJG<2UH*$5&y0o#}RR%X3SrCohMsOz^?x#KX z<69sGd=nSdDXL5IaW$l(tWJle7_`As12UO9Y(c4)UYyw#+xc5$I(I&FEWaF(3E>xha8N!jDpy<4 zAv&GGEd%Ap6{6%q+;~j@b=>j^Vknk3miaA42`)@}3NcY=de;)?wy?s@hpu^4$2yD%_UE|RNE zvSz9n!7DdBzwt81*37355E)&{9=lH`>l(zS{K48U137zUianY~YB+M%)Sc02(bbvx zOyF-EEYoZ&0$$gb1m(*pJb5^XzTaVdeJl3eAb`tH*YGewu@hujH5gAb73U>Q zu?F2nzyyckno3vKlz>&)^?iU# zW}^Y}nbjKH=XUvI22?)MTL!CP;9BhIMnBfe{ zNm#eFGRhN~`6~N2_Mo$?0F1%-jOa1eD3$X*KBvIwjPPbKR&hWK7_;}w*XeeE(FIaB z6S*a}yOVI?bCZt>fPGvr22sDT!Ob)Jz>)~Fu7XQpkxh=%sm%OMGuTb|`WJWJWWlgZ zOy0aE*C2w$ta)$X5V_cgbA&o9vZj4 zFBT@hTKd*pf*$u2jkE|FtgVfW6Ek@i!_5r@qXUDV7^y=#OSG_;1Phz@96U7 zv49ORfm=$oukp|N7(a zY7=v!F+-T7e9|$I3smNIN72)OIlcIAk9-Tt6lj1q5v;rVL7A@si(BW|xw5G!~HU04i?o{X7>sO?~uGfHZM%r6_; zf;0#QhE3Jsmd5&a1Cvdn(DQrsZh8D>(j*iw^#2`Nyo@J8q|OnxBQ{tJQRSpvGKMv zROJ({Szy+**47m^3=*gHJ3e$J6P7+>OAT+y!Cxdo0hnUyc>!&g(W|xgipDsMmu*tO z$Mj&{yR6wn#*Fo-!SYhngkdbbz?0fT!7|T14NfO}(4*Xj6H^|T_TNxi)BNREVYvR{ z2esAp9{OEnLuRtr7@Lc)7M9(XWdP3S_s$NvAe2ja z?^Pe=Kr~k$<{?m**TTi&S137YpZoyYAd^zJI09b%r(b|)o1L5>1dg6Nxl`Th<}g`) zZ7owQvu+T**J&rVf_#_%{H=0F$uUE%Vd(vzyz^uXlm3%iz-qOL{bGim$BxV!ctLolNBzf>f@)G#5W-VV+R&|8K~gm zL*OnTdRZ{jLeKS|`=Ms(_7RnhQ8~cJXpGy}yQm&AjEN%V54XVq{WBV&lBQz_BNrRz zg7jn+28?-k!Fr!am1>%ut2nZTsM^gD22~3Cx>Ir-TYCt|@*x6dQ;MHV@ZdQeSRTj@ z|Ao;K?(V)AHq2GzvFz*1@7x5iGa8dh%V`Y)Je052IFZ1qIt<$D+y4c*)spzdj_eDCPJUGDVR z@0X$nwIet8OlR+qQDvbXiuJ8k3^wuR>asa1coh0EJ*dkBiyUOJ|DQ)KzfFNw;mUm6 zubxj!G()LXiFo1L7{nWE7NS$2@a*W;@Syh}fC6-axZ4tt<)=nS!BWRj@hLjCJFXO* z8W05vV!i>5ptEn-fU|`G>lGr${d%LDLyNbD(ejE$@_$ZHIAf z(z$`3#Fw|&%CLOf-$5mFz(1k_CTrbAX$S0xwl87-`Bnc zC2FVG?c$HiJ)r(0R0L$Al6Naqy7F=+ZSEjung(u9ZMX$0eWxM1WDL4`PH9#s;~tEp zAdUS|Lx-QHvgZPgHX^xG0n_GSZNkeI;-8n?VJ3?;NOB@6<{XBh!a#b9j=mTFGW_(0&=s)kjb z_0w%3u57jKAYhAu0Zy9=tq;wyG~<`Ii(PnVpt=AT__sHICDwxN?qjySHNA{SQZoAE zjqEGBx1JQ!+U~V)LvziyUkP%qs2fOc#;9R4bpIO#v5J&WpWC1-NC*3uerJMIai?kJ zD7gwk(AMXuQth~+(wRr9T-A;+K(jZ4dHMx|^HjIwy>7Kvj0Dpj%?z%}6<1ca2Htgj zOwTpUvqe_vx#Wxs-vtNQbpbv_G8ZyrAiQFMd(_aM%%(A>x?nilc!mnuCiHpGmk${h zM$f&L*Pd&wh|lk9w=Z338OJnmz!C?z8oj9CTb9p$mh`f~Yr4cdYB2XfOIEc4wFkuZ zu?ylB3Om8(-RQPmoz8Aa`LzjS`F)GCwI*O=;NQB1ImHIWzAad1Dmj62LA7iw4VGV+ z)$CSx=)y5%b@G%J5K}qw>+d}PaT1Ioy96&G;4XNrIM6sd!3Gmd)t3=!LmPcy1~EX+ zAb>Fjn4C%MZRy+AD_VX77!6|sPpUQ<*Mr!&0N-se@3I1}g8Pc<38jw!9>8v}#yzr# z0Gn-l8{>xPygI$SsBSD)7GS2R;7ky9olwi)7M5gK{zy?~`Uuh z7Lg5G+l<IQWj^~Su-H1Y8w3j+`1_*|#6uKX{?NCD?)mIj z!L1N|`MYd4f-4U}R)aRbzV}z1Sz?Up73rDs<-=|f(5GY!9%q~dr~5Zj3IXqbf;!s zzfV=~^{2!(k^;$0iKg#?1ujzMmmgFQ*M^F>zRRQqPJ!k>d{72v5yAt4JloDZv(V%7 z)l*F?DXkH_?O@)znU`js%oTtgf&_aanjpehu;E-X(7VMjvbJ16gLECIfwai|V|v4N zG4QXC-%BSCm+XL#fLLSzoebD{Wfmqyi4NWaTFk2MwzT|w46Iq5sT@%mxcu#`QQjfY%xn?xoXudaO2C~1 zL2M8J{hh+z%4@BH7^Kw_Mx!i9)e&@N$bJGCcdfb!G+JXOdw!yXfYAO^41MaPVavIz;%nEIJA6@^R1VmYGB(? z-A3iRvvLa^`1YH0=^ir;h+=W$37s?^7Vncbe=JtvY7H{rzWY=D@*vayulM{G{Q492 z){eLYgDogFltDBy6r`@wjKUm94eg>aX2%Df?wmU=%Vf|2NH8IyBU+@tp40#O=8NDhtTW)R z&p(yy{pieRHNcr~@VHl|Er9$4_kjv_aDzRW>rBujWA9bHC%**(a5qbq845oT$Q?)(fD&JjblA5hHjZeX{WG3!34xd!V}b zb|)~@08{ixcljJK4P!)^EI5p@%p(RiwktKs7T*xgXqn#aIKbu_4cTmiuDFu<;12TJmf z1J9s8c5+J!dHH&O>*Au(EP`RH1p$Xl3MLt#@*`J98t;a-t&O0xs>Z@lw8o!(5@Nsl z#xL#L<;cS&HoTT7H*l_s1H2=4<)wLle|L9laAfN3TSYn+hy?%W^?&{DZHbYQ=6`?U zSw3MH7{C#<$s5~R-jS~q>q+j&?f_JXa8e7xXc#n0UmcgR*F)=OVhS3KZp0T%d+d3wF$4ql=X5M*+PU_ z{-8m|28eHX*$9Jba?m{ZB;o~m9{om-TYvm_<$ILM&6wZ=rw=6H^9-<4H4sN1513-a z>Jp?}u0NeT1rr;cptz%<==jE;FY%z&ftVDTa^Pda@Ip+n^JOp9|6pc=iBR@4r&(mc$mQD%ZlapIxnqVkNDNNjNKLUt zK(NFObXq5yzL2 z>M(Yqe~YY%=MSSxljGHXLYJ=|FhsLiG;Uk4sc9k54zv-J5*gh1 z)t&EnV;!3ztJl3({qD9PrE`GJ0`#EkujC#Yqr7KdOyv+4{`v2~1cwU?VF}C7=ljLLZL%}g#pQDy z-k{=1Fd&Wj#JUmCnotKevfW$H2mnlApf{-*+>ZNKotUbw2~6PASsB2~&slz1&L{)w z>A1LS0BEG|t6q*c0ueXElo`<1CV^KL+b&Y%|pA6J-`$od}H{o7IEOkB?8N?8XQf z!KVSYJX@HB3jNNh(d&8EEkbL#=?%j_Kl+2~ugc-vjgP9ms#L>Y9{C9j9SmBpx**hU5WqDwaw}i_z?p?7 zsEF0nlgf=xXj&Ie&*(v=W8>g_IoAmSx*|n_8#V3hZBS>mArlr*2?*_j?`DcA3&H_Z z3~E$GMi;QiMnIPD*MQ{0nKPZG$>4yxB(yV6)`+Qlqhh=It?ywFt$ImC1ZzX}z$iJ2 z)!@H)nAQi$Zpb?UDr!AYnL>deGYGix%Jv(H4ZEQOtZTOnd>3TNsTC*yBYHRh)^qvM zVR|myXSNH1NdmNiGUhsXXQl_R#+OrHy>zS(DOa2GXr!_$` zkpUkObf$_pG><<$1J0^3gHC}2(G1?j@As9i>_OqCt7U?VJ&FH&%VRHn_PSVB@{* zfF8u;0KB3*>G<^{9=)cLY4n{tWtJ}Tst`R*1PjlxkjcZ>ma6di%JJ(ve{P`F1N|{e-q-F z$Qtx@qPFUdP%4L&ZwkD`tdg6)0ghBl@6y#{R2;X-B%_l;Sdw&QZ%C|H@^Lvt{5qZV*3nZIO+Y+QZT@WNx}v&7dPPn_4*qpUK8ZY zF-1Efheyu5P^R`)X&mneNboL@8E}=+yF7M+SrDGag$pKUyjD(UG7;%3pTa%xE>JZS zW6P~~vuS9>xy??+ogw5z?(mT1yRx8meefXo5=fvgTVh9CW~-VI)3tj=1~3Bqs?`>X zZ9Yh3StJ*n%spg@1)O1?0&QuQ#0X7=Vg`BfM)9CsK0Sa7KOPmuXAaV};`+0f|G4=j z7YU@Y6Y!QhprLq1oLB4_p5xk>4a{qSjbnl3OBr&US&#oMxUe9<^oB_e>-EoHn1b5DeABQ3?b9d0-nm_D-j-2I8%FkGS!SFa zqUbygmQP;Gh_z|?P3yJaSeDI{X8DDT^4fq}3$&#^19-SvFw`6nK$p2P3Cls&A2Zwd z{sk<6n+WjSNiZF2IVoV1t4xV01_xMI`SaCF>aiil|Cy(B=2yxQEH+g$&^~_}EPwn! z<}#|6E${VQeN7L;#UFqAGu|FxD5J|uWKeZf0c9D)OFGO{r$NMmET4mNo4E24^z~@Q znsF5DjYGPtj<~TMPgmE!{S(zG<(N_tYyOaKYK^HLqmzlI-K9v_K6;2A&h-K7Z34S; z8*4G4g;g&|_Zhdf_k`|1->CJn$NFX#gh6y4f@RyDd+e(pK&f{<__zDH*kq7^+Zfv=xM!XSFwzdgq6<%i z@Qr8&-{cBa;_yl|fzm;XpmJ4ARc9XKRR9X5LNo8v>nh{Npzfr-WnIG-d-*msxw8NeFuB&y1F_F| z2KU$9tN+qe5MP^hc7yTTf#iWrwpG$0zXZ&R!9fmh?$9H;&I(v zH9bXtlrkxpUG^YvkbnsSFhv(v`??cqpsW{j7lGN0;2j`73hcU*3=ja*j;&?zfa|GH z9;oG|fwjyHaQ22QF+__1U*uUCAZ|a8r7d7sfCkG~8u84qxB?g5C{rJZvHZGmNCr)< z?5_Z?N{2_NyNK2D$WQO(nE~6pliJ~XAYZ>4_#I>6bV4%5)W zvT?zC+v&;5*SkS7IbzuP`ku%17#v4|g%paJ5BO10!E+e(cAYjpskomUT>Z_FO4zED zsgn~Ybq@?29ht8JK8vT_oBHBZT5)T20UjA~{j+&c&p*t^1~3A$7Cr{n1RBSvgsE~c z+ZY-F*$)DaMdT~hEW_PYYs7+KVvk}DbAuH}=BP?*KB{gNf|=4~Fg~i7CT6w`4dx!BVaUd{Mn0GnoM)!pM4KH zGft2bsq$i#E23wIvb?~p3B;AmF3iv?f>aT^5Uj|~$hRL3WaXd4uD92y{`}KP?3sx* zV5%I|RT?T`qT86Qe!N38D1(Vp=|{hBf!H}ecmphi%W}y96VR`M+6BlM(FY;e0&C1* z5n?-zoU9t*bxz{LpUQ~0Wv4lbSua0Z8E-MWnX^DhCs?cK!nv<*5ZBrPYT5*gXF&n< zU+P*YW82TP0Ahojxa$3>?MyMee+wl02CZyRZt*hkP(`?@uV0>zt)hp4v?H)%rNfI)ft zDKNSPh$s*VzQBhHuqUEHME==dhyq^e3bxG+azOygV1j{DSLdqPFj95~m`Q>wzZFTB z*04;}EmdmDu%s`?k9_&Ogh$RC;NrEQ;nhBU1fRR`rBI?XXB_3@$LO8IKMR)aB>I)}5bX&*WT4 z)Q;hj=TCvDRPmLW3*UnD@L2K~KaR7r>tj9dnd-rtWS;KZ<0isne3DlTgp^~P3!1b$ zWVQqBQOqqsAH^n9gxP*nvj?{5!34*^_y%C^kFd|F?tEcx*J zQ1sSRw`S*lU9}wVmCM{nKcbs}Oh=L6*&z308LC-_9p(a%#i!+Nd64C|kbYtV4;78B z9b;YtX65Jb0u}<3CDcvdP7eUqo5}e2gI{xBg&-Y>0qLGtY*D(>OF@$vDz zPzgxDZooWdFV>(Jt&=yo`d!T&bLI_A=hguRf-dXuma*__bRNLf97J|R?H+J#S8uU; zUFNs!L~_;|E9Q}Mo=m&cs1uKtgJI#~c?MU9f6mQZ0jVMK%CAM=sMzp6wG90Lw9!uZ7JxuQU##W1tXx7MUXzugx0`?Onb1m6tAkpzwr_2dO0YY zSwF}Wy$M@|wUwG_2b!bvL|D~Grr2oEz~tUvq%(RgZ)TtW!?@EdH+2$BM-R=QnlQ#g zpv1n!m>6x2t!Pw6fm~P`H^#m+u*^1H3u33S0r6yDI~r`{5Nu+#w3v5h`#IInq_Wvo zbv1uj&7i%^j#FyO;ggkIxbg(pgD@kKa^Ra?5BF3zx~| z9Xfz9kAZqezBpOqje&4JSEtv&n67-86}P_YVS)ry6+>f10;gi%eZ-GZ2$V5jP_gWT z{`$k`AUKRr>tSNB%szYwOu*GdXd~smddo{Q-rSKF4wZRN2=!~eyjq8mWfl2Ru4Clj zII=Jd`lu+< zGs&Pa&@O1?{N3Ohl#elHnF#hF{_ElU!I?`yxri%_BYkq1d*Ok2opJZyU%tgbc^+1^ zma~t_(H!dNCYuZ~fUDl1u2*~74BKB`{Z5m1fM-9{V|I3fUW});ImX;cMqC8Y+Yuy+ z^=Dv=`99GMk9hwF$MJ2IPCRCK&+@+2RqowA>C`xcDw>!Mc4EQoobwo)-Z-CHED*up zpM2i(XCTfp+^`o}We^#}3yd)FV@)dqX)N|ZnfdE?f7({MlGnpcI6_~gfB+CO^Jg} z>>L2QtkD1G?ptrUv1&8Mv{~MxYQ6E{ymnXsSCG0O9nGj}`1;n@G#!fabAjd2la=qa zQn`?N?d}cCaJlO4(CF=d#D-dByiE1Es6eT(@e+c_AU7ynm!PVu>c$rkXtr>5j{s8y zY{j90K5U&#=7RDCCJ{Z{mu>*R38&cSC{Xu(l5S6<`^po*5lb7f$^4 z7Q;G*6QHA5YMlpNOag!h7c+s274568jswYF!gXXw}$P%!>|MW?ha7ch& zQ*LB%fWEh$05{7(<3XaOS!4V8Pg`MQKmdOaun-Vl@s_G|;WPBV{p1Vm0QWH{Y8^u? z*ivf_cWZZQRXU59QKR1r*W@^G?hs(iW42TyxbPz#x4{n33~Gx7s{?I<1O@YDXa86_ z-Zm4juI>lB4Tz!RngAyTZHZg9Q{jVZC?1M{I1K_)MZ`dydklfEK@8k*wwJmYYZdTp<^leE z79TW=3A|%KQ*APN|G)n6+ljmFVl2|;az)GCn+s3nLVQ1YIAG-RgW5cu;R0I4*9ziq zXR0eN-X4>UG3@|-0lf2yNR2b>8r=I5_P6&LWR@;tas9R^a5~B7#=qO#5`j_8Y&W>^ z!$(+k8k)JV|9?=9$H~cbl8fbgv$RXu&Rg65K)2-=z1o~8EIu|cV165H!5wVqKrDPd zvEn)lh8dTIMKJp%mTuFZehEGb5;W(t{EGs7xd?!XnO*@_09sMG`1!*aoJ7!=P8)vA zvQGz9fmUz4Tl1H@zAZ;oOp4U_OquoW2kS61fm%UJd@2BRw700fk?~NN%7P;n94$Lw z24@D%%N~D>z`fgp1Z6CGrs}+-<-ypI13g&gz%Sh`>g9Wz_TK+Bbf)I{N5IAs58cAp zEkX?K5fB9|2ChJG?gd9q#&Ur`CI^j!ZQx)2rYql$ja%SVZ{74^!-S)+HnY6KwM}(R zXcj1ZUQB~h#mHJ0oObYMU2?Mm&q~Pt4X~7GT?NAqcwBmR6nnXMf8x!d+9POt3)8nK3?Fs|JYtB`~fY zXxm~FV*lVn(%LVWt3Y)NmY@ii-TwBgC%N)*SFjnCHc*QadKVx%B0s)`ZfEHM z`gW1w$Et%*TzYs+B`9Vx4?UL+a>R(Wv%xSp%N6K~REf~XY`_4MSp(}Ob)VH=6cr85 z@y;HNAKWu=<(XP&mM=Mj{E|tn!F6jsY>9!kDUZ;MP#oOAB11b`z3kvTEF7cErk%4PJklS7UYn zD&XTX0(U97@y{Y!{@peEI&DT%#{7JD7!zP-3(Pw!ifOr) zH9Mhpu0j`O|KE7R?Eptswy}!+cIZri3jtQvxVCE0R1O%6w_8;J)GcnH1O$YN0Ivkq z9PDRkhIJ!7u)==jwb&IQcGf8n3u0=q37A0xK41o&fTmC4r59*}ML|nKXN@pn-5NRNHy!eJpfp^r|fje+x^dT=HIP=>v7L@0p+pH>|EU-Kfpa(^Y`qoPoP!hVkT{@H-F*K!vHOm3u zbUxq{*9)GjPShR5tMX>LGGE#l20hE)J0}_#R?A2AAi1NgINsKd%Thhy z%U7T$aP^FFb5}Mvz-Iv~d?~>>#qy7EutY|>BJInfic*8PkMULaadj|8jKif{mG8DO zP=x~xC3QH(5A=d!FkF)$SZspTrk5`1MZqdQ|RK^PhOC* z(T%}+_dai~QRN3qIV;78pKEwel~dkOH55Z*O$BHcP5H}HuZE4W2$?cB;n-FTTRd0r z{)3tdh5aBH6y~LJmcbXque(9X?BMc(5P7YzwcfR$b4DApovzflyu>a6g)1mOOKnF8706T_3^^Ive`xLQ)Uwu|oH=AWB zr1*VR1x&iP-%x=Kj9E{<{=|%D{h}NX9_R#C#Q7busq&9y!7nm!yZ9h2e%cHV$eV7E zL&`;fp91B7_#1X~4VHgl*zeQdE$|9}il#Q-$CCVCKm7K!@3%nl5PJ*+mns+$(Foy^ zx{2SB#`91hAwE&~4`D41PPe9|Lc*E$FD#V-qYE$J0>sv+9@(C6_#-njy`7wv!4CZ@J zf{lZKYtIzr@~(X<^Ti526>#Glh*h5W=D-@QqF-jO9k>Fmt&)3MTxW+Fp?7S)x&wT? zoZ+lZF@Ou~d0u(C<(IcDpI(YcDX?BpAPBg8|2TsLFxYf*)mjw`HFm?g)Qtle z*!ObA*4iM#%$eY)L2A*^!a*8Tc;-_xU}97)&y2Ba--DVIQtHFc7gat=8F-Oc;mb9S*~0Jz|{hDyZc8kfxGF!Iza|dV>TE&+DXjqEKD;|*;LrI zyxeU%BIE%2W<9~vt)<^Ah9dr9fp-scM6jW0qF67!#zLW|yVWtOO=~AG6209qjR`j2 zaF;gEtsL5cg)^c>yxd8A^1L=m9XRvDrdkZyqwpbzwo@)eGO12Vw0s~RrcDr^>5RT$ zG=ez>F!G7)0c@18k0$|(!h?@zUS7G z3^_Wu@%Lw6g~Ck=Rw3BF`ayMK>da;^yS>4#{y|LPs7Pp4GGgwl?>n0j3%ED$4n)+Ot`9#YHrSLx{$|&H8);*rDwfGjsV{d01M`h(+prA-VX-QFF;Kg7>j}> zy$%wX_vsx$2Shb!u_FeYt5#iS{jl_yoALuWfds0HV&Eu-2?yT1PzJ)CEHB{c1%`Dd zrZ~r_O|R!dWgG0Ei#2?CCj%U`+E8NoZF8keaMf-g<>n20x}6=?@@&XO6mL zG6RAkV2sw;ikL@BAT9@}rtRGKWx$T{*(+d?6JU#_bk9k&wzno3xTk$yfkkDT`7cjD z-ps`dRNNLEw=piSpW|ygpuW#F55jm&gtqtW0(5hVtp(r_jNPSYzf&lWq-7Gloehg=J)*>=G~zB4ZZ7fnI-+ z!uLd6`o&^5AETqGGB$_+=K+kOxO*h=l=z6`=%UWVt+Hp;05P+}OaE9pV1#7Bes&Hus zT<|xpi1A7AivNgDUt~xBN{6a`rI2a(1B+3f_|83cKp~ zpgn8>yYy|aco1;?r`8Z*7^xhiO4gCNd*uZ=6g)ySClRbWU(hG7gYm&M1_BHsnEON2 zPke!t%rF9D$zbiE(;NxfAc8==R)mB^Ui5(=|wFjKJrh#Q}Jm}(5+|kO#+yp+USp^#CfhZZ#waP;s z%h+u{zsVEw3$uO4mkHh!#9pTUd9xAIybG8!zvg4>$X20v&^j7US|1IK~;6E ze!5Y|2U}s8!yMhia=G#{SPqEeLWd&=>#Z?Z+6O!LvLR6a5sw6Rqr5NtthAa*tJwf=dwUY_G%}k@C4Q$#P)|ftr?)tLz!BS|xJ#pZf<;D39NxTU& z8MyQZu=0xj;@Su(mM=<@iGDlXM-QXpdt;b42lnCb!93=vJqjlH`UOkIo4PSv8Ui!y z41%w-A}4B8^6k-E#cBZc{5@cdKtgaikIrLJ6ik5uZMrK@jX=7A%&uUcJ_N>M1G*SQ zz|0;ih=eH6Mxrn+H1L(7U7mEVk)rw6Wu%ghZu4*2adVQ|4YGN-N7ZD_~;@=JWl7S?h2hl^fUfwcz)?<{XZDJvIiI!@c(@L zK6|KVUq2dVRk>Cc20#EF^`3ow^LiP8$G-Exf8770PVQ-|ICFWoqH0!dvmecIEz`Vp?F{*Mpu|Vcv>to2d**DScuWNiR_Oj!RxY1 zoqx3p{LG_laR9vq7(vY2*%29oPpJY-;UcS5`O`^la!BMvs3_?omYF9u>~qSAToz-k z;bY5JfhSnL?B0td#WnfFRCV%BNB=Kj@4;f$K&-IM*JlUjy8XqIj3mG~3A8t7&)?FvD#LD5CW&DN;!xB8 zwl08iVVnEU#~ubB2l=R}XE2jgh8#m}IA*j4*+!Mayjs|8{B6MmocHG6Uw-N2yNzsv zf?S8G;@z!D?K@bP?-kog%rIi-;ADA8fN?qb%WYU4eX64)nA4lC0c&|*orwNUqBGiB zY@_HQW=H@CC~W}aBHKX&pf%ejw!m$WbqE+R#TRIdpksZ_aHtKGeGsrq8|SnM@&y4Z z()Lh$!uL{G6#>0qhuSZTK&gxey9w(Ny5S#xxFvN2BVQF{UfVjnFqP3QNuncwfOSq= zIlr$kU}nPH)B^%Tv2C%C?E|n#wtV|3HoooM%E}_hye`?mn5}Iz%Fcl6g$JQ-8o@5#l#0*t9S4k263R$Hbvkmw3~ zQv~P5&e9Oa32{Ac-o;FhJW%;g@@f{Osvc-Mn5-Q+&M2d^_uKS{w%h{g=T*#3suG2z$;${BQh^0tP9#F@WIhAjXZ5|4-7dU} zVUTR0YbN$Y2Un5Q$A%W3=7@dZ-`=bbkIsTSnUG!`V+b+cS^>A#Qf{OB%y0~DsjCdh(akKtZ#P6r>QZGROkh{?}+ z964XP2Z9<%xrv#eGM-Vo3CHtn?8{>1)Z6LRn_!Fa#ovRm<{7y4Nib#%`w5d`%m<7F zbb~k}x*tF2!q}COSS$bZ1D3G!Zc=s3U4cUpS=Oa0tjubl0OSdZzjV6?L^oFk&<|%` zqc4jLbjMKz*!m~~6};=}WY%%%Zgy!;It}>r8({X_ojZ5X?VR2=$TFU%FC6m&uk}_= zW9GL+fB~wp`4`D>9-`QPQ8fV!Ea%rF6S9fXX3;HG~jm#Ft z@SmdP`tM+0WV%@{g0--s9hrvR#)bGM7V~&vZybcca{BUJ`^rPQG0Bl`x$wf3I~|J> zbnA2{JAg_zv6ko?hd#d*x{)3d@J>TlboeZA!a)8yJMx5WY=0k9PX{9iaJfM_VCtl8 z=kJvb@s7QsfM+o=0Y)RoyN5ZQs#58XkIANrddP4G8Gxr-ocZ-fcQ46!KGPpcHK;s{ zDgXD|9%T{<-hOxmN)PuCuu6cMzEnoiGjc@5mhJ@HKx|IUeJLFMRZbI|w##9W)7&?ASK)l4TU%DUw~H>Hj|TFJv_h+GIDhvpb|N+pr{Db+)DK^wQ&kt^{?5m$80|Px z&kCpM*!0o4bQZ~JB+(I>@nB5YP|JYXdefR^M;1&bJ4q$`pnxlU=k-Y@3@{VE*7eSh z+*`TOMo^QO4;U^%QEXhQiWT$p%x}(rvnQJlTO4yyvz8s1ZeICAaifV0>E7q6FVHvt z@$5sY5%_8!&dUoag|!%z1lppfk3sez!S?hKOLVQH!e&bQ8b%otl&Rc9-FIQjl-uY&`s)UH3u z{x$rUCtrpbJ@*8=HYXhbq6je7h{m*Q5EYy$F=Tw_N4i0EaTzc0472_0w?30hG=Sct zd0U1@9**@)2OUheIgqn%fns`3mRs@F^{q3%fEEc-uCRd0HUi{&%>^?31rtnzY9CG; z@S-!aQii8@8RHEe1H*fKO_874eRzStFXx z{`hOiB9J_RU5wy!X?d!5&fIWiY;ugs<=j;`ps6^WxgR+2EF8I5?bQ*3T$bW< zl+2!L&gSw}MAF@DU$wJ7fmHlUgIto~ViRPgBaZ=6MSy27dM*1jsHDQ!-FIu4#m)Tb zE#=gC0`}1ur9-H*EE#AM#OvC$u0bgRv6KfusoHyzP-;~`mor&CpS~Iz03k5w8Wi(> zbE3T35n2ZBkWo;Wlrd2m`A(J3B(SDGQ_WXP%n2yy|mn?m7mUibOQ!KJEZfOBK z@cZ~aZ#50haD+7iaPj{N%UjNt1*&`trg>i+JDlou3xeXez+PhrPK(=j*-10X8Q6pb z4I+jNrt%&Z6YGJwzQtVC(VL_;i>1p=-vVW?=iP_&@tfa-I&7!9L|I^pC(HcePwc14 z7={D2p6nSpf>UAyD$>DNIq9n>e<@0|vK!YvS-CX<`!Meuf+3SXVKBu+w&p-^0f%ct zB_WW!U?~VN)u@c!v~5Q+09VkyZ22Xk)&tBf^c1#W=SKst`{ zYBw>U<4+!qiqF0>T#MEJxSfaF?YDDY*R=mYWk=XTA6;fl{^tkx+*u5H6ZlG2x&Kf$ zakb8GJu3tMk*y*|u+;h5GZ4wvV5W7-KBlTahJggMh%kx(S9WMl&ewhek)Nw?GgXm4 zC*kU$Z()-7AF39B#&2#Wn@k@{$5^?@he0#U1%an)AN*JOFa$&Bp~`voR{^_wU$%AP z&XF&D4LE;#*D^%O(DY%9n(mk1gJJEX@5l}lN03ui+B-6Cyw!8}&H2VYh)$3|-ECRe zd16@&<@jUNpsw6%szisRw1uILmJP4s`dC_HzYL=m7Nv9OXB4(>k^Z#HQ;Hfc0qYEhHE! zUEReUnQED!V_Jb4&a6W=Jm>mH7NL0tQ`ye=%e(eX57FZywpb(HC z0V0LL*am&{Jma)gz*YbGYv0=|fE>;8*)c-SgMe7j1nA0>TfE2Bg3L{|*v5cpKRp4d zqrXJdnjllW#KI$2FT4cKEz67$aE;8DUN#sk`k8~@P`qKkl--LDudiPG2U2#c=d$-Apu(Y>RgC0J>Q?sdZOB_A7J*b*!%m^bt4eGeL&s+6qufE_0B&VBxAaJ{le2M1eG9Hz6)J@D+n?@nI0ER z>Kg08C?kU}XGnvg)yTZ!8F~ik zeBROxAlA5!QD|&YsuogM{EqGY2@n-bykS-@z|vB$J2{_B&_NIwun(%>Z6v#vNzwrV zJadXf0YBp90pHpR-NtMaurgF@^Q0A|t*rJ9qn9_yX?>a+S8fNmE ze8?08XynOaQU<`|W59qd-a)ttsEJU;Aj6Vc$elnLfcGT*`)zls2-W1%MMIqA_J!BM z)#*&R3LB5l_XA@>0?MOZ)!nmI7|zmzH6Z05XgjMPJqICJYuDB5w~I>0#)67la@6AW zU7+|e4sP>T|B%R{jRWz&nIGQ5shTx`9fMM|UnYbC)G31CO#5YU=1;WZGsq7lGF@C))eLJfqc_IKS%8eg?kJwE(8+@rIMK z8uIz?AXOucFXRDBBVc>86X3ww*lKQ7`h*qlf`TE4{?Wc|h~Pip{c1O)_7S5_*NG1v z!XQ!Q1=MBE#%3N2IQg@=jBZ{QtV}aMP@#5L4#|f~HxH+E&6z_oEn`83&LD6BU9>Ce zo8NKfD_g;`u0i?YSNiCaZ@6m@)Dr_kk+I|Jt8ZrP9YL;cVBMy!R_LknBY}1hU~G%B z5UsyqgL6&(0Eu64X6-5kmCdiH%UHbP|pV}_>D`qIB{Yobx z1YMRQTZXgTimty#x2Ny19RMR2?*c!Fx7$l!k#9x^`2{Y9^QsJ@;ExZeY?mQhR+U4n+^j0>z)_EIS(&}(g@OV_BK&t+xDOovv&<_1%YS9;J=wht`#u-6{>I= z!xFdPCNDd6qhZbhkv$H}C!amQkQod_e|kIkr5C^)#8PieuoVsb%qz@bPFUI4E|{+N zrhUdR%#CNFFo9JJp{0W&(kH;1_l@D>ZBV+J(%BqXpqs*m{7OLpH^1 z+JR^hj7gY_=u<&9Hqe+cyd|?{5-&as?mt|{MAs4X(KFR5Jz!%c04<>0D|gBOoYUxG z*ntlp$=!h1-DNN>?NxN`ct~mwTdye%7+bXhXzzc2u$;xSVyU9blr2}qnpzjlR~02p z7y(xU7)AJni1gtZ1n)lp$=Y-Bs%qD*oWZFFJ3H2!Hof;UUBVy%^-K~TEUid6B_jK z0H?F5^=jP)h2d9C=(L$mK4J$rV!HXg`6@U&p$=w!kcf}n3TR%O^j&ZuBQ zrIThsM<8G{(T8nKW!{HB5G#0=_El#nGuvQ)`@>H^zwLwPW$OX&Ks0lKflVQpy6e#u1;?Mfa{j(4Xmef0 zK8R|Y>3$Dpmc0eorTdb>1$+L^J0&Bmd3Fz;2k_)70PVF_Li}Fm=PzTV>Pa>U?=?au zcYpZ|-D+|o*Dhcxo%}~FWC??6Rx8jK0me(JRoLaf^Ap)}DB{Z39cQ3j{f-PvTn%B+ z)WkEO(ml)LtPE93&?-TKcOHi1(sR^gJo7N{X11q-fL#!Wlt%%s_Q4;2bMAlr=2pBF zn-oVc0XzO+eZ?*e%F*z#7{TL|pP&{@gVFW!15H6HD`*J`Ir4xwcAyh1i>$x_UVWz% z3~$JmH>>s7@n4_1KVe%!T{)x2Y*)$dHfWjN!vX=cZ+Z_wQR6hIe6v(tTXxDdr4KWf z+c*!v0NQ6POnZ!b$|jwE`m8tfFYfA9p3pbw3cbM#vv0B7d4bI>bGkebKIHvLMPoPs zx^wjAdF@3DbRfj!O!kTGi{BLcJ9npuQkkl%ScY_s=@9c`=GqF#u*zpeSvc?(*%P;} zedajA_4?O%(&^NVO@}xrK1Xpu<=Pht?Tw7K?I;*PzW{r5wIK+MlQ-n?%e(q@uqF`Tpgo2{^rUT7#$B8Oeau?w z(>^yWdo+ufFp4yIwLxJirh_c@wf|1L_ck3gee^x;F>>^%APAy042*3r`lxDc95Uh{ zf&T2_3zYGo_iyjxN`S}-k>J|HP`U3WJ0!4Xk;9G@3#zKHvr4E)&DDOQhep7XABj~sD_h6qr=V7Ip#Z)@h(`xSda?8l*tgh1I>S96$^Er%uA=O-M_JjLTxXX0sX60`??bR>mGGg$@rF?FVyyY_Q2_^Uhg z5gua=N^H7*C&cb@2RIPxKV<5w(F{FMu(S9c*X_u501UExm_%HdlLuJ)K%gl@1A=$D z8S`kL97{$;Fu@ns0LvJ-em@xJSloT>w_*wr0nS&Of>IG&CNCyn-xDx+z&eYe$mBd* zF@tCEPR9G3DnosPPa{(V4>GX`C=(;&t{!8;y3%20$sO3nRx)5Xox5X) zeUL*|Eo$DfEgHfJ1^9HVa8%7fIp#cRBrykh7 zQ1|yAJrSrv?ZD`F9s7gxgq$m|;wCcqh_iB$!3`P)*^6MiARY{Ks*A}e{0Oq{(^uBC zSA)<;X(*A6;GM}<-Ua#yN@xImOV_k}FRixuFaq-+K$T0z@|zD}R2K-Q%0{+>Putb2 zJZT7LRDd(DgC(ZN$cQyTPYTrG8K>T2Fk`zu{~fRl?YFC#Uw|2FnMM%HQ1HPcBCDCL zfFEcOG(>3x)3Mmp7P*d5ad-0(TW53K4O2hVcZ3oQS|SCf#C6piaY8}U-P$+eW;zEp zF$6l?;#H0mG-2onVqyligYizv{tL8-Sb*RPX6VB%mPw5}*y4M?00(mXlH)}?un@__ zWKu8#ncLyB0pRvs&b1Ihpd1iz0J`>k(ORzq^W&h63AMqXF)e`I{338J8OUhtwHFP1 zn{yfVW<*>#&F0-Ol^5Fwa~Kz=uY^E+@N=*~J@rFypofe0JIGOWz+3@RRU2!TAvmX8 z2l5ol5rn(z9(XU0Gh#C3ufGu%I+AVeA*#n`tfeFt~y){C2iewlNC;Z;%6L*{1B<>R5IcU_RGp!p!{iLleag&hQAL zApE-DdrAAMOHL0)yD2M~5KT-%*8?dJm9c-_Oq zvk$X`vOwjaNCrLnkW;MyoyJIXd#7g@vxI4F@=B||Q`O1WVSuc9o8GLCgPuR zMhIwSvJ5$UGutA?B;1*u1epBnRq%5^mys%rmiCFs8SC(#;^M8*=AveLv-_B5%B~sK zzS(^K!Aj00j%hFGhVY+`1rNOX5IBn=nRTlD&NE~g{j&e%i{QYqifd*b20Ye+*<>&V zf&;wbC*SfYqH_hTnp&nbaG580>!~WQIvH!vF@~yrYaBA%ZEI7uq(P`tLOXdcT_u}J z$_u~3CN2(8w?b%#oKG*Pvk`2;aOP>QO<>LBSY!}v_e#KrDwz14B`g9!)rEUB^rix^ zuyA(8_@n=G>)nt5O9}|fIoe&`Vlx(~<>=#j_CLTw1+xLVdvs?07JCyjh6{-SsdkwQ z^)wn+^pT4FvTlIFKC!maeA)zk4-}*v4b`3vTELP#;#9&64ad6G1zL6ylbr>?5!{?) z3n}1LfP4`iEy1FIAA&+Mz&z+@+w4Z#17ji8*E~4u zSiZ|P4+XFQozcEG&njy(@K$8bx?C@+PJ!U%y;OGalyuSdrCVGe{6Ijh2oDjQNH~a2 zVmQJW*cOm82;gpuN?In14V)QT1mfiYXEqLRP%Fb(yV2u2JXmKc1ka_+OxO$A3w=PP zpW z+0&jHa;)!d(sUCPcTk~jC}>$`CEByl|Mbu;)ae#>`lCZIAPf`&+H2uDxQuM}lNO%j zy=TF0!kIf@C|~D}%6VS;9uvAIim6C3-UjXd+jtvkNB{+jzsItYr^ym3^T+Ro}k$Mve9;d6h-7 zpXqIzc=gG{bN4!6Q`S5uAAl6n^orD6SA0mFhlF zHlM(>$Yh9E0rNF3qpAs0_+P#~DKFk_zk>yJ8V#zmDu=anae6o0$(7H)2;wAGRZ=JbKe zMyO>YvJ>m>FHhbo18|FaKIh(%mmSJpc4w5R0~>VQr08tEpT6d}lsK6b2ep1bUF=h; z&qiN)+Q_8~YNpgZpdw8S+$%>^duCf2OE`~BgF&>`{x9|)-}}kz8b-i&`2d?I8}RD! zxY!tb3sG<475bQoTMEkH(E?`9s;54L^1&~QvStltTVAuhq1IjufSG5uFTO&?ZlXig zfQ+Y9?#pP1a~@JX#R$EXwGDWH*29lNAvWn|;K2IfHYTYG=LA&N%;g8bx-+)H0k+;K0d48?vFZ8Er4q?ZqIVWgP6or^~lIKHw~YkG~C;<}=6A+}s)o5UbApbZymT z;LTf-N?0QTdkEzUXpB1W=C5LzU4w~Wg>#lkn-*e^;*CI{<{2iVn=j~{1H1Alv`%N) z4V(w?EG~4kj?mOAztRVr&APhJ1p#ZErV1dB=IH_LMY&3i$?-zIbgF6n^q$&ICL0GJ zE(AyrvA=aB3OGYyCV$8da*JFHV@G@IA+X{DFqJ;gfbCD7D-uoTF}lXU=@?Tl@lGBV z$5R0*A}T~gi17IEw;#SZtMsDDlOHgGfq$XX{=PQ2k{4ug$<{0(q{uWg5}L*adAc-~^}!XwN! zU|&SRnl~%hi8(Gvb%11m0JZNd`(+oe{eND6RkrYaZ!=8+Q$Av#{h?1q1#KaaZK28Z zyXRTF?$G`rIKZ3lzBdyjAQ`Lyq=P{Zzxv#+Zh*p0?UW~$dR3BhGCVq@^5Jt}JW@5d zw*KiQjCh>(E<|mXdc9(&T>Q~*ny~F?l!2%#0$W=L6VRpFCpvWtlyQ|2eRMq#w3rV5 z(bHgdpwLt>z?R8+?zVd4rQh?^+E1u>20^o9cM>ptwwbo{2^$Nbef~zBLo7_WV6N@c z7sLRb0jfz(;RS(4P-seZ9No9Zw#BMc4oFcq{F(M?7z5Y;?dL3sy&!*eJq)xzzCi*# zZ)=%uadK=Y706O5m;vS(Ip1M99IXhnoHTvYP3Ill~gh)fvVjd8ZiJ?K>hJx zT=^6Tn7u(PxMSEGlL;QTG9a6hAm{^687eo@qq(M;bz^8s)`~q0m8aw=24*R5AVNmq z6+8mi0tYmLa(Zw+PhZDB1%pl{vtmZV~z}^y*CrrBBYjz&O@{*x^sU zAd*&s0R!ZIz_8YXEMvhY3;*Yt=O1<1E@5Pg2CN+yir>1GC8F+P+4L=s)#0OS4s;Pf zX99nJ^ffjM+lWGN?E%OC>e!P!`Z*`*j*CL2&A!c;}I zaWODia|S>MsFx0QVe%({D69VR>u*2^uKiAAp~bt}FHpNb>;++pSL9$uVxjVMsI6Mb77!Rehwnl&ens&U=YK22OR z+SUt0wmT2(oV4y`N0<%-TadUy6L_;Tuu@FjMKN{@7CD|TRZat zKs0D}$OC+ELM*9y+MjXgz0^)H*07F9>!TJ7VunHuE|3;Oap0Tr=|i7>XAD}{aU0{_ zS7Ys+3>mwQ>EfGMfz#2)7y=tCOwGtX1TgMV8`C!I56L-V)A`VsuZlimtJ|QIm=+ti zz~U0H6hfxJLL-=(r@>$te7NKkhDX4HHP=3fshd82X%>1F>j20DO>tS?G6rV~W`P_) zjG2H%r-S)n3UEztl5D}Ep|D3T@jV${2VmdlJb;t$bXP(j7L#zE%vv`vg9MhfIoj`- z9qJY+YAV?UbadnL8vUW;@^^A(4_S~b6D}oqA8ZD72Es)Mpa0-qQ9M^7wBIF>(L|W`0|heBYF}lgi&gv!AS22j*hotPJU*P6Fufi?3Pch)wD?M+cnsH^)dD* zfJrK_SigD;54T?dOJcAE-L6q7e|_ne0E||wz*_ju6|0^_$2Kh}f=J&C0@r>OLbpTV zxwv_7N34B2ObK=}9p<^BYJbM4WpmT?l z2efaaKxWABC>?S}d%|ghSVDOdqGKxq)vS(JLQZbDFtc8pK+Mn-5SPyuCm3KF?k&l{ z`^p=>(AHHZw#Q*Gs67j7}{YcHD~X}r$zQtyJWGzBNWmLwy!s$}{< zdQjhKIw3PLusbBHf&K*8sZ>Bym7MFRv0Ao+H+YW}Wy@$-1oL>dS9G5})=?3N_eH=6 zVoPb-ZJmMWDF*|})r6o<)_IO|uXf)yaBp^*mG}|*oj#BK;x1M#GhnV2j0*thq7lkW zh8jo(nYB_N{x(*L|CbA9WO(0RgL->%5FXrK#-IpQgui+Fh@ToZop9ixCi&t zK4Aw*7qEhML1oOi0TAUrsGg1Ht^2^VAIRmbjc#D>oMeV_Brld_2gc0#^d2z5CANrq zPzR@*+N*Mglk%l^9p>I*K^>g`sbd|a4`avtwFI?mU$tik?Od_3EHXjhN^?t_nADA6Im@Pc`DdRcxxNvzHAKj zIr+R2vqSs8HKh7Ypmwgf7X(~=ZB7*7|779V5IDtJ0@B!Ibd2P?xtL+Zl?qB6ArCl% zoq9&@CTwn+Qiqkv7+t!bRX923&&&coc|)X8gwJGl3bX zx@dH{PbJ+HUn{KadVm85VGGe|yXT)i%Kfiz-7x`!UAJ3Q2Xio9b`ZV>(ajzW9H_jK z5xvZn081BN31Ha^)SZm86{M(O$f!nya;xg%>ILT)cTY1X1*+f=F-p*lqs>hE`j37G z9%{|x;(eWcnf*!-DdKP6|LM4B!`C28Sn) zuYoRDV?dkyKYJ;wtXnMEJ3h?um3Zyhv)n57?_a=n>f44G9fg36fv$g_cR(FRUEAOEuaZH9 zM?G6F{ba}l<25T282#~OTu){wJuZJ zq2sULYXLqLt^G%%QdKl8j9sC+Zx0kR4|g}g^4Wx$PdjuJ)7+Gg%&S$93Olrt5*FSP z2pAk-4`L=WQ~)Pdm9q$5$MW4}yA1^i9IH8*O3;~SU3qEi0N%foK~r7$J13r!8^p#8 zu$cmfpv|G=dWh(u9_Jmg9*fYqHCGLAwm@7-5|3lhdc>kigBgD2S76$Glb$aDsY^i% z2MIjesyy2`@fr|wgVbKb~V~Fe&$=GsfYW`vs-j**iUi+9VbOYT3&@;fb=dL{k!2$vTK-H<_ zhvp+osT*PiFJfLt)Te{9g93lP^Le9%!HLWB^k*({58&L_M1reJxD-R@{DJI^U{N+4 zA&b+(hhwylVTqA>1KiMC`0nU}rECDV>8myT<+UF`TV%1iWeqPtM1TOUwXwTzILhvw z7tC1f2O#do)Q$!XF^!p$>q%fhg@}YbE7z&UK^ZXI;~?G`fe&u|=zC6_02_f0-*lck zE}FFr7)6Y+ewk7`5xexsJe}nL5-{fl?YahqALO;O=IN~`4#AHZjgMad>o6W2a~&lr z(olf|6~bib^uI@wdPOs#?x2NgEW3kSuq9~kn{goKh9G(lVCId}J1UhKs$-Bq#iUO+ zKsn^zz=prOJEzADQi5H;JyghoI#t5Jbm3935#^&V-;jM5yG3q7 zurmN|Zmz}6SAWg?`0OWQ0(Sw`_3}pL7NZ-r$FE`Z;Ul_9HSQ%C%<{(q#qJBi3yrnX zODgZ7UHuK1jyiQ4Y~o~CVuv6m!VzdMJU-S433Q4OPA980gcceAwvsEGzOeS^n^8t<$D_MJ%`aY3m+J& zJOpok`z{%N_AN-(05I-67p&62g%`6P4Dt@*-ZhJ0-TS-&#eteYiJYxHQ*FU@s>XUa zuamcjRPQo#`~4bdK$S<^nTIw+Zxq2qr#ts$sB=4C=T!_gRHswveDwjkvL>pVW2fU# z^_TR_Nvk@uJpEWFP7XK@Q4*p>)DjVAAITAgoux?BEfL^bp_F;Om zrP5sc`)FxU3Pz_fi~;A_sxm~jji?URWhUcZD@YzX+sfS14l*^3!DIs+ZVyt&EgGOt zUcF^@jEaDNZG~28w5+1b1K)fsB?R*D@L0CrQ2k>1;Bm6J4&^2hPv{6J?M{F)t z>D32L-8;z`9)zoyjLWtOj|s;vs0EB)cBPsJ^wc0OEe+%w4#wmInC+d6#f~>;!xpnh z7Xd9GH&7DDy&i0F1`K%Xg%9r&Z3&_;i+L659}R9~i>u)37@PQ1?N^JT0dee@oT_>4 zTX%{HTsfj_TPzI_J8wNkPXlf$FgDP64HZjh+85a%0S`_O=sjW;0*2fo@F?z_Kv%BO z1;rr1IaQ?2Gjt6~505F#zraKm_(6R91`py@RPS=^y!g6BQm##F-E^1gaM-A>{=s9= z3$?F3+d>OsO98e(Ob?YJU)hIhZPox-vFb`#zbueRM*~-p5 z3N`{t00GLW*)l4Kg5=G+0aPG1I!eGcC^-3ql4%?4L$XcPkPNtkssS-rs`itVSjtuW z*UJxp$AEZM#z?S#^7rK=i9Lh?aWKihja8z zqD=X;rEPEyOru>oz#&4mL>{QDrI+NX-6xg%S- zduJVZeIgiepggxa5#!UKUf*b+kz+p<{MXBmwKAu3$84-2=0S9T4u*%x#pMIs)m+rB z3wJWZ1Vs?5+(@OZs|h>>Bq(5lX`e7Qsyq$p;@v4IK(?DfFlzwyYXp_rA4A0i{@IV$ zoQ6;C@VfP1Da>Mj**FAYKJxbC$`6r+VveSt^F$ZU&tM%1*4Vn4>Ockz{`f%E zzyVhr1On`w!03xW&@||;zxzp_5%TgYU~a>m-~dy3Thu9)G8knsed*g2TT=qbUgk$? zcNw}Cut6O10suP*z&7J3Dmc%xq&*<>09q--K$G?tufDxu-eXkGx>vCk0nUSEDJm8@ zt;5WA9Zoz}<(!4;V5a}+d-MPKtf!G z0c)7&D+CD=OCb|0Yh+Z#gs{_|NA>Y@md!;>ZC}}R^7|*@BO{<|F0KS;wSgVhZs38iw2QId0oT zKiLI5)wdp8`-W&qzF4Ea{4SJ38Gx6wF`RsT%~UWjvW^FReOJ^C0wcj>6sw5I%GU^s zV$^x1-6pF*Fs{@-40-M`eTyh`KrgF0ujHmJ@u$bUo@%0Ep53$9hQ`w}Uw{e`;4zG7 zuXCVo_*cq+Y-nL@06S)tKzm%1X-3ho=E|0BW<;)Xe6i_{s=WvI&mKo)7;}B+hfHvb z(LcZRw47uVq8Fqbn{KUq7~kw*2XmOpRf|E{9o$Zk(mzkIkNfl{y-1rupKXM z8p^_XGXpQ}m(NA=I-kr0WLOnakL8YcXUIXz6(1+BYFD-51Z-J{KRD zSj}=9Ok5oZR=!FLGsP(;8N0A-fE89>VPDM2->$_f+zaBp9~!|~%G&=%1ABI3p(h_$ zgR7c@Cg_CV!~sJ;V^vcpBUUh1e))S|WhfOkn9AA6j~|t-r+HT9bXOGnhv4(yLTc?~ zPIbgY%NW_j8~rCQf*nU3SF>R0ya6B1R7WoR2fz~Vqm$!6_0~1z5DWrP%X;MD-b@wC zoDOhj&=3f)?_j3@Sm<8N)QC!+gih9BaD=+t&X|X(Tq@c^G zqyQQXJ?e$QTpH9a~+<#L5Rc3@dD zFMOHkhNgSnvDe5PCXXotDIhI_n83Skd5{FJ_*bWa}XWM zU@|sy@w|k9p39s}{eND3{Kr{YmKY3mI8*cmgJrNV|KA_~=n;q{5YTTSl9Mrt%KybI z_t8GG%UdAe4cbeg^XO8h(jaJ&MLj&=3l0>7jcfkS9mik<|0BW^<~+hemku$UmR*E5 zOz9&nNfYuhE)v^2zjn=CNDgpdP+oCX8u4Gv7Vc;|WF3CF1;(JIm0#()yXU7r(AOSX4F24A})d98}aWQI`~? zo1pY9JIq7uf#|Lz|MB;K1(^i`XlB^mkLhAwE&v4Zm{}{)!MRA!j7=Hy%IOR*z_K#U z7lUq46T?{qQtu>jWdN(shUd@V!NnzDZ@mQeudu2*yS_a%l8qrv#-r@D!1ddCA{|`O zHXY2S$VElZf@u%yoYS7jfe{e)$5-chdfROobAiAY4>qyvyMtty8N<5`kfVJny#ku5 z8)!}jH4_Gf{^HeuP?wZcnwZ$?w7w;2gNFX0Q0wC`*+=J`u7`}+ym-cg3%4*yO|J+0 z_ahc%m0%Xc;bbOVQ{(=@U?Yp-}X()lacU# zh*eQjAQ&QdTZ0$^t2~&iAH8(kBL>%xZrezPIvf(~w*%>v!say~8^N20z=}bCe3{x` zUwMURqN^&3nYer>1!AHs5S;ssfCMQ+k-GlLbIWVV>o#2W;mmpq^OGxU1*w+Ed^7N`}_@_G_Q#Z5^OSobTA?tG8%+nz5xj$SY5rJ-)B0uTI||0TA`L+{5kkS z`xMu}G4HZEMxh;pnB4+4I^Y0Lm$a@dgEm+PCVy1&Y-Bif&{IG>lh;%L@ooS3!Oz%* z=DUsB0|)5CeGm}n<`4@(_p&CIK*0IOMP9#4i)Hu;=ZoHFVtyVr@NK-TC}5ssx?J2G6S8xPE#A z!`G7(tcp{%Xit$XO6~vjMU2n?jLnFz=elGoc(|^yCUwjWXpdp^qz4;xYlvxY*eW-D z9P^k4)@)QjGbo*jmPs$|*|VaUoq$V_rzk~#dgL)^YLdQPTwyM#qTG}_84;!;lB0GZ zn0qcV1XmrIu}j_>=UL)Gwo%Mdj(}j$JKqAMYgxT?IIEAD&vZJCRDIC16Q=7>)Y_)o zP8%0a4|10rhXPoM7oM@+Rp((g>*xs2rXSUr)QMJ^diBhMK9H)xOW&cJdN#Q`c?f}k$YmkA78 zM|ZaK{v4D%kSVBY$!6LQc&X#DL;w4CRdODaVP@>2ru=hehyXW`huWgn+M)6f)<6H` zM@vVLj5WK%_ zidDw$7e6wG60D+`2%Q%LQj$b^iwJ7&qvN_|l&4P0a#(opHo6tyedO}3U`zwH8O}*% zF=78qRO&K$u$aAO(L*uGK7+S%xr>;U7;OYfm-UT@UYS&Fr2Dmat;Xte&Ly+fx zmMNnp>juWenJZ-TKl^j9o^{zY3SpQ6&{%H5wNxtDV!^DU7YW`_5xk} z)_1*4{WZU3@8ky-^&|f4!Jn5^T45NtaPEym_I_Y;Uf(VX3td$>G_HcKVf8k+v3Sp` zrMF#v45RMr-_xGNUhQD%&HBj#O*T%n??cYUf&r@fYGlMHC9_8CWh|UC=hS9F(>%Ck z*Te81ciqPf=1dp83#N0J!(4M1NJj)TdN$WOTWkzRQr!;tzrFe!F*Q{5(a8D%uK#kG zli*omvs5ZEel1>LWx@Lyr94o%q-}ktZWxGUp=q}pNCsvs$d>uVq4fK?ITA)qvM>RqM= zmllv*39dtX@|=tuxiTN0v2)y7;w#3gbdlhfel03j$%3{w-M)<7GfQTiM!O$NfvtjF zZq)&75+v~CdU{LxYDRU_&hBB$h+ANe|DbL6U=+ctEsdS} zY@C?c>khy8jtw)ZptDf7HsnKWGTnhdW?=6z z!vWa$Scnq9JCq=Ur7)#sCX`UnkZb@~Ct>T!YF*E}`mD>Y@^>{%eq1-}TM(aU${3!G zy@dtGJWsVMWs7nL0U^~DO)7&uKl_E3{}ya(AY1N!?KUt#1sEEapdPFiB_QHcQ?udn~Hzz-*eW3i*U*&dX7nUHMH4oZn1p|}+`O=*bj4@ElK(K}E&CYp3 z^e)9WK$L*Sj=@-3ZY3<9odGZb7s+3g7;}P>s13qb=G#w+RoxCOY^UU9RTBP1tZTtc5Ty1f!XzkTy8Zeywa%=29V!PuzK_*6`L{Q|En8gQ zRl77}tenfRdnEE=`^h{9yN-@M#>H9t7I5jxrfwFce-OXY;R?O`*oSp4k>fj9@GA?(>BeXCjMGB%FMMGL3A@Sf^p8e^nLIJSJeVc@8<0D-5hK>jZk*X@^S4= zCgyjorEJX#_vJJMT%A)(c{P)-AYM0fhX9t$UQB{Vwhxp=X0F{Z80SZ4e-*5F#x*PM z10jk=U2ImM zu8nPgkAOPaowPTKBV#(Dnrn|Eqib}?`@Ozp$~&;Pd&X`gd!M;qc3}+&ST5`r34eR| zaowmZv%M{xO))!N|6xl4#DMnM64t1WSka83ZBRI)tss?cs$^xmClf#r%Dw0!5|D85 z2c-~PG838kC@^Kx5k9}BeJAXgiTjmLuknJJKFYy77vroZ22IC-_Wv?xm!x7Y%pwr% zU|qJZ2aM{wwLoK|XTt}W-5VWNe}CfEsA97?v1Oh8`LdSr>Gru|0jE2i8TG+;e<{Yr z7|2-=!zVBou~Cx(g>f4cgU;ER7D3^<<>;`xx!~N_GhmnlxiDn(;UUPWjIf8YJh)0-;13kT2rB7hU{5IdJS%_9BNOD~^)SZvuM zgtIPz%KpVb`iX~5&KTAV@862IOtoT!B@4ui0=Cb+EXonY^2uKTyy$CyI|6heYZx$8 zFmhoarX_F;VpeId$07Sc0_{0ZP(#*h#PHuwJQO<9#He#xz>M~Y{HS`~Oygo0??fvW z6_7Fznfk@AjDBNw7<_rh-=2Rmo%Jp_{{S0OiNgoCCPQxWZZ_@<#!3YGX52)nkMFQh z*Ji6z^-cFxo%>;^?33UPNfU7Kb};8*#%)#mX03{#Yz1fK#;Kjfn1UY>Wl2ncRNlx2 z;U4Sl`f&A7)OYUSy@7*r2kSO&XAgA*0d}LuriOsA_Gbael>*rgt|Ir}PrRs`pzN$< z?Vn`=16?B-p`Se0;Ns1Wn$qjY?3x>5uN`>*7vKWZ88QCwVf+96;A>Du_3a&e@dTJE z1_N`D$twx$ZR|@2pj3x=aKH%iI`;zj^#{Z5vh)lK{11_N|~vrb~&tqvZ5_D#Z2qxvf&d>c|%N)G~OyXEkS+fJNNCX zeqF-ly7sd5OxC7*yvZc#lihT#{ilS>mzUa%fUdl&?y zhyds8O(V~~tw|U<2h+jVZrBvQ6dpGXOPJdpI6GZqW`!3-9*DjNRJw2uj43_61p>-J zZuO-+!EB6bMO_QB4SWTZQo)?z$umF4(03RJvKaZo5fs}pp6&?&<+9yr&H*zcOIy5Sg2-m5X8{)>aHjj&XOcx%ms{! z^9^~_9{smFf2MDz(@#F7e)$m$m{h=&2=Iv|E!r#I!5H+ZxeuBeAX~+ymowMpfUK|> zC@mw7|MTnjXM6;`SAV!M9L~60 z{(Uhx{oz^*=Hk`>F2HXF;G3xNEO@ir)JokBWqbSVT}!78ys;gFc%sv|@LPSftZPu1 zrGXAHF1jrQn)@&qO>W7D;L*xo%u8SkbnT_JqGrn1#1@t_#QYU7ent5$;FSSA+hQwo zgRBr;=e&##-h(nZ{S@7l?dlHwqu0!wbrY1KD?CX1#MiJ9T8FFOF~kg`mbHBV?gRp& zRKqvO5S$HL#gZfDToK3yw7m=ll!2l_Bg_cE;55kpMKEOcARa=`&25e z>tklq69Ex>*B&rJQRk_y&>kG};nOgAC)n&WPCDE~Q*K~(yE?n7J9vtDgi4$-*(;(+ zKW?4}nlZ-g1j%A&@ab6mcSQTID^#|tfBMls*blm{{Qx2zWC#aKV2W3=7$UvFoE^b{ zuJ-htnu-B6DimYH5F~FzjcjXg-yg#h0A3(AymTw3SgThRD}s&{aFY)6v5+OEkkz0m z9sk!)?%mtt`aMIyKK>S%auGCU5DzE-rE;-4f>}i>d9|kntnn5gNM&X-n@^)GbhEU8 zS!&~It38rsk2lM*S(R!I*|Ia|yW9@eVLu4|`MqLJfp7&`fM^Fiqel1QK7SnA4HN5Q zy7`=YRd(t=U?KrL=>0cD2WLug_Df>16A>lC2O_?=S8GyN{$T8h-}StDo3(7?vN+Y5 znRma<3Qq#P^=mLw4x4jfzyavwZ$|M&ff(_srxAt$OFV@+1gJ_e%IKYEKX^QC_WVO! z9!>7dSr3Q&a#vG;Y9S+MMRbe#tmSwb#Sn>4OTR}%ew+V zM*}rsG}H4pp^^L1w=7cU$Y5Jv{DWFW=Q!l-=Q0PnGYTh|X%^bkMs`blpaW{PCqJ6$ z0TZWQ^3m&bNTVOvf>p&TgpO?Ym5cXaWKjsV2P#zY7>O<8u{xMKyZ`>RukmQNvHX3A z8)jcx`}U*0C;vsW#8!OH!z`A04wyc`m~o6@BdA2S^cX_TmDiavvvckCvO##zf>%2` z)7fw21Y|^rZM>!e3eUj>0rL|(b7d#LoPqJx2gM#8!iyO>Rlt7!$}b@jy4qFvFbFG2 z=2W?E_E~xfXey13TBG}cFWGn7Xat3JgZW;5ER+2{Ihd&dxSag=u!jy&jenL^TUF~B zrczIKy@_uyKX_4IKuz<)_vsF1A6HpA3^oq}_)@MO)C=;7&m1s90?I3!W4t1(99X*c znkt*3%ovPE=Z?Xa>JFIZvXhS6UMrPyY2VaNs z!E;%hX)zga<#|pQpc9yV0y`U;!)^_2q?A*4ybTjKI$_F$B@7O#koipMC%qZk>`XF|MZO z4~zDFuaEC`fXbDQFpri&f)WOb5w+kP4_&!cV3uOD*&399!I+s?Rv>tDI}M(8y;S@5 zN~QMeTo~W*8r}3Xnl@}I0VRXXW~y`dcs_pyv}RiSY#ubAleaY$O7_?J1+|=a?r1NX z?yQu(zys9%4=QgV@u0arFsGO%^D0QXZ4>lWeDGVbnL`wTSTdLI0n@%6yPh)_41FEs zFvf(jU=Fg{wZEbcgm1B==}RDyy|q(~2gD}4za&3TG)_h>2%6Q_VBM%}0Qc9md}wi? z5Z;4kJGpcnX6bp{agNDD_W_e2tE#+8hfjYQ9n)Z6F;vP-kz0eX^o0St=(_e)_58%#|ef-s$vTBr6Iebyv1l! zY{(15n{)Vhd4L;jDhr)%sD<-yFo{p&lUGiU7&{&2d+OMePdvNnn`#f0ubC!mouTUx z4IqY1pgohMoNY#j9c$H|yUnrRuxJI$|80G=%>-T5o&Y*xVa>8IHPz*vG;{V3)yd5C zgL?nJ_P&Ejvg%6nW%;V^>Mrknm#;4Gz3-~(^4@#zx~j{&tO5ZeAtXQ`+<=fkfB*?0 zi4h>zL;!-ubt41~bVDO)rPas;v$MNDV88F3n=i8(G^2@)*@%spitm_v?tS;&d+xdC zz0Asd=`>KPM*FKf4J!xP4VtwFQ&Gm0*}z`u??!gSiltLvr}zEAx2t$%GoY?SsWABL zh}A4&vy8FCYprTCa|AlRV$V&ihQ{|QRD$M$TzIgw#k2@zX#ou23HL$6%b-8jUPptsI}0h|lqxLmXi!I?1J09T%#5l?+RMSobSl)!)U+>LG<^PC0uAjMuX z_$J6cTrBhqeQyg6I=h^n9TPLB%!7bYhe>V_zuu5AF)V$2hba9{t-QqUT6>$3inqR0 zR0i#pFG!KmZ;J`rLie~kqwA$dlO}D=(3uo#l>^m$`OOMCX`gQ|jtVw?ZbHm=`(H$&c08qfzJ# zYUOSLYC(M<`&IgUiq6Y_`@7((N8j`}??VYtLr8FxVi8YO80}J$AZ73g|Fw3A+|!Kn zGiP?EwqrYzdpH0_kfzy1mO}z`^}qd89$yBx<7(u=amYbXH^=~I7|gg+I|y2v>iV5? z)hH!$(=4lSJl=^yDJS`-8_%i4^Wu{z`k!!AI|Kz=&=uT6K$l^kGDGFo03tvt&cvWL z0Sdh!Mp|mlG`L#y9XcII)PI|2FBEDlXQq1^^0+rwHe&X~urnvVUbY(qaV#oksd^uZ zAXr`E&PXwpn}{p>!PkZ}d>3}a$_R}3=%zp)DV})o2Lk*K&xc>0USDfUAY1*I)TzbOau1SC(RJRxaYffjUGTz(W<;J zl}*PW(_O%hiXj8y{KF=2rm_OiTmzWegueP$j^g)Txp!<}V*6OYT(&nV4KOSjW^*(8 zM?}KULZ6XZj!RH1BKxo3zu&si*Xcw9X`<=xNI-4yi3eBL`@i0Ba}P>ZAEvZz)8WU4 zqC$ru)oO3+(aWuHl9N!w%-t#L;*v3LSSLt?_~2&!Nqy}Z9-T{!*`POnyd$;8R0vPo z&u zg0o-&UVUC9^GqlwoGWFOeWd`-?MhFjml!5dcn02P4mB3=-pCft$J9QA|G8FK`ik#D zv#!JeN(a{Aqol$iNktYc10aAYs_|HT0z?dm>FKlB7ZZoijgtG?^b}I&D2y-{8qnY9GfJYQKs5Iqa*JgPqQ=9O69EqSK<&dev($lC9brBalo=Wndqy{d z4RE5PP05qo0fMbW2dm!~Rd@5MIht7it?FQX`f~<-4s^FOrnSgkDfUC;xy5lNykgdX z;!l9Rd=3n-&r&{=;%r^6)^YK%|8>WcPl(~_*F^^V`$N&XN5I*q0YA$ZykS`bb@22t z)=rP27{TeJtoL_v6?b`YvSV*)M_6|105cGD`OD|;kE}phg-K7S)D2WKBJ8}R#E<}7 zrki*6ctQp=FeJj3_i!J(zxy+>jm1F@e#A*dPFAk3aKo8FF>9>gs&E_kzxZSDd62-) z1G0_h87?kXA3CgHcrz(*CI-ybgJ3hWd(J&6&uF24l6z2!uit5yZ51Y_=HLGO9*C{D zBWXb#k_tHe=TFsM;CSec9FPi*L-EL@w)e@8z-Ju_-oF77U=XA^g7GB!!xhSl!~gl* z{r`II$%ppecnn5CPwv4km-GE6?ZN-_i>EtV>W#`yXxwj~i`M>O!@gr^mgn~I!b~5H zf~X6MAsm@5cc}4S7h>*@S5v9=6|4AegreR{uS5@l{01B2aA15C_>mLDEs$f_&S zh)YC$Z-zPrPkpxU&rd!DslST@$&jf|R!e$##z#;kXc6HD;i$Hg{x3JX3(@`Dg@+$RDiA4Y`hi%(^BF#f`t z!Mb_C$zY^yvHF>wckd9(pMUo4D5gFAJFbCV+_*anq2Pm34}Nl>Pa_$s)&SUIM%3!;N_ z4VglKF`g7C7a@F;*GL1Kh6aVv$eV#_!^nu_aCgEv$9I0>0bUf_lF?le2hAG21N|rp zQuH@HM8{7YmGI>H&jqTNYSzy6Pa$)Ko3p_Vs^u8rt(B1af14n;m=EY`ZmQy<|B&(X0{_a`4|uo9Si2anCSJ`&IDf_o?_h0_E`=^z8$->&$-ZO5Jt&RO?Qw*sa`hkYg!aqF zI3{42v#9q~*GK59Nm9`s&YtMh5m3#`_kq`e1U`laB{HaaKNOM^=Fx@W$J8=0Mg%pj zkX%cFH@yrnw8D7{0GQTyqPR(c2oY0pZK5PjK$feRFEOJqC>6q+lOcUUd%of1Vg>KX zd+!-3_>0iruTZ_i(wq-AeEDgq0`^Mp!UwClfU<)?og)`zQZjl*z`9cWpOD^%`BDUm0LTm%cSyq1Oz zbcztpu!?9mEDk+f*eWS&W02fRZS_aFVyhs=dwRP5Vd`P1YoNG{-#z@m!49-W`Z;tH z42TyIS>2K}1htaObRahHmzh;^)6aTFMjm6X^qS(Fuh5kPARyA2q37HU#uL*M$}Qe~ zT-R6FmVxI0qHmWNBL?hhpl;jcfNyStg&H+bwk8_E_1|M!@JWEm?ag}zfF9#4IiX{T zrF1=T62ut=tI-9CSvZ{t42O3a)=D0i5Dv!07yRx{6-^-xFr>P9bBiobOC@~-aM7AQ zsbq-3;8T{LmMVg25~Sj=k?wo%9Onp}0+~g%A#2K%#oRb!Ak$Gi{C$W{%a_&a1p%nMtA_gaO8q<>&}B>vXhT;L40OLF5C=h z;Gi*~=wI)8=1wsjbA(jeRxGm+IfpD_Z+iI_C>L&^Pl1|3kxIBWFckAjRES93_;JYh zACQ6;2D!^{dg*RIW){4VWN=r30B)(6KmHSkWj2e+mUBuMo(0oDbTZ(zG#GRK1+Exy zl;et`qXZ^4N=u{9nG5K4U}R*B_LkhLS8fmsxLP`+Y!7V^lyMepI3+euf7}mrPmqA4 z5V$j5w?~XJlu%G6$lTxIs&Ah%cm2WlFr}rk-gV^CNKfB#DYOG$ zg7aoEi!lo@@{uu=96$S%a&u zvzZaj`~h^Aw_E<-AO_){DjgcOJr06X%oK~n|P@9jdo}hmS-l$R8dKCuu0s2+*0t=}*otFn0n91bJL1v|b zsc4{|b_WNTiPkqtIOXFYphyI_LkweaKd?SS?U{iSD04troZBMkH;+A1?ExuR8{}1` zRtk)K4tYb09K!)aX;!W9keq295 z9nB_!6&`%Xqn$xt07~TE;&r(osC;ws?5LVC3=9k>(8YVffb%zZHz_xDWY($C)kq!q zdOMi1ZaTKuaS{UWVncO;@~JD#^vECY{CbrB=xH+w0U&w; z&|j~L+7+Awg@MiOC@^P2woiuaau*%2GiR$GlGHh*soG9kNm4xE8Ze26OBsBG$A2!` zGQ?a^aQQp74R*MsoaQMs}s*M<}TR6oFKNIZEv#1q?*2_T6@XZC+-)c(b zmB@e!@4)t4rJ;sH1x%FPjWe2xP_g1i{Zg|(c>o3d0o;2~daholuG`%cD|#xHrYb~G zVB9a7x!JhB0O#6#lCv?3L9Ginpj8l^eq3!V=8Wj9@=%|To`*K)47Oha#%o;VEEr&2!CI8!qIG{0DY(pY$2iZ2!yXY*Z{~qZUP*z`a#Nxj=XvSuih)N^P~Hq#5MKmzq_>1 ztp6a4Crs}>e6k|9ygwM~8$SS>{OzsZSbpQ%&{IGjJt`~YwI>{K9R!Oa8m%c{Fzy81UutbbAF36B<}}PfN2&or zefe9yzkCMLl*^4adFl2s2>o;Mlk&J}I*h}n1Q^PD3~=1(f|rBl>!>g&8(fya4KQp& zL2YdC&Qn>*c8OBw2GOPe4pVWkib+_im#nunaVQVlJoO~Z%p?oE`M5~o6<2QU_`@xX zbO8rhENay|=b&hxJZMd#GrU&Kc+gvadFBNU0?;4R93VP)8{N%GGUx?;>z4KWhptba zQ@0V5`>5)H=bsc(cupo0W~pBJAeI6a0aBnr2kOg#-#qQ zO_&lLioiEhMlRf7w1E-}+Ra}E*Iz;>tA#CHn|Cg<-=9CIm&O%p`P(w=n4o7s9?7GF zejol~p7|nQTIK5>mPkUO3B;}vqyoIe8?2FWTN=)EapikpF(9{;rXYy79|BuV>I{Vt z%(2$)bXMnq%>IHtI+Q*ND>N5FG^~I0 zg4Fm9GKRTCm%lzdC^m5Y3!`~bJnK0dr{-Mt>!&zCCK$hH{i!$+P#K-s;XIfFGr$P~ z#UQ?^&xE1EVs%KWz>gVPQ)#p1JHTRVhBy#aErro`I6UWrFBqjiJ^BRndX7>5&l6-c z57xiJ#9$8v9a*!U>L~usw7>}97bn$~W#JTxSH3RhGY}0`>T1gdOkUPVmPRXK{aimF zzcLD3f99wdf+voKId|(nb}I&kD>pT;A1soK%Jl@YxSb8`a$BiA>0ab$*tv=Tmmu3P z=61PlluGH7d+q74m%b^*iWH-f{`^TP@vOkd_v<%b&}u+xWYj$LHzYcs0>dJN^PFZ| z!ypPw;sAq7P%om!wWvFo&gV)1sUo69a6ywnd1fhtkBm%p(9{6x41D>n^iHE{5LzUt ze$g;h_wz!@-Oik$&e7E;^>oLdUU&-JewxGi^Rw4W#eS~Hfl^;lz?lmKd!F`CI5dH| z+feK}fYOph$f`3R}Q3+%`eZ=9WHvQ_+#W^Uz z6zIi={05f%pv0a(2i`=d0S)C~0bGbccF0l=GwZ^<3pmg_$GI^SQ%qA0TaXvNdj1CP z{FGlk9Tcp;safe1!f64VdDHxHo+qF#z}YNT;asgI5JQOl0QHw276n{w4Wm2=jbS!A zn87MuC%qb&d$@wkE(RJlMeP-FoZ|<$h0kzO-1;p^VCs4UtuPl%z=cxX6{#fA5~Li5 zsxCV>j>1D4*KIaJ*#-GeuW(2L?x_}@FB@cyS|*9&ZZMz_)Z4-p1USO2P_Vemx4TS2 z79}-?Ry#&F#xW$9)kKQ*-(-Mcu7!RTy!B|g>d^#4f*6-XOeTuV-HNv!Ag19=k(`U* z-ETt{N1Sl?ZyQfDtk)?q`)h~Y$OweRad?~-abI|o9 z%=+guAmeu1csgK;L0NeDPAP%2%vj{&vtPk-mdMQUqa23AG^=7jXTzj|^+6uXQV`zl zpaMsV@Bc6e%Fcz`z_~~)m_=q4Osmy9fIUBO|P~XV^)z84l5x@BW*g>Vy zgrO_wgc$tu*X}V4W<#@88hIJ;GoGrlbimcCzWQDIoP&wYua}$NR2DUCSY?Oy$a&81G}L-Y5wc~-~^H4!kQLW#sT{Q*gWbuSkR?AX=eGC zKjb2PF1pLJeS1+eozSgz?ADMp?4eJ0qre=FiADJ0DNhClW2Ata!W*Oxh4$V#F#TiP zkU;iM4p$f8Tm^W~2A{q336Ro>NJ#od&}?X;{QjPMwofFup%AC{6wJ_X$k4#;RL|C( zl)|tHUM0fas_f-Qe6}43*FPs%r&( zn+wV+8!MWF1b77m<`4V=E+B)H|NlCp&-XPFyPX4BccemzVm~!^-FMV z-a|~JVIYk+1KCdf9;YA$FFw>>QxSWdr*t-q^eC`U0G1fD&%4^;C?-rA@G5olbFVazQzN>Wi#cz~Vb8{4yn85)@E|DN) zG=gEeOaFRttI^|v2^VcP%)I>S-sa;p#On)T}?G4&jNaF);X*+YNl)!zh z=g{j+MvW-0A{dLI_M#sOMZOI=&2$%TSF0V$O^MRFL_~EDWZ~RKYn#JHP|g2%;)Z7z zUVc`pz#~E2JNstlzty0ThUzlL6L2>Eecq>l)~Wf|4Z8z6wO&1B?h@ z6T}@BVZWH-b@di8#<_?DKgrnQ)yfvjQrHsV(>7sLDz`Yf@lpazi}>Rg9_F^^J#L0r zRHEZF4%7^o04icRAo`%|=pZJP}pB&nNVj!+`YX*f`G4$eXNscdz&GYNfN~H}N0pv!aA8A%haI<7N)p&6 zw@6LF$9Jjx$zagmtPO+`76)b^bsMVw{i@=4G4{rDb5u%UxLqW8KZt3LVblEQZ#)m7 zb`&&#i2}ItH2nj(m7(I9C$(H1}TIb_X3pvp#H5@vcAqoV8Ox2^{gP?f*L3`YsvmmuYpaFjR zPX$29kuo^Lu3xQe4tFJxLk9r6pjCFn_0Q!M2MALV>e-47HZi#|^623d4=`KM+3I=# zt+d^*W{uGZr1^v~)H9Q7!8wR*%d<}xFFhMf2Ljx0oEEUz;K-mk%hm5%U?&CDIo60m z87N}3sJQ#%E#Hfvk2$uI>SS6uYu7L^fpQI3oa>VTVs7nD%XsVS)ldX`M{2|SLr=OKRT^_^te-m)O`9Kh7VU5*yd!oB2iKXuLGUf3g=`wEg*pt z;D8J?aAOz+9rMt!}Vq8&F!CJGbls`TE)k2u{ zFMUgEaC)JqpNXJ?Q%Y3xum%QzoU7byyX|-uQ2XOI*!CeUD zJ+T>Ftsu%u8#byKina(wp)4SPo0L6!fPVMLZQyFt4>mkV=W!V?}Jj*aAX9+;}Gjq-^j|{o_>Jq_*I4eR|ykB@jm=-uNj5qYo%! zlrvn?*ds8Md)huFTy&Q>HE*7&oL5)1P^y|amdo_cO<)@gmyo=LQU=R#HJAnxSf}Zq z#ci+y+LU|Pa%o|GN>2up=OpLa3(^=f`p?dLdj>)UmOy{J?NKQVRY$$`5Y)GBkTN)D zmiW?rQqE@v8Z%YD$+f7A$max1nNp{X4xQ#kfZ_L0ZDZGXW^@YME}G) zs;5DnAg)&_h^YZ!;GJp~J&KFua*DY_e}q+9?jqFjGyG4TJGA+Os8tBT2lsP6f)x=h zyv|rR;>zo{411hWM_l+a>NdH!-qP6M`u%bSikXHkQNpWUR>~CMkqv^I_ z&<5y@=fQx5Y_LHPm(Pwh0R0Li(lgvc&HC>JEF3@DKCrUuBb6mH4ZK}(N?~{c#h~&I zFqV03Wtc^Vb#X`TfUW_r9yt?W^g*!QDNX`7N(c5ghh%;9_^I@!y;EU*rB#exyj6(@ zpoI7a)MRrXaVG;j9iXBUn*JN+ViX=OqIa` zQc(q@S@d4+1h42eDUB@5zx)`_UP+$<8E@Sv3OKj#f|O3v?c9ODDXt@sF~Y#i=S~{s zGG>8F7&CSBKG%nX7*-woLI>zf!Tb;dkE_K|GS{nX1hk_eU;xJ|a6AHOQlnNiIrM;F zikmCO?D6US*O}b(|HbgmID}E-3A*$Q*flsPnqoqX3(QT8;o1Q)zA+x01rQms1pf9H z!G=Mt?)uMjYEZd1t+PNO#(E#X;E&9$0yj{>61{S=n(YSHj2iB}WDo<3SK`V5UZDgf z5H_DX$+Fk)TGuCIrJ6%JTc-S#D>hpmk#Adq{@OV(rVOB{gsBJMl5k9C^=G&>bMPTt zvb8WU0Ug661S~V`06dnCZs6AR;+rHG_>~O0A!GF9@RiU7k zg%=ktGIIk@tX!480$rI!yK-*~gKnL-bMYNq5rO0WhOSP)i6yA;1Qq+y;)uu>*A>`K?Ba z187a4*YB-3vTs;DSAO&D@3b4mAqS|nIqyH9ZUBr5{r_`0Ioz+DOR-dBt3X0Y^`W=$ zky<*@iCzv~^KjW_{@Mc7Y8%`=h^1Q)J3(=b~7-vZmacMgGb#Lb|KUt95Axb$QoRF#Pvq1L?pa2*#m zEqDSP;I0#dpHT~cTm zJ`xIE0pc#ms$;axatr{2X6pA3-zFvDRjn7kqT*B!!$Yo=lradTgCdr8R}&$l7~*pbd;M{ukieTag7uBpWpFV7*At`QyHXHd z7gqT{pMCD1HV(q1JjzQRz!f1iR3!Fu{Q#?{4&23}G6ubo2V%T1eqBp@vmy3dxo*r% zz|d=VX!Mh7@CbNG%2cS+%mTEt`Zx2S00(Em+C=Bv7%40k7PP29M&bDWU%vEnNCR}! z@glJ1pi^u8kd~jN^@6S(Zp~4g%O(@yBsF?&3}hf7qrWq@H0$ke@tOu?iD0)|Aj|92 z9O!g-NCVg;H>d$NJ$>mX;O_aJ%&UTi4;oE z1&NVhPMx2J2k6?F(m6%eXZV`{FZP09*X_5sp?+ZD=aS#?38r zY8#`9s|$3cx)!w1y^Qn5VF$*{8Ys;UjJ_$Kr;7y#7axGaNwvtoRufb-!t&~m>D9B> zW=Yss`2sh2h7=e(LoE!Zl2owsH*!N%g7!fI?$$_$ZEzn@Uy!v%xv3zZqhN!BX$Pv! zs`Dx69-tbI-KPcYgXh53F5Uz#m{ty!Qb{)hgS6jV?b8XlQOdyp$7aodQ> z*HI<-L8v>;_+TcTT?GQTPn}=-MtwfyikO6RpK`|@3Qzn>5=`ZcmdKL_4l#dt`k4vn z02d%id6&-Wwomp->Q}oUUVniHtIaDSlmt8hoek7a?pw#OJ8AK~k$1&=U+lq@fpDJgl&@BZV?i`(Giy(CnY@QooQuEP|PS`eaC`E}W zj@Bg&j0QlDZ^%|t$}m2r)5&$swticYE(AQ7Sv*yb+_t&ir~0J|{7|f4yCQ$!T<2Rpys^4ZY!u>s)nd#H_zzk^RvCt=-3&88V z46~tR4w>;`-T;&>@9P^?P^?|4c-l2f^^R>TW-!3NC6M<>J$7+Yc7Wg}KNcfp@DPrG zYeV8p0X73%Su7DzgoC8T#38q9XDNOY0uJ7eLAtur$d^nNw_gEgdhJw2P zfd;6nM+nsUIIiQon_3l~N_iL>cNtv=&=)Qs71xXsF47qgJ@nr7*?O{s3hd;LPWdwL z1)Mp-sT;WTBSYONt#MOP1{+r?14M@kI9U}Ut&JQ6SC5;imb=z<%kbb)s=jx??9wHdTVUKY;tb>5EK2LDQ zKMk%gJ>l)9zRZ`?2LqrfcNR<0;NhYjx~&W!5~E#3#iF_}RP8y#RuKtC%4Cu3E>3#8VS~~gU|Zsn3wAV(UI1QtRDVaL z)TsLCI@2Huybx4@ctgMt07f)_QNUYN&J+WuRiK$^KTapNxN_%(Sbl6Eq(jYz?GBEB zd1R~PVe-*8W=@of+I&0uv!BXFS zm`AGE4)1^tM1!`cdDMrGZ8M^hPlmBC0iDZHwt_mv6r;wczZw|k9MwpFc|GF{P(wK2 zb&d%qfX=D`8AwPSg}x86+YLKrm_2!ZH7#gCMg^4fKbYm+L^o)mm%V-#ry>lSM_rrf z?1R@sfTwd8_5CN0n4!IMqn*`V@$TGX0_%HYUHS)_LYsSxx?_`~7D9xC8|HjoGJJi* zCT2IGR5M92;zE*Fy389*DsEr^xc0nSGuIyn0Zsaoo>W0h9$T4{{S|C8)Ca~5*{Mcr z=gt0jr;!9=h7u$+L8c7*r=O}81FungTq3SnWozJ!Q~jXSe3pm8#JvNkb&TX0Qb5YD z3;Np&u`s9*DC8>UF+b_Q?cTy=(Z3{@2_<^O54?sW1S*)x04EU(@saWj@etqG#YWh; zoq^YX4CXDm@K^4ONwh6PXQJBH7~ORtoFPyX6ZOHJqB4g9ye0y}=btFae&ulvNRYr# zK4ld3Pn=3AVq~oBrR$+H_mH}sLKY!nd@8}YC%9J(Bzt;wzx$6y(N_8W-QPoz5tvHn z^U!JT9!Ac9ibnbt;Qdqq16!bif4iH*Zw9?|8xL9orK-52@rPimyaIQZ=9nSEK<_+! zWPRAoAQxCZdubS;SWjeF2MRy=Mxd0ge%Wn~=jDlTInSVnd+WgD9WkGVIOqZ`1~NK( z3!Fy){v#r}An9f`R?hXqTeuH^0}QN|c(oUU?3CCByZ1M8|te^i1ZLFM$Gp3qEt&;3#!o_Z!hxRo`tDK-#q>wvDy{BCzO@$YU$jx3gtfk&i z&n&IaagPj_a*~dLoqY^HAdN?qmV)6yC1PT9`F|#=etC{Q09+??j^+hL_e8Zz8NBer zyG2)lX;eUI?J%WExsKbOz??2$VyA3Jm6;aAsYf#bOpF9WF5j>=pw*;kJ&}VU6Ox6q97B6T< zyL4!08+j;g1CwSyh=v?6KgMR3{zxJyfV=**Y0-t7AmTX#?oMD}#37VJ^#WDXg-Hkg z>sMLhki~7I9Jck&E#CVf`!lcz)Zr+s$8aWUQjDY}55`7-)sZN%E5YofJCEfK>hs-a z0!eew-_wuaLIN}KzkBdI%Eu@wf6k}Iun(pJ;LUG|j7n#q+08QH0dem#hx39s21Rg%0FCPf99_>hF&P2$j{ydjEe2M);yDD`IThd(XdW~iCuK(4p~K#P zOe(;)OtIh&W{(eVVykFJqsCseHp8HQ{fqVfpu$2s^R!XLb=+mHZ4gt7Q6q{YN5XYC zFd`bAYz8^5(pf+OXes5@XCdCX4lIwkX#b5bf|v5%gZXRc0>_`gIOMez;D}197ntdR zx#eqL$v8`eaE{Y}iX6r{pgdhec>Rt@#hDtcIx=;f$p=%PQ_oQ&|K0UJ z;~WM4YUGAN6ngS3NKbC`eG1Re_t9P8f<6c}FYey5$g3|TM<4X6bt>TifM^kUJw16D z8R~Kc(`p3RtIvRa`rRLL7DqrsAOZJB4aoADA!oH-e)cdX)pGQCYLTG=HLGCswEh7K z$mn4(peVam3leotGpSn-lm|g0np!=nFWeC6aGB#0a7XH2-l4yNsut};yQpQBM>S=$ zjoscchz#urW8aA^`)Q**k!jDNQ7s0S!0A`KGJe0Vr&(x8DfGm*y zPI#;SP%o6ChN8e%yupBdN9LmOZAKNHWkBVXP8gO9Bb51YU;TA(L^%pPzot3z#De#K z)(@pn&7QmW8ff}I=A$<^H+LOSwQOou(S1!j6whkR$@FNURC2v?9bKgG^0`b=Y&m1# z#V?9wSM#7BavfC^+qQ$A?iAW;nABvYtZ9Bw7(rIob~BmM56m&cSH56Po&%<^oaC&o z(^cnRK-FKKXTUSe-@J#tgq#Ut)A%m;SQX$X6*Mv=GFsxn1*&esFtn*~Gz_Z@O8qBi zxcp^}MgqVK5d$L_)E~veMnD38rznPze>xb$A!igUSbwm({%f8NEJlLS2YsM^r=nzzP|)!4 zGxgB2Q~KC#vd0pej!PYQ8EDc=ii`xofz#Y+(foQ1Baa`ZhX9^SFlT7$>V-157*E;_ zGBxESEJRte2=_(tzufXD1TX+%OW^Fz%eOMx^e6e!ra!t~>bmKu1JS86s5p|@M6rQ) ziDrUGvyK_1rWyS+rwnm{HO(3(D8K&4OlC|*l`Uppd1mHJ*!~Pdb0+rVdsd}#^F$Lj zeNIcG!<8RFdE+Nw!|Sg+Al0fl3Q1xR9(2~WRG#I{%|^Fz-U1F?CI2kW6lmg&;1PAL zhRR%Gz&t}|Pw*DX8y%{s)HF{R7MGSba9_KyxG^&UY4gWe)3hVxCcKcfsX1Ig#i2?~ zE3<9;S>0aK7|&}fqw}a7Sew_+46E^{`qR&C7?pC(xjygQrZTkEM-Lf2IkC1188W%B zx(3NV^dWsasIfbzFHX#kq9$T-;#xlx!OYH>fO)W%JTtMdqHACqvlEjRow5t%rP|Rm z6AP1UO8US1BH$pcR$*T@(v z05`{oNpoT_3b>QelgIMtDu23wXD0=$uIY;cRWJA9vqRK17{sxo zpre)}?ZZ)Wwv?nd6F>40aoA?tw}}@4>9YeZF?ZWhd{%d=wX`s0S~PY1ERH`3qVM${ z?P4sNl7NkReUEw*TDW>8K5tQrS*wJrKv+DOB;)f*K08b7%aAH zCFW^UJE51bKVWM(RAV_V`;Zm8%;soBE=q^TgcbPpO}ZVcV$&8;XeoVir>^TPRp+rf zKdp1L(xqaq3IE1DkbZ@-+@~;p^glhG^@L)vP%_AE{>p50sDIZTkPF84x9@>aS*6)| z{Ed5{)ur__wx?`IdBS=R^4#?8>@3{MM@lf|ZbI6%kQT>n8?OR;VLgX7| z(d>9PqI_f#qRrmM&OX9l*kcek-0 zv#}qyvD0Ig_RRa1{*i$UyN`{X4zslPv$5mh`-UC++~_CJ#vW{A=XWP8{e;@s!))xh zh8X=s*w~|N?8qWUKhZXJ=6g&3*n~zuaW-~b@eF&SjXlZ6j;n~#PqK|2*}$-4s~i0w z#~bzx8+)dWJ>NDY^#eebtG!K}VyZ+O9|F53^qaH9_ zb1&w!u`+*edS`2d-(2~rk&Jziq84Z1Wn`HS(?VT zJ2m}daJZkvJzlY1F4K9%h^AHP+9{nk;A+XYTQ#jk=X`%r)A$cU+|^>Q^1Vmg)goVy z=-N1J6S_8yp7|D-rY+%Ww1O+}s$SZ_b!`XNq+PwlpO4}DhkDrkEs0EnD!lRn|66UY zwHF_1J#(leb;zxs(;u#KpJTQ3j@5f%koT zZ-TVr&aHQEy;rYZUDdbNUVEK=_ORC4>kI&)6KBs3T5Ohg{rj)~@Dt!rzO^33`uyyL z)#(hI)lz8LX|V<@7AVCDyTGlRfDHYKcl3vY;$I-A1;?yyw|n}6-8^HiZ@L>tbdSX& z8W{Q6Xb{-!Se}*UqSJJZ0y{?IQqURe0dP6V2B=3DR_6DdSUu}qTu4f-w`EU@x3#&s ztGTna)$1FYzqGbIyO^5xv!{3V&3;wi3+Q#pS^{pd6y!kPH+^|-U+bQ>mOagXd?Gb8 zV-wm-pRn0gf*IxtE|AJS*FPk9feY zcAZ*8L0`^;hI|EnW1MGhbb$eLgU+ichW)Z?peS?PI8=o)3lx;_&~rYR^`uhrx3LG_8R0rYGiGh-rmH`@Ou~Dqu*bA_g!uNbaiuvvQ^vm##`g9NVPS6 zbWloePH^9)3~((;T@Pse7awR_&t!xafeP(lm#=^UL$Uy`%j&t@@<01e`y(4_!FQMp z1I6F1NKfJaeZj49GNx$;wIBqY5;YzO=B9@7pZ*tgYjW-#9UU#g1htf;4jh#ZZhC6} z`~OA~Mofp+^b*=Sqi^jTZyr&Ygre_Bin|Mc*8)AFAd+-`SJb!y!b zZCYAu)8m^BY<&a}3w zPm0sBP}fkk@+&)`JqY5Zc~Z8pa|*|#Jg}I8aEqfSPg1v0$DW5HL2!xT2+E&*fSKhL zUZW0yG?2v11(Zjn?2$!X70O*wPH+u01u!=_a2`cjlcom0R*35TA6GLw4_v2`^H^f8 zIhn~zFE3992%v3NxtI`Fa|Wa>ROCMU83_CPW8h5c7kFC(Fir8?&VE@3-$SdqC)0HZ z|NN03Uy^m&K*GNxEkfW>jUzK0_W6gH8o8Q#e+%~7lVCvQ=#AZVc4~icmG?0!{}zwf z$7k+wa-tzkstm%L1FnTB7r43QN7PPDfvwbn9bX4ym~jQd1Ii9>{-mGB&*i=J{b|~J ze@;Ul&A;o|EtTX^$^69^QqO#aMoQxzC#>wg=7P-4wYVE>DeR-I)Y)U49ANP5D_RHm zO{sbMagIY%m4gPV!N$hGB%&y8{rUkKNMf8V`*{bF#-*YJZ+Fi4*yFE13igjrUG33P z&Vm>Wz#ba?>J(4h>U~!QHw)0;a0-0&vaEq~K((x%0>~vVaeqi$v)n%nPew?)3c%;T;zNzzU_Z3) z??3*do6Zaf5{1Xp;=;iLjFfc-e0>7!T1&;bdrz%T-@Z&Yamd3U+G|&@7hGb;&V3Mg zq|Q*CWpM%iVX!rNbDAc*P6GksyIoSKxc{T02Wh-EP{Rbfb=86`uqzu#f>Yvj)#-VK z9wr~)jM|vTN+^uY#zs+GaKaI*#DIQL1q>vf*raBF-<9<=mDhRThE> z1P9)FvUvxy&42mPU%&NWFNE9||CU$HMNk?XpTQ{+<1CmkjMGxFwScpGrKYQ2mT&(J zH(?2P&v2))mmMoFLt zl)ADcTP>;VQ54v;`vR?!1~)_@NNfR=PSDD2FyK0fX?AQX6cH2WD$=Nz(hL7!YgV3U zA36iwEve6K2|`e7Wu_XA^h9C{ZA&U`G_EM4UB)pQZ5K#_Wha^c7cWs#|gE1*({DC zJ7Y;C)#3u>+$?$Q-{1cf2WXZx=4WF*{{-WeyEJs7349tf;qY*F@4|3&Hf65;(+|I1 zs)C4f0T>iBJE)`0fAs;d4v+|Dl!rEy-FE)=JF_Y*xYhii3=l)J00f+=0&5zWiqtG@ z+4A^XKZbU5V~#*ROcSQDtiq={t9og+mLYUfbvHIv!p?ze<}jdD7OS!VcQQu-oXA_B zp!~i+2WLWi7dZ1P?q@$H)jxYQeRysZ;swTzG7F?D&wWT9XU1+EpQF|1xDk%|4|tDd zLx|B&61weJF%43a12#=(F7<(NC4e$Aq>M#eby2fj1e}v8c$WG7=s@;C)K~&!S?sH#9L3Mnm z6u<+_0-BFAU+Sx6Tmj3w!8Yx7$<<>;wW*B}(#8c#D39zD7aOQNVXk{6eO&n%b!AOR zYKf5Q{a0nneao^0XO5s&1>een+?BJasw7DR1N5^Ry0>AY9yP9@+s&n}mx2MhEZ*Gm z_%Yg^fjB%QkMSN<=ehtDxM5iX=k}PS4W54ptP%9PuYav5q1I;gK*8CxEzw>*o!#KT zvwttkNfcE)J7-2J_9~~3O{l9S)VXS^Dr98CqdRVM>Ra9!=GeRA%4~s^2;)`wDhh$! z|Mn}Vz_&dKphkn#F~Gd$I@jF{0vbSVD(*s31%CJaQoM|UE~^GDHytfYqHGl#;QTto zXr5VkV&r5DV#dZU8jfq{1?W6TLU>DwX(czI;SB%kJ5s}eZyuHM6pE9F#7KvwriR{0 zNcsQnmXWW;U7PQfQD%VFnq+TVmX~$OEjE;=8n1GaZB;M*Bu>)-WwNM}#eU_G`e1J! z_oc*}AHFCBTFwm` z0pPbEktH}cEGM}?vrgTr{PGK^*3JLsyN5Sk`BL;q@;Z$c00FAW>z{#~0U9kjMmrT~ zbTSie3IyZ~8qFIhxmM%A)@-R$;WiGDTNWtd=8|~!Kj&pZ5=P=?1{xS*my{)f-Qux9 znd}sf+>ounhdk`_9(PmpH=oH_Lz%}(0;ic1PVus&fp)EeWp$FrK?x9`CZP&0m|J*& zSt9GqRBYtjCHhN3#ewP_y7{7K|B`1egN}h3D#84WBM(=96jU+@1~f9|u`9Mc$S@fI zT>t?F=;ruK-$UU9@yJQgyTBQ*d(uiSySMrBq*TyH`8RiU$r_uEftcaAA3~rEb<8t& zdb!j)`P^gBIY8446rhGNI9wKf>p_%Fr{4H0o&l5EZ(S3LG_9>P=B!&8mvfL}~OgW{Me%iHY z9!y9(ZPnSmsGK4F;uAwCYCu3s{mPkJSt^K8&jcN_(}Ky7QgEQLxs>8{4EW90(nlRq zYDdALBp4LKQj9xH8YLX$4mm%s%*^cbj(MZbG$2vpL^&{4G1Y&bIif&nJ@q1&$cp63 z6`|1%b_l3KR;(xvaM+rPY&r=TZ8Y+aqvXB?XeMUIOG`6~ zXTA=`Xgvo?V-(VKx+aC!zrqbUvyc5t^z3XXPgl4%frg5Tg6;y3b{4Xe#-j^>;E2YF z(4>1orSzUeklrd)hWq7Tru8`qhS09dI`^|{YgEgsI=3g3augRjcH{7c+kplnDNnti zGLx*;$s(9-m%_$G8T(^&EDoNh=k9$GOgxO;fRM&>42QndCGd4nCBubz7T_WPh5%4R zTA!7bG++J24@w*-r_`vlG*Hp=%J+8C_<&V~oxu=h^h(UqWqX*HRBqy`xJAM>7daQ8 zbEd8JAg{^6Ju1y~F?-S~%)Y6f$P21AXH?$@VF>gdH*tl*-oD^}tb!ig_!||MmyK>du~`9j?9jMfwn6 zrs=oWg1_{H)PNfnb**DuNzk0Ym8M^QJ*``2hGAq&=qSpWS5B9ubw?^0(SbYPNoydy z^3B_{M!|h9Y(9NtdGU>Jp~8zd1IuFZPEB2Df;5)`3&;hFZwRSHXQI?wI5Td8_ge1C z;mbI(9c)?0AYA80VUCkv^humUu|s?b=oX~Ot@XY96Wd7FF@{)^3V@r-s4$KKE+#+V zB3r3jW>P_zI^Nuc;WWbBRf%3E4aP= zKwAD^2mhCaWH9{6NZ2<#Je*~*TbS9H>42PHsEZln7TD!{cOrmtw0r0Z3PvxpHt&HX zxDU5d12aI&tDlyK#`aub5SQHfa|ctW1owHMTgu$vK#454@lS&CL7ubBW59K`cQ6G4 zp|8z^(7tR-a$9`v2o_g*tNEhdY*-!!sOsLgpABGUsfarll(>Fh6? zmMCeIhjf*$TE|dlvMqh_^L;3MOdEi@I6xYZ3XWcOu1cLg%(rwH+NmS^5KmzO@!oe5D*;{en5BxnM3 z7_>%bG*S}^g`yj=HAov5XP$k?kvuuI`Md=k;9?HloRV$s+`MO37#eL|UF&wCqU;ap zUC7a-A}9;qya4qoNaEEksCgzG-HxAqxqzNXF|hh`!6cr4QU&6%&Uh)WFcQulK4k&V ze7cv(dcn5 z0xf|6hF~e^#O`OlNJmHv%Hj`SemueWWN@az)^m)HpFIV3|3?eT__D#t=AhS{<`lRC zOM4k^YeTf0eI4u_ZU6mAURD5;3zj%qlY*dMz5mHrj+S|V+lN{gP1OWJqabqD)`jyd=*|{h5#exIMV>|wMiTi9tiW*O>19a5B>;UHm?#ga0RN`UO_M(3MW)Kokm`M#nwe771`2DNqot?miEyc zsRFuBV;=F#qu|R@il}hB(5B`wSJ~Nl^^G6l=*WPcnQL&}dTU!N_ao5FUI5L}-7FG4ao=h6bHyRt|4jG~+C1Po5yD)J@)>5fwAAmGZ=g!mRK>I-3u5y(7redZe$f!(m z=8KD#EKlE|DTpJTn;Ux0!HblLQf@BsV2Ki_+I1ve%f)%Lpm*EX1>i~UOF(1S5_73` z52UDC|JY%*g`u-pxIeudCbA2RCv$dVvtXrxP=e8;uNHy>u>{Yg?~2Q^D~h?v63SgO z$}g8KX2gt|y*RWT&wjhAH`duHLZ`e6M>wL>X|vO<;u6r;%?VGJnj-GIX_J>f3Ff+} z{0sTTPjrHx;6#D?SAKHR9taKbdL_Q<+Q{fLHAm1EnycWvG{#*kmmQ?Uu~^jibfY*r z1ExZktDOWL1RZKT6)dJ{9VyM>>4C5rYY=DDWV$s+Uv))wN2oCBUO1kMBb?d(;P3t?RBai4bkI_Pt*S6*{xXo#|Pk3E609R4MTbk2S$!T!{)1YgI zQsDkV-QzOF_xbzg+_`-%jF)8fmdjp$v>?Gfb%;ZS0wHjTlWB>)S-9n_)y#xZwN{=0 zh?$~feAz{8=q=4Jf7~cEp%5R(s)nd;B_dR^Ks&Nt{QI{)%q%%ZBQU7!qhLU?#W6<{ zQxv8yE4};S28YEuhNw%A1&&vkRU zA*X{UfpkzByBK)9TD7KC7ioG2~B*bXwGs|3No5S3kDtk1$TlKlZfj;Gx zi%?wxNl-aSpUp7q9R9CQeb`?ZfkY?pWH)C9l;v{YIOYo+%}z%|H#x2oGuzx*OWylQs)Yo5^6QDq>2-Ru4n$52l?C%*a8AD*Atq0<$Ln6JFxB1ISX=F zHUZ~wc)bz1c+@g-MCDIjrLG^Ktl_N#>t6=j0iq{V3gxqV<`CnBkzh@5cnP&_RV*ChHLM-P z6#wiO!3IDQIcX^^Bq0??y3~A*Ew8Lg+YD^8%?`+|=TCZ+Rig4_3yMwKa091tV~R`9 zk9UIeE{cW$CPB%Uey}cUxu;|au8hO0!aU9EdEuec8^KI2wF{KSkxSRtQ0eqCq3lbm zo$brr=h?X80G&-Y9lNGd{$*6eM{P&6GZ2;WApu(f&;BI(7VrP{o);f6cvZPrdi8~$ z9zJmkqGeaQ#m=>|6IcZl5MXYU-`+6tr3r)Eq4l_q6t#MoRz9R?jDKj+%|v; z2gV#-z@)?7c!+6CIgmP<@VQTc4f8&|LPc){I<*X9}zl6kxBulPT`aRS-}b=>vBZIP1nN^IYdisRtjkuoFPM88=7NFq4Z=iR-;Z zS$U)*3Dq(!$~G^52+YgX16rS1iozc#V&}j7f_F#$imT(rZ=uqzreVy8spy3@>haH91l^Y3aZkwCSm;zbv ze2H1tmR|!t&1tC#K$75rG?aznP>g|!E%8HL^i*RHSbLv?=I1z5ydD83)H<3iRj7$6 z7|MdEv!!?5?9&VgbF<-rT^2Va33s5d;)g7INbafMR{-MRB(Q z*<(_}!2t_<>2u7x4tvWSPk(&pidrkQhpKHfk!EWKcP}LUp{P?2K?aofrfV5wwqnM* zFR~zyF$2U8gM}^l8|KVq7tMPAGg}>_T4+x3Odv(QE8iksvOu(P1Oo&F7#6+RI)?b zL)55YBhg5-q#$@`a4WOopocHUm6vX5Sl zT2YM(a0@EHRnV!+j3c99E%*gm8)gvA!&I7BG(elpW=j1tb)$GcC2z~lBYEs){OFuApW);?J(r~6Tx zb>3iDr`TNMe&}2Cb*nATeFa2Aj2Dx>O<1IgfzNrCGFZ9XU~VM9*-LOTzM5H+xx{F5 zqzsfC(_n_s_lUO27L83qNIj%80RK=Ip8 zjuH6%fJoB+on3B{|=@7fy@HRtc*MLWR=6h0V)$LLM z=Lpd{N3@wcFBZ+#IDeWktI#`#V#CoMnVDsT>!ewlWa-~s@S*GI-UEg$iM zbH~)vz|}M*j{ywEG}I1D*QFo^>4e>12y(Fywk% zZgap-Pwg)41s~mL06%*UYaqKrN_(M}T_J>kl!8CBnw_Oi1e2hA3_Q z<8KQ=qk{22vee!eo_hKRva?;>5|=q#i&TJHxrS4zbe-{eoC%E!zw6tSipWz>h-U30av9)@_+gEmt-_qI;4g8mVsXx z^IqQYJGZyoE_3d{n8Vh}=l?Wlb6!o0KvkfcQ(Qd2`K&6xsgxi-gAU2V_6~DRN|Nu` zUV&_>yGHZ3auaSjd1RnDvX|>C6R$b(fn27hHkJ)qfSEez-H;W|0iabKQk0yN2FgIn zSa-j6|Kq4J8}5oT)LrJ!9TmyFZ1t)KXG0g>rmsvNi7=T|#MaCima( zh=pPiNSyf)2&glD|KTT8^StAuw3P%m*SyEOg2Jq19?u4a{2E2w-3JbgMKkN>_{4Rc zT^4eCNT_5uG#B0>KECjC*r;CP@%TdlbJRS6Dz8|8m9s88vay$e-B{09<=FucL#vg6 z)>ATlnTM4)4MLZo5>64OUcsdc0nKkG` zlqR@}YNw@d7Y{75N4Ldcp($0GgVY8Ba%BOo+uzQ<`$Ik9p5XyHhY`h& zfLxclD_*LowV@svbZq4d)Ky9?xdpkYK12uZp#371W>yaqz?j>k`*U@Q0R5G+(t^Ms zmzElo74h0+SYxOFwfBAi-kfD&5IVeIWenArClHCG=r@4zc6_++IQuNOSu$f%ouA#z z3G^NYTe5RywRb+`_lZxNEmNpk&kU?^WKKy>gm^loj7aoAOkGT-7}AzX@tHHd0L+$V zNrCg|b+)EZu$Pz}%;Erx(>}ft<_f?$z$ze~#-zxhc%JwkeX+%=62Cm?>pp-BB@^Hf$OlsW z7f_C}qWQU0cd98zYbuOnN7h<>SJ_yZJDjqwW+o(oGLZqjRdk2q{_FQB0 z(_x)PYI3^@RaBz}aLKyvZMWA%c~4)fd?7yU7mtput9Ix+b1h@zD7%#1AleEv(>&qu zsAK{S5U^wgEN3-dfdPKT2hb(r6Zu4IqRE+}VV#=zOqo*x<_(nue7#aJ<=MkEg){5G z=|5$5O5$C1*wOFS{4Vi?T;SANc|3hf1R3BmZM=Z!aSO*;2UXjc59^#=4itcCfVtyd z@2aPC&FnAfBp8&8-HL zIFO4nGLuVZl{ABy#hfeq^Q%d?t*tw}(GCw(5_x4_2)Q$0Q=qm3%vG(EV7zgrkCSt@ zNWV3=Hqd5cZ@>G_qdG4^!#rsa90GdIuV2 z*BhoGs#I^R#*4Z0%&5y##xu=n!@)B>_rExhC}QN&z?VS)8?c{BP{7&AS>)#Ci?kg& z$(jV4>9iM1P;0YSTy*T4`%(m>=GSUHW|)R!J8@?$yu;Ih(E+fYcs8p!Evx|5%p=@8 znvrbEHpP7=YD3xzIJ@_I>%{#E6G!~eX+}sk*OP{)ueQvQ$a06E32~BoXrNoQSdBCu ze)|c!?I4H;XSMqew4kIrct*A!FxARX6Yu6x0t^K$=42>bYX+m4Hb4%I&a;EWYw*zK zx5%`57j%Q~_4J;LC$7tCn_Dlv*ONQ%DuDcgpMPIU3~gWmbfVy56$*PibOgmT=fboF z?3M*Ld~<%=LHaeT4Q##20)8zA3Rm<5naNo&u)4>^-@o$O04y%bl78?@v8%%# zmlF6m^Wg5tC8?m^H_mK34SKHy=V8H|Y_8V|js>4&bkb%wKtTQ~nB(vt9{vH!)5)8A zpMFFh06)kSdLdnfaI;f%#@iLOaUl6@T;E|I?94$~`ZARU2PBdbNP+ zUEG*#Bf;~yp=ptuEf!j12?Wp$>J^P8g4N(3UqA2aTq?g_2XRD}DZ96fbTg=7Rj=>8RUf*doC;_*qMa*oSpZAxw6hl0ZlfreKroY`5Qsj_;%YQVz?&cF9K*QnwQ5`D_z2E%D804Bb!!1dIa?f|~JECNm2e9Z=Zd)dr7 z3ZR6+Vz6ztUwkAJ)#g!Y754^0uXCQU($u}rmP0tF(niavylE~c>YEFTzx|VMbP(r*ZkVhpqWAtZ{w}6+F z^8+olSjrJ#78 z2@d4N#7FFqoH3@g;ykGbx3bCd@Y&&!6aNN$}^)RslPhc-??f zg6RVgUz$T%%N8_|0W#x`iOr9HQ59Del-4})kPk_M4(H8MK4@-2>R2}uI+CK>nb5>V zq}ReNxB_w(dXJU8{$$j0(clFnYR<*DbYsAwf;-Atq(3`_3@~Ksv#X-9cE(b`3YKvL z47kofan{p@3(nY5YY4(zZsvaOH+MN5v3V)X44~p~zV|%`yQ|<4P)2b)w`>oM$7|fY zhan1xPav})ICI&f9_kg19Sq0FoySdeCE7Q|zSwRR6s>dC+H2jx;+R`JoJSa}F;~8_ z^Ki>P-a!CXUe?4Xj|_OAPZoA@zir(6>A;DMF+r|=oURhMoPG*Rx>q9-iWQ&2jM)(6O3|4+Qu4NWArtf(lzeFr~`%O3H#V- z)XE;)wM3iMSglX;GN!j~{`w&^dAL68vRPUS-Hr*rqt{(X69byRHIX%wyJDwT+4sTY z3Gf~4QzF6%Zii>{xiS^w34oQeuHkApZr*X;5CD^<%w4t4)@ZwU0btW=@YkPgMjz71 zBxh%Y>&oW_Q~jkNa#R;1i0i^dho^)!_7CV0@mc~;);#K$%1WC$*ipFoy(=hq4F#+d z;%6Bm(^bw8zN^YL(tNsj6X%m{o_+#lf7r`G2ByN`TE^Q?JTH>nkP&f|j*Csk9nOim z#k?VOH-kmHz|t_X*?W_$b6zzdE@5Ymxbnrs53+K9*Mk5tqgNi06%&QHK+#qbmw7Vn z1Qhn`?70WTnYV%%>m4y}1t4^6c&I1C3Q2r%@~AzVPXTF&KL@6}BauPQTyuyIT_ADJ z-oi_@w^3$UIJ07qtuJyGg%gxIHj;s&9pr!Wo8aR5$*BRHi`Cy+Dr*|kz6AGH({&z} zupVdb`27zbXnbnIemVbcaT`CW+}8G94_Z6r^T<`6!e_}phJ_~o|Kcz4sRv8U zPN$J+Q-F)$WwUG*YzR~yKX{ly0MK|ZJt$@H2~ZViYu$Tp7rp$csr?f-pz%t$S(qF3`>p+E5Tn44Vjqt)>HQs8B4^Pqa z61UadQ5*LE{A*H~g<|kv5PTn(CvlnS2T*&LkG}MICkGkg8J!njfBa`(;tp8>9W3Ex z7x>K&7~ue;Pomz^``d@4SSX9@J4dd({sqWeQ|4N@`0Y^U;^O;3;?EaRZtYpFe3Uzz z8{_w1d!+ObO7VkyRoq{!9G^OT45ji5Bwp{BwSZ!7$oYK-!0qSz-+FxXHp9ObG@f0i zjEjm|86@DDZziMwe&jfq+5hHO`9KHE@0KMvbKvXW4stNS!5te1F-&;9X2?u%f6tv9 zbGW=r5mwF}7+B$J1E7I6&%8MMviNyV-jZa4j>ab2?I^FYYdRU&ms8?L@w>;KR8t+= z!+Qz|8baM7K(eimH*j!?`hCxT0@CD?@_XJ*rVeUkF^}TCe_~G_FOA4D&5iqi54l;T zy#2XuhTebs{DV)0uR=Ka|JYCMiR-b)7_J<^FtP?nJPFenZe$1fL6LD;3o+zsJrNI*5nIn~05VPuc80ZR|qFTV=L z00UT;$egJyX`z=~?KZ}wGNAa5wQRw=BnJMig3Ii4)t(1luC~Ij{s9$&51gOjqYeO3Ir)83+L#~E(E?Kl^zkP}zMQ%sN)hQyVtsm?w`#S{Wk%gG!=zdVKbjnjJK&bbl1Yv-i|!;B?dS z0+>Y0ufEF*U4CmX?Rx)n;@u;pIo3V~R(Gr?yC3Du5E#H9UCIPwdu#O#@Lcg5lf96x zFcuoTU_hs<eEQ#b;^y!h+(keMFDI=>jvP6P^4V{I-T9cZ9ana_t&R5BWoLG| z3AzRX=xqb^k6-)apTFa^lIB(>fdrfZr9ceC47Kexy4Xb)H6X^~j%=_rnxrH?t$XX6 zvSOm(S^%EFFpTpGMalsk zq;`*N(X7Io!%95!fwU-osl54uYcX#Yx+n%u;K}DIz+U{DhM~yTmWM{o2vU;E z0d6j5KL_`jQ^27I_-J^#*U zI93Sc{P&-H6vblP6Wv?z+7BUKQX!$@l=FS#&%uCGoB+VVU;TS9u5oM$?3qW7gI@-{ z^wcGB9|;klWdSw;+GWA$8Mk|qK3e6Z)V=3-J^6#$YzsPV1$(d0Nmdxo>mXo*-Uhm2 zHQ)@(OUl_0Z{A?6?WcYq{L4zj7lbkXTk3!{)lmlq&RfUeyHqt!3CbZ`mAMIa_V z4Qao2gsT+4;>bw?;=fuEUwWE8BEA!pwQQG1z(t~wwf!eAUWG(U^E!J7RIxDv&MXb= z+}BezE%FmFSW*0YSmVED(6 zr}#(sYvQ@oDbM_4-Xm3G4!f7L9+K6!xKGAF+%`c_Bx~SbzR&CkD91CHe=a_(vO(&U z#}d(K4=>bB^|9g|0kz}3m!Rohlr89~T92WXd-hD6mMU=0&rQoS0ZxL#n5WJIT^LefcPBGNm3oGwuu8Q3j-@4~4j~b#R&X zP^iYGg~TTg;!iPJORqw`lc_D1%Z*SpbZH8OM1Eny?Vg8B3rhIJJJ_uauiH3}+C(!r zuS~^s?z}At&RKA8umK=G;oz(^L)SLnrJ@?AZS_cqRtp6&(^F;4NHLw+uX+>=$O`(xE|g*}n(mH_8U6_m z6thBOkI)|CzwOBJK>~W_A~Ac??TMIrD57PDgk21MkK5XgqvDHmjC-qPM=Od8OSH+J zJ@lgl^Rg*kC+1o5FYKLhgwD<}Q_yX$ab7yaw-Wn?`ofS&E(^CIkGN{s8xsJ}NUehb zec2|B-{c?X(F~gX2*{qhbV_qqtEkU#aPc0_iJPCRqse(j^!cyy0%(aZXHY#_DgF+X zQ7pbP!X_jMPCq<+C0i>FNkxSFh}{CK%j!yMw-g_-+kB-<*O#HT7T7t9Ar%mWf;t+3;kPgD^Zo1t+Da;9cvkh`3RG?*qEFjO*#I%B>%Kr%02L2i z7cn#PP@H2lFb+!CheRcc94Q(oHV2S@ z_z_f;?5@b|Q^vJqO*x>r1@c#c4S@ja=m@W@f;UQuGXPvWavb7Kw5&?*5ZT%}=ivej zBNQ@q9X~b3Q{s+rXsfT0*(vBk^I)05q9lr|AhWO?Tta-V$SJZbz^w6_S`laEq*l!$ z=8I*|xGK3D@Oc%uwXiusw*tQ3J@`@0UFfKHMC~EdFgD+>i^NjLtqutD2P2TiIlFV9 z78h8i6F*Gt%yw%YxA-EisXFjJ z{grxl%0bi6xEzVxorV{G`Rx#&u|yChYz(k4^_M@?kl?ie;5bS;*D?yp0C6*!W#a3H zddQ$J#h)}d610%G^7wb=LL66&yG7BMg-#BT_&3(r&}Wcs8MzZ~H9$d7wGe8^(r z*$xM7aU;xJ3Y38EJyKLtFJ7Aa>p9Vk-7kCKh$1SeCE;|<+ZLu*DY#@JyyTo!%`~d@M10W#ki}dNCFwCHbbPd1IS>qP_ z*3w%$av8_>g_)nWW72r$W2b}lA{0zv{7nzK%wnt8EYle5jAbT4V93SW$vESfF?j!z zV4kQ`({+#TcIs}f#_iS(ac_`9WyTH$jB@wzD6`Wb=>7v#n5Fc!t`}-PO^lBktt-Ab z5gVxd=P!N`HCrRMFHkhy6RVMbco9O?*iJ|WA=8YAzb%eLodq6u5QoK2vV{Y~5GyYP zzciORty0c0_oxx4RdXd_lDJ$z8$4C5BzA~HE_mqX(4?gk?Og8d0;eioOeH( z9U;t#;VU54NYrR-;dyB|*q8XeQss`#CwN}u@~HPj+xqT(RJJ{i!r^ykYbn}0%nWen zKY=l<^D=et9u*MbXWlOp<1uP+PX`_`OM{Uh{v&ZCBtilzz}#2?S6lVJJoR8MMH({0 z$NklB@6u?1xP1T|;DwD#aXQJxr@Sb$G+HnM3Na&bTU@)vrs~0nPeke;e;=IbsvE== z;9>h6zfGoWT$jKxKs|-r;MzMt`wpe#0mdPqyhTYn0KV_vfAZ(@$Z2DpbGFmJ+i|4! zNYyy6;M;KlmAju&52Ll>m6bfqK6j^EGGxSjBd%e)sMTu^UCT-)4K05=`PvBp{J zt@nB#QWtsWw-egYwu)8_51;6`t(MY-dX%dauZa1q_>1q;krKu9%yJSe#|~CeC)Jn! z@v{)F$V%V;{*!OUdh=vOd|kH4MPC7zZszZDqU0=#{a{>5e%J5*>?0^82F3pr!7X@! z!*Gw%k_^#%-&KqDZd-IA7L8+RsQ6SNX&uD@`9C2*P_yV4U+eupKK)au=o;YekHBu7 zoWFIpS_PEUr+Jf`P?4zq z!()#vLYz-)Ae;cb##5_6dpoB1h?QdR7H0=IFf^8!|HBVId{WjV-< zyi;Xpx0g2m(-?DP*7c)F7RUzbdi4o#^;iJuU0^H%T;onDa?@Q1&4L@f;s))w5%Uj? zZtVhHtpY7tl-0T64CXl|FF{FWC{1^S#CK9`5!byBK~e9DgpxJlC;3oI^0Z^L%zU@U z<7(w8sh&3;En}(N(rwjSaKY{dyj8;rFg*s zjoZ2u#_u0N^33;oa1XB)Lk?A^=H?hNcOFc5Y7Fro>|R4v0*~S?xeVZ2OZ%^eW?Kx1 zOgJa;pG$KCaKG_7z&t+Fu_*qpjoQsWRDn)nTBT^zY~Jjon(b+M&=o2vJXhO#fD?;J=vk(n8L2O>67EIDa~5M|m|L4UREwmX-rl56AEszO z3K|~H#4V2QtrovW1Klz4fEE~z)7lw&8+{owTfp6Z@eA6fu8DgoP4}3(TReV7aCC^v zss{lnUZ3uMTUKh1GTwpfD@Bb`Y8DSekiFAf4G`-A({wu?sg!xd_jLU#qO4i^K)f7H zd|@!AWC?NW1`$871nyz?FMk~D?q{F*1`3l#t7Yg9fdJkh@$B5p?ouyp4gA{&82#0e zaJwPyW!$_A6+fVbavC%{4`#|&QM4)GDr7)!?Nx>dt<0^??B@dkb|nV0o$Lq*$2Ct# zJajkbqpGx=p)MXT6CNXxy6r8wMMB#P=P;;?K)?BIum> zV_rK)1ek`Cw?vq72<{iNw~iA9hQ}DsnGEO>Q0Y`WiUQCN(ne`$w$bjq>^52d^*3e7 ziEgBuSaR9tJ~}-BD((Yg3;R{Kt_yG33_N~`r!S9=1EY3*t!%v7RxeUnNUS@?EhHg+ zdkCVw`L*4952NO8ZPlAit9YSBgVcM#*5iGF0PW2=yOTafA8nce!dnK-EjB@sO*{cp z$Hy9!bb$nI6;FcY$T~e11hLh>edE(Cd>~F6xc5m}QZwR;1XskptvO+Xg4YK+A!HS4 zU1?B6I7YSjD+7=Csvo@-Rf;1L)5lW6H3jl0AcsOt&3M1q|)fh?BnDe)Tx$^z?LZyMYgbB4c; zCS-~v0fqD?;Bd8MzerzHRJR9py~c>fYfx3|<0)9vd*)I-A}K-x82aWz+TK0l zu`U$|SE>o~;zAoOupKCaF5F7T9X-`f5tpWTLleP4fI-!kX#~p|3E7-Pw#^9cb|ubL zitOYt#T&p>$}BJ|@J#{6-#}RR<7b(c+dU!1P_eDXK2I9}98w}xZSr#FGeaJ4FgxT` zUeS@)?X~^v2GAK{Ty$ZNSDnh*^M9TK-@`?a0Nak9WSJDZD{_M#%}99eei@2|MX3e1 zhzC=8?T~n3#rt&k{wo~}D?q!$FmCb1UVGBWpSScc=ag1*hCukr$maZcI^gN60cSoI zZ(#OSxGiiu$6G8Dlm_kFU@8yLuMyQJMC%l-o8Jk>xztR{w^Jd=T2a75U-|rCp%QEF z@bDlc360%(_iC}44VkZnNg>9Q#f?7kh$3&oXCtf_lSFN@y3uGzxM3-3q%QsPN$*IAC$_EI+?7 zc*VVc3bkK;g+93mngksNNiYU2C37n)(-2N1)Mck^)DlQHfv^FJ^t6J{Fs?%roKr!4 z_!sZ9oVuW5JS&^_$^vQrNMZyf8)eOam7f9wO!1v;dM>2-y^bPUiSY-lmD|7>ljo#@ z8)`y|;Fq`wc<(ZVEv~z)u2GYFDh;j7F6CfkC>ERYaBpx5p`If&8L-N;%Zg7K;ww4~ z39X&m%*B|isXgT^M z6bt%F{o?m*+=aX#0B=7EwyO-Ro!6fd$6~LG+nMtL5VsDzZOqp6Y#PsDO+@q9SW`Zo zlFb+a5}8~iGqjf4ygzN53*iTFQgeZ(4f*8QyzQa72v3fT@}(jx_}04wj&?*n<}>|B z973UP>eQtd3O*cPm&X}zz#qQyV<{N2=v5IRYv9G8s@q_TNw6*u(87RTzacB&+zV-_ zI7EGHSSp|XE^o{(fi^!4f_Sdyxi2MeP5$OF*@oc_*qF+ez3jts{CAH&_WGwF>GoHC zh$4fY0U}$gq#tKiqZfkm$nO5*5GEOW6`>15M{sn9`; zj#FoR+&%OT%Z7d9^RzfHvdT%0ZO|(|&@`xi+yM;tcM6kA?(TNu%#<<>p_Sp43xW@}BeFjWc$TCwF;DtxI zV`zXQN8bL+i$llKM@}z8Y(L3s_KQF2POA?~slQ5=M;6)FKG0}^_=!-dmb~QIzk{F> z0Hn2z^il4 zE~`hCF-O*(e(32#m72G=g1mLdaUHb+d9)?n=*4eCy!G!N{E^Eeb($=AWvREevf#FG zRMwW-DHz86j})ThGEP`6xh|A8ZpB1lpNjz+DIet^cfSq>(3)4ISgdoFTzI6u#=_w` z`^y+I{q?m*X?ZauK72^vc@#!9$)pu7 zQwlL~mxFTz4$A^O#7sa>aA}-PX>8UprkiW)Sssf=tv6MIk5HF!uoWbJ6OUnFOH6^Y z$501Y1hgIEp-NdB`WSsT%a~WeSjok``eloFok3Y*bY1*Sx#gwr1&u_v9m?t!tI8&_ z#h9g_UEH0cy5$2u&3I^pAqdcz0LKK{WKoU65%9_id*D{#u`bnoq#G6SXgUaCJJwMN z7bdKm_aq(;+|mUmVn%$Lg-WM&$}$;PrE3>Kt+Y$Gczt5OXmaheurC3I&ceCwOUYr-m?(8i&p^SCM?U}d+?CRs~G+w#<+ zf`)hBP79~B21-=yUb3Uik199tK#BnX{L6(6h1-hEM%#fFBz5BVe+GW|5&s+}JGiINC^mt;$ezwb; zQ*)_>-kQyzlj!z}Pg>0tRJlii%^%rzaZc;JNQ*~O+5vdsNw7U2UStB6K}+ER1c^5G zpbRH94a^ut`axOS&bg#w8cTe~!IsM_+&Pd<)4_rNi@A64ZtFVl1pyGGa7c*4ixfza z5*~*j_z*<`6h-O@90Cx)mq?JJNQ#mP5`svHk|^q7$$B`BmDWj<#;-PZk~WTGr%i5? zI!TkJZEj~R`6b1Yqu8-)IdL;>@6Datc6F_p`3L6r+j|qFeU;vsyJqeh^7lRVKIiPS z_qV_Ood?d@M>+mZmXPt%ledKvNp9iX8V@Va_9c+cfdD$w^b9i+{(5kFqfpOVP37!=U_q@>CXaR#LThv{)Jem#5#t=e|h#>6@9UsZK=~XjHGaMf6i44olPLoe(W8#dlsa6 zZ&Z9B6lwN=@Bql#*PiH)vmn4PMINFrZQlzK?*zN3Ix0{W_b+p#is!Ym%A5u=G;#kK zirbdAOf>9})en^>OgSe}S^x#gyDH`@*L4wri&SdCABhYnO z-Xk6$IHvKkfjuD6mZ_Y`h_B|dCla?_WYzRQnIlpYS|@H{$@R{DEIuIb=BMR?-iV?4 z*e~aJ52X841BM;t;?9X&F|lC?3X5me*fP3(LzfjC$ap3bO7n}W_LW4WO!#kwW~DoyN6Vb#Gprp zQMOV=BtkB0G|ciN;{2%N5f*Li2Me0O82KHb2utR9o|DUmSpYlGw*+R;H8SFKi&3j1 zpXr5==+N4sL~aq?Br3JHRdAv6b<>Pr!&k3Xt|XTfdp$n!G1OC z5iu=2)ZqxXXqyXj?CAY{;x%*n!%(#-G6A&}2r-Z zb*+R8#3YwFaZ3zIiPlURk2_Y7IJfF$?Jt4a;|4fWYr!H}mr`v*B}5)nFtI8;5nlim zG00W>MjL6Lk*X06|Hyh%xv$d7V+Je_Nbz^wf-LK`ij16IrIWzbdx7K4;Btz*T zIDqf=T!L5& z6*rN^T9Gd7JX_WTSwaf{SET^wJpfhr{#9d+?VTON0y<2DRo)Cbm}PHto$Pp+QBUH; z-~T0t9i7rrR%WHY(Y^q86q8n1T4RM8Sb@=xhibELjTI)4A)Zk=2j!QKgB4L*)TF}g z&)#`OJqLrtn(}KuaV7i-TR$|1)rTwD+t)zAuU}Euna%80VE1(?!TaK1T~||inZtAP z)SW*DpUdy@UzwqM@Wf<8bo8}nApu@21N4gvyLnv8ogNpTFJmkNA{*X$1)}tqKMq0A zss)BSiSrPQ2ohr)S^!5gy|Ei~)eSaQHtK}nSfjlqE}e})$*Jz;iFPB1wUfBQ>jk#- zr_UAhK6vRA3zeM&WsMm(Ni!Zi(5)(7+(;_og_Zw5P6`m6g#&lzw6`(1pe;#!3y2Y zry%Zs8tlFAI-RfoxXpI#2oezngPD=NmFg;so?lsaG0cs%g*x^$J?ho(fzgfjfFwB5 zflG}i{zoT-aPQiGP!=)-@muDDxIhoRx|M-bf}=}3yF9}s+hykF?G@rH=KF5`>G&ha zngDS{iB>1%F6QSz+!;_mA3oMBRqw9;N4G(Bff&vDxp`N;wrd&k^c5Dw;Ki}Sea?cc z3`M{&2O7ZGATbHy^II(3jhI}S!GlB21Y$*ixOgvi`-hLquz~OO5 zk1R+H(MNZ3F(W;zIIxe|;fu$4oMAZs+21nc7r~QMb{a4ug&E?wR1Zk;U;p5DqypZl z9QUJVkl;b}x6T&9R%hP+t`~w9;l3HocZ1p!U_4H9Q2pa$H;!$5B*5io3fEk$Jln-~ z9`Wn}=iy1BIm`_zSY!|s0387V!=N{Q@6K0ciq&U(0qqD)BXRbTit;cUT)%Jg_9;k4 zu{NARHS$=MJ<1sC01YuIA%2;Ks>vkx$TcMV5Q$u7HLSNx-h4WBFu)48ZV4n0Otsjn z{dJChnp9$@Jm`V;v3Vnqw#lXAI(v154omB)QOex_v-F=d`*brsE%osGKY&oq4GlPb z^LcPO5+J?-G_`AZk4#oc0lpe~SkC5+hrpwZ;H|MYzRt*IK0WJUR{@8+V-<&`%9Xja zh;?r9VV|W{YlYQT%2m0`1DE#i+%N7Y+UV`vyxy9yz%{t2}JGTwLky%e(1ux1ID$kK}aOB6!v~bh0l_yTx5# z+Zm43@mM^>d4V3vQJ)y2_WqB+c7?^qO~o^;mM-uaHWn^j!<*qn0WLnmaO62%pyqALBy4J5~g9v%P0;c zBxp#W6qI9h0>+Q3@Uc}n-sKOTsGf;qiTJv?6^a*h{}nH5%ONuCyRHT-?eyQmV$d6F zdbT*}2>})*!P=3!Ks$f+Jb0F)0(k4Q=|%g6nlRMuJVXPGIkb6yYaNFIgC)=@#cF^T zA4igz;4_OHf&fc8DOJfCEKT|unFv7~VsmRa@)uiP`;PUJTmn81iX8z{`|95boh`ci z><}#>v2zacoiED74v6=D*FKq<<_LKGYje@aE*|9KZS)wR=PVe@sFZ0If5gU*|Jehbsm<8N%81Q90MHTc93f(e)GGK65=VfbuFq{;^uNy z%RwxoWzq$P`8E)%aD>+U^*6yJKKzSaP}Gr;JDmk_1Fp>QE#3x)1WrEu|8O>od%qLU z-T4W9g7r3CRQ-C&XqE+bZG}7y8mIFxdL$ROEhLktb~#n&(LW0BiVv}KWLJYu=A0ZY z@BSIs6(0704WLCc*xZMIDs#kJ371bDk*Qa|RNtK8frROC4_N2DpuFtF z$!w3#ve=~=IuzSJkSupJ9~H9^s9!FTiR%me@rpxz$u3@oTpQ@$!K7_nliDNJS_Y*+ z^ta#q?Mtt(c0LFACeTA5fZa|bnIh?-JgYi+&~Kf91kM-OXUx3yGVK@F$NW{|(NA$- zwj5a{E9c?R8~W)#bUOX&_&l2lXk+5@iXm4*roqjODmnMG%5jOOu)CC>kn z7}h23@c<@%6}8>E(Nu1TQdndXL@xkxJ=^nP)C?Sl8A4A+a<%%CjYQfUDu=Q{rV$F+7mb?=aE%=`G?0Wosw2BG{Cz`5Bp} zH7#9WY$+?HrIY0eZkg3%vEACMH^R_4xXoNk%=$2=Z-H7wqbBG2wl2lFF5to~wg^bjCu>A-*TI+J44L{ev$LZaP1By6W<_!!&8T z#de=pKa7j)L7KQN$?5|xkPU>#dzA*UQS-YKQ|$cYHZb!p@8aQfc*O2$nBV~BraZe7 zE%7tO3efQKOTAE5-u!@C9;qCjbK1njAlX9FkBfR7^;hXg0C_3sF@CA`wL=brYF>SMHQbeO1#XkE zSiu37=FMo}HJ}X%+hG>%0|7ipjx>Dm=jHQIXuKvzzY!BJm|L`n&w{}x?)r9LT3*D` zBG8#uJ44-QF{@1-aFM~|*QsbCFzhSk-Wkmjx1LeEOy|M8nYMBZ6^Kg>dpkSGs@ zy*Iy}S+92|IDIOx$EBn~wdryHKOvkwe;JGFau9HynuYTPwGu8g@ zy~u)?SaTCTmbKD^9qhs^XdcwBblM1$r$-ELtixmzzaO#^0G$X>1#4J z+GS3F$>UOQvg@iwGF{~2hcHXN-5#aeH?v(G+bde9hVy6pnvRN3Bgu8Sd-<-J_~Daf z$Hn6_dIL)qzsj{e<#b8NY1hclQwZhjrlu#e(`=cn+C8L;Fvq%P6x#7CE93zc?MavPVQC06;B7U z&WtvqBW>yRjYJ{E@h>tq7qsRV)T&Oox^KQrDk|I1(nzno8V!{;0P<2uZXd0ciVcF6 z1UeZSxKCS}j*=EXGg&Qp@ooTHpJoxGZ#|3mK#$eYr!D8fZa!hC13PzA&X|Dr#i-Nu zec~t3Ay+2ccaimCC0(8qN`vrd;zyfTpW3Lb7#}gjP!k9HsigSjiF+5bS90?NnRqRx zdJI}4rul;4&VUBZ?@2V-`ix9Pyop|AHmYmc*HtROwhxO3G4FhpBLdHq(`$5`KDd2i z*t7j&x&`L@#4JNl=&FetTVSyR%4;u!WdcdR_z6C21ZXYbK|f>E;re0$%V;@$2@H>!`6=3T@p&%F= zc0NXK-L3X?3<+TMHjkqTNn!UMDqqay2n79i>`|E?fzCz(@4r-lpHK-E5e7UHK zS+UpkB#Rm4hc>bZ#>u+wwvF`H3lQx@`_40mVot2v}GW$RO&F}u^l`lfKl(VH0ys+4pD0%luMn@px>uC@Z%t6o4 zd(}Wo#I&Ei#HIncrfii3vO(M}h^DCCdN#x}Es2$TRHuqL!>Sl$h|@4ymNljiGi0j8 z^puGB5^f;_)zJes#IEo;z$*Nu^s2O(9uH{8r0iz9IU>Br!MKez?pw}_>6BQaYf}ik z)!qpX_{D2+!=al`_x5sRYhDNK%fd8#`#RlkOS$bxG_jI1nOtwQH%IG)bKWW&O(LFI zgB$<>F%atobX&p1G)t4L58VM!jmr!}Zvge+#>+8ra$t=4wH8aQF%c7w?%aEZyQE(L zj(48~uj!&8tXjIf4w733LXMI$*HznskHt3z#5Y!1m4(VB@Gf^r83VH8wP(ODvvRF_ z4R8rvPS=D4M~)UD107lbJGBS~hzB$obUIia37$X$dyTih5om6P#uk@?*fAE>d}C4% zi#Hq7$ZQAE*E@!-;L%~QUwwPY!acI$tnpYygpsI*gNp%=VWtBF-2Ob6Eo*QyfLj&b z|0Z}V$W+>1$!(@M+Iu?Q{VNFeC4ZVDWubCnkb_6ONe9{3%`Wmy4(K~+%06Szw!AK?LuJyH-I(E? z`!-jv6K6I72Mu6lgYXg33d@4VdW5P}52%CNQ1!<`K|5CQWviDrYDRUbp~h*uZ_u~Y zJ*xQ%OIX~S1GA^kftQL&1|}A`BVfL_*P)BkF&fJ^urg1?82vOf>8GXG1Rw*7^!8+$ zy2@B-l?$yq!zvinF1879Uy~9%X5xUgfv*4CU%v=3ds1(R)~uZJ`@LdrN923c>r6B- zU@@0@E3$)PfG@4=iKClNk@Sot>qgeeHNiViN)?k1*9;VMwVQu(`)Q;W!fCUK&V8gi zoJoy9(QCxBlRi3-OG~>e*=!D=GLSqBAApAMrEqOPjL-~=9^@UH?K0O=&d`&#nw6h$ zxQUxY`R*KBI}d7V;5y}W3**)2Y46;imL7MD$m8&uGwA~IEN(F^D6MH0&lf^rA2vEY zn(B6>B$|DZswmVhu5Aw$^J6wsrOpm(W_JO}^4YB$#aQj|yz>-SY&TcaF9FtAkKF!N z$+B>^hVJEb#$f%s=_z3bB7mJe@DE@8av$4pOu4#u7KsyF~bSpOm{OxJOB~|cyH++{ZDJ!%dej1Bi@ zytNBZoZ6*62w;HQUY2?@jnWixc8cp@cG`%B+`*tPnh6vJb@JO=h#11)UvI2AUYS z{SpH%P+4olSWm?69$QZ4f#+sua*nC=i0?dv_>>uBG))6WW?k&kF;TZWPeLui4ozI* zK(aDANE4qKw=F{jI_aT|*M@j^D%?wFUEQ_8v0K?kH&dOL=xks~OfAPlg}&uFO~3Or z+i%z_*pBps_%s|e<%iJvyN;>NSMwrS9%7>1E>Dl979!2z=x*|dN4uev#MpqThbO_e zXT_rh&~ofOAahYlapPB<L3I z0b-*J`VcF;plUd_(;Dd2jJznvmAU=gvbRPP&BsbyXBN~7ngJD&v3skJ+0N11fMu6b zpmQ+$M#02wtmCYw$MDoNGtk#oX5PNh>}~L~Wvs{&K4ND;c3P|huf7C@uG`tVTTB}o zVlSFOy$#x{Po~-Zy|iMd#dVDPe1{RdmX@*seoq=pVOUQK50CUA+Gi>KyTrK#h;`UXS~ zh=l+y%MQm#XsYXt&v3wQc$nuk>*x+GTLyb}Ltf*`!1BL-pXc++Gc_Bdi z0MXNojmI~ZfwNV>%6%-ZtTvyJOW0XFj01iKB9$$3z>k-{!(}Y%deXi!bC5WXJ#mGD z19-Fwtehi|Ee1FaE?K}1g9bXX;6O8|hu7@!ql=eh*6}Mxp$t8!|9oq&sl@^HL47ET z0Y5Os%BYhO?j+cHE(Ouw{PQnEeDFt^q`LGzSm286^#I6q+82e`3R>uI-n6v|io_`h zjtn5NVwk8mF}fRbx^2r4p3@44&mA`=O1pWGDZ~r-Nm;d{(fcJ+lXQz&yItca7PtJochzW%fsv_0M zIl(?!*Yf%wLD1pue-3P03q7;?t*5|&b32(g&c&WmFpkyhAlB+C!%z{Yj#TwGZ;fB% zNNb|;P5BFJzOuo{0xZxpMXWDsBl*Ao+Rwjs94_Y9bdqOBt`|AZ$aj?XexvZ!0J&#&SjD*WKo<`_DC-}l>+dN8V{82FdJBC$N%IWGz)Y2DMFR5Vh&3R4*S5DZ)hh~wf zymbMpM2{&}VZO%3ELkrGWnMkR5vj7E2G)RmK3>DZHHPeFE7eIim_(SB33M}HB0b5XQhpLJ&nFmZ|TaX5R`ttEvh_Io#RK>SjLaSS_+G|l$Ol;|Y=W~WTZXKPq z-F^kp1lR2P}brIGwojd87?7 zZ&wT&;17ebvSop^8U#o%6QHB5MeitWFd5Mj_dmNfA9^b2?(jldQz`HhDRFnFyJ_s) znafCb-Q_TFU@rfB^}R1bmC)mc-zh4ljAV$%X)unkJZWnNZw*D*hV`-95DO||H?+iz zOzNS-9npdj3DRf)V>_$fQ5$qZSZ(4_9%#U7%;p+*RWpwBJfR4>fYooIg}A*$Z!k*B z1BN4!(EZ}_)6Vcdt2+{xAWv>)1TR7jPoz20@p%#>*frO&GL2`j5Q+xkqQeq>M^QBy&_-T`fU)7`uP(Lk3huM(HubB zv7cim0$LJ1vI2(DNTO8-utb6Z=`J-`z zLBJVRgP~B6eV1BHR=j@NEPgTY8g$YRw4 z?xuyrLs6`DMmY?7nGOZq`3tavdW&BV#ND&XBy))l;h6LY556w}!Ldx~Qbj0RvU}z( zgT496aJ~3Sb{+|LAz{o$f~zLL#J>62&Y2QRSz3HH^P1{bth*MfPRQcfmP7`h#0W+s zD;6*2Ln$O?L-`af>IupGxww>Fd+cXu_QH%zv%x73>z~lma*BNaN=xJ#1!Gf?%XLtb zGkZXF@bP^c?tf3bKqpsQJR#nJW*7Oz)M|9!#B6}8r$E8H+o{?{K?62eW?98D&_4PR zu#3yg@whV`oQ9Tfi_^RwWmA?F;vQHZAkTcIL(60~nx{vgcsx$)xhtFRf0d<{f`Icj z+8>}nQi(#|?|tp_Pz!;TA1s|Fn~K$w`6|zqD|2#v4n3ZwOK@QK*Sc@L*h&Wl60~Nj z5LCX2L_3I;!u*urz`VF7|L_ZDrmwS3m*s+j7N3Xa=MZj)xcuH*&l!OT{m6d%iBuxa z_G+wlDh!qZu`+M6+7cX#r$9WcNpMhPG|d@}bU1B`Mo`RwjXdkTh;W@yHCHw{owOF% zK}GC^#=-|cmoE>4XYlK|X7%E+DrgP$i0U2?9Ebia?bR_B&13hnqBB_MtSK}Y6 zrW%!F^dPaXdT{|ld9U^ePxu0HF+Hx6y~kDw!v*LSaWQKX6x(E|w}WGv-ns~4S#SPG zxdrsNIT(hxWMqO7*IZZ1Sc%EV1iI!zbVL)Vfy{qgxtgj7vH8@R|ou)F&1$wG&>VqH3eD-o&_ap-YGJR1Bp5~~Yr%zgPmEbCrLE{1G#D}kp zo@LAEVVXMDCYO-_O7}RWVnibBZ}CMRNQ)^A&ZA;EJBH4BWiv;T_wL`EKGmA1Pt1!U zZgU!eId_dM5$NFJ0dW&gKF`Ev)*S%j%AhEl&!P&y=J97K!4+(7Z8ml1hVgN~4~ABb zZ4Rs>92!zz*xAK?J;RX*bW5RZfyCGxquDH53(z8g{H(Y*>5Ol33GU=BgPt09uGHR?BWU zsiNYQqxy);a0c=PBiN37J7|_sLR=+?DPWLr=5QQ^q|URfn)$H0xQurOJ0P%=9oz$s=0rVY!h5$BS%7T|3?tBPBIqbkL zo)b!p7PYm?R+%VVU=OpB_quLXIjq9n0ZmfS zoPeE`tUnJ{!)jma9=+~8v{#uA8SEVP3q5wH4Xg;Ro(Em404u)&M&F%YJa4mDU03LB zDPL6$#Kvv!J??z#OKtreBoeH{HIS+j{THYTJH&ffP!9x_z{R71D;N3n&@-UsTCgH$ zoO7W2kAe06?~nh*k7(*Qf5<*$K2ZBF-~Fq4srH<~fk=&yU~P?f?x@a1gK|6#?CLmh z0&+}tgl|np=;#Pp1c#jJjEu}NKQsqrL`>8`LrRoLLGy0|iTA)|R7Ko&=65p9QP z${|okvW7c(w77^}$*SiPZkbmbY&O`;>stQ6l}$45a9qk3h?A?)P-Yf+ZCk1y3HxxW znB}0VCfuM|Wn|zBR(6nzJz^G_;wBGNCT->7Np4zwC0^Wp5{cjZ^5MICGdj+7K?uax^`G*NAB-t|UR5zWxo5 zURUs6r+2ag=BtfiJ=|nV=(^!y6lEOda1cjGNHt0u%6RytgZ%s(Q{e0rKs=%B!8F?G zL_*~x*s_3l0C{GCp0bon1h$L$vgc|Ga-|5wSW6%?n;zjl@wh8KxkioT^v9KPkT_}@ z``{0}+o1vbS+O@>)V4JAwl}$4I#5r)~MD;rz!Zzr#jLl(kMzvwVr> z27lfaFW3Fzq*>+}0heDj0~rg=rh9(@$zclw67{8YXF$Bvb{PsS7@C)v;4?*yU3cpX zHO`tYUry7*6Jp+ccBQ_-%JCP}xJLXS*wUyKJP*o&Y`uL`)sWo-%mp4+L1AGghl!}; zo)K4^4$xF@9rvp-b6xG1+j~VJGiIoRj7ynJmOY5+)3XraI^D#es0MZbDzLSAN)34= zOliy--o;?!kxRuZW>z(b#Lw-AuppqbmMkC+^0N9s_MMl(TP9o_I}S#dt@!Z}Vx-gt z4j7=E3QxzQ%boW1!RyF64bscjyWV*k3Xs7hP@eG0%@=Nq6LuC=tHeetbhi zoT!V}YCjrL+0aMs)5Q6Z8QDZ&vJ*Q_yQ7?>yCFcMC{<)5{Pa~JL)8ynb6r${C| zCpuG&9`;**371%B$%_aW_)sWPtC>|cS~{_pjs(m&@<00;q&bhgJu6;;omsP_EO=Cd}AvGe#x~t;Nbp3Lmz!PGTxb$Y$DYYdg(Ojf3KWIKm}< zV-xFL>)}59MrB`q3ChIbr2&Y*%Cj5vL{}ON7)rnOr{m0MXEisOW`g<-9{6n_hF*!z z%vr9FFFbOLmTxe3m$O8mdcl(&@r=dH{t~+$;h;%pxli%17TeXf9*mv5-e+1Gm>A%h z;~WnKXQ|MBtT7M+g{r_*&6?P83lTP&n>UBTb_R>% zW>Qf|X0`FsDLURTELiKl#h*pajSp(lP@O|F7e_OQut~JI53>E>O9w` zeQYv|^_Uj-SUO}z55%%Medv~1uFlv0q(w}>i*yTP8c;IBx~}tzYufTVbWc3o;*4=sjSimD*%t$)40@v9OFNH2 zozuaT!SUUvEE^QiadXt=Q1w6t8xjJkA#^a&97GDJpSObtxRVlU;?*}u-1x=!bK_3j zS7F()nM4Fag5CTH(2|$^Dz@T1D95emifaUeA$AG^2uo5mg~kxd{P6n3qKbkiCS~5N z6nKeI5932VxX6+j;9A0MbMAqXTJ>eQ&_UA!JF7g~8fd?Ad?hJu;Pk-2i@bTflbO|# zn^<}HYAl{ezwtTd@Cvp+!`kBo)HME~;lpbIT@Pq3pLjLQn|-Kc9NFh*!^x!h#3DPR z?+Qz_%_hOAqyvUes*>aLmn_R4n;8&4T0<|d@Tk^}dFrO=q~e1hsoUSr^P6{0@hp*i z@Sr98ihyZB<%P@gd(%k@IA`7vISn5|&LpgJ{BHs1QdLkup!of}euH>9WN zvUGIwd9Zqp9vRc208UTFXIH&Ew-WVAX0U|k$m*mET8#w^U}-s!>UGva2hsnW&NzNc ziqZeddi@dcVh}?-Fkn^33z>{>m(m|;5Z@I`B)O5GG!$VzNWYau13iID$*zhq)`~k~ zSuPN(i>X<0)s0h09p29p0Dg*@9R>LIaDy;NOx`l3aU7K?$AikH84YJ)_!u9PNh``j zL54Xk9>M1>oa3ztuU$--LK6aw95h8}PEFiC#oc;WoSqO+J0FhSd4iqRX8Q08Vu3BT zpK;(oN0a}UpKVKkBvPrBqaxDn)?&V2Y8TwmgJ*Yix-2DWWR%uw4)KElbjxC!TPv;} z$mgN^N=tS{0~+oLvLH`DE-vpi90&Rsx9w&+1aP{23jBgh1X=A{PlFAETvpFKg!mOs z%*KKQz69G=1qrAgJiHHjsj0nOjex5kJcQfHvz5JqljMp6UlhV_^1`sS2R8VNP2(0TCX~Y9h$VvmLd~ z@d;=^pA_r8G@foGj0Cz$8V4CH!>7}R+RbOdd7jA?Rc)%2S2@DSqh-WXxnWnoyD;lv zY?rV%*`V~fjI7%+bXH6`8}F8j+OO=mSfJej`X6wtuWwdriHy_lQ*l{dY`3qu*ls{{ zkn4f>oAn1h^K|^3Z*#0l>@F*+2b(#38}^liyc-78tc$TVWrt9Hiby`9!co1Vlq~lJ3V>0wGdT&;~yG3HT`e zmn8y>f?*zS*rq)oIwYfdD@a}Z53lK&M%n8TuEv1z-@;@;-v5+|9I+pE^&8*sJ$JH{ zLjZ`|^HMp;8UnjJQqxic9|AEf0CcR`1+eR_44pk!!OntceLrX|7+8+(0<$hgz?VR= zyUI{z9!oiXs8tiMjHYc-J&Weya z5I;5&BfCSG6e))OwoLmGxr`)2Hwfu*_`K#eg zj359#mgi3rul~N2JYz3{C)_o4H2E8UE;G*#TaH4zAXDHh^^sx<%BG7Pf4eWP*5F~e zI1QnOCS84IH^=bUxdV8lVK+4KB_T)}{6SMa)^jCp0{nk7)guZ{w2XuIWWWy5OdEPQ zXzNuxIN?F!QRZKFfW7&`7&r&f)d>}rLgO13)-i@i#HA=HX3ST;{v`vc)1dvJ7WQrv z9n*CTj6S<@6bxvc2HOJSy1N)ic0C045Gx3%GuFmI7RLnu_rooK{(pJlOUZpaL;;q` zZQvJIZbkF($N;$U4A^KXn8e`Bg+q2W{W6HY`|#$|V&dXa6(a}k{%yD_CFX@|tYAHk zg90EPzkmi%uT)j_SUKR<3sQpbac!f2DmSA~>}jDNPyYJrEfBOcOJs}2Q|E3ywG4&D zfB1#t&euP?@p?mswF6jmH|U?g^cnA)FJ9?x+;Z!4%x>Ir5W)$1M0Ls|Z2gTOV5=)Ze$dLzznVJtj%%~Xu%B@3YG+OTv6Z)fkT8LM z8#dUu%gx-l^`wd!|MK{YnU;Tk{6BQgT^d3@%>d}23#DpgaU=H60EfN=tt`WPawyQ7 z+5f-zYp^J&6U0Wi4uEYQ-+lyQEh@XZXpWF>-24I82Y3=fe@T7xZSdjCs_A7mq*(sZ z<`m7(MjQh%DA1I<3RaIgl9^Pq%8=a(8cK6Jk8KP1nMY$SI-Q<67XlZh$cTIWQg2Z) zdV-ZFb%9nUWvs<)jEkXD%F(f)&~LBHL$WRa8x1J$E*E!0WL6e}AuaRMAA+;fru|32 zs}A*m+Xv&^&5nF&p2xxc;-gX=U4S_GH%lG7Ahko3Y{m5IA(ozTlH-vzqJuRa9{~sI zrSL(hnnGhLl{-{l$HC?V0pj&x$f~MC4adbd64V0ZZBhNfX&2cgGo;}1;n;7UeE}+P zea^+qEd4#)02XECcE#z@945#5I$R7CpTLP+?Fliv3i7wlHF7t`H89sfuxdrjIN8^r zP804vha_-r<_gakfO^nCPWA6V$qAV&9!y(ckfVKpZRcp955IGit*?FSQ@g5|{1I5y z`53s`4w`lejIn6sa0U6uVMfNoi@O+#yBvqvNH*#U9cJv{=2u^VT-f~q%i1K?VU==& zi)|1^JR~(5$NIC??Ba4Lbch(u-gEYym*j#gRrkI<4wddb!pLShFJ2+1R{^X6g;v3l z#O0T1h*O|dp-gifIU_vo0Gm##aS%b`A7A+_2jEG$h%Pi$U{nF#f9c%R`!A`JwO>A6 zOGlQV#)8Y)#WdEWx|TCR$Atp^_;cZU5HbQAz-YJM2CG~!X{%UIV@v$>@i{1$LEM!6 zwHE%JGPQv#NjNwVlxZ9(qXQ{BC}3msa7<(q5O017OyX=&Ta?-^NB796-)cN4v~)+XiOE z7gVibzb0P%Z49gbG~gpW9ioUH9uV?@go?ZY@pjWdKy{dS_DHUqzWWW+%=C679J91=R1FfjRC1oX zI;F}=&|<8J5wK^O9h$45JBbqwt8dtV9svC|lrR=Z2)piq&boi}Jl$>OULc)@P9-58 zw;srgM=KM-NHEGCV$9%iZq9x8e~EJat^$W1aP`V59_O}+3H$gEQ)n)z`bZoxvCNSM z6TjC%sy?G5?l&zt$D4Te0@88kcjOX(hxg9o;u|$GJ)3elN4O>r zerXkrq0SX9eelNxxz1@R)DUG=ieNq2n1Q*VIBOXwpz z31>|4;Z*xD(gAjMLEM>i&$43xG1wtf`DjEm89H}I+tqBK!S8+nY|bva;_i>m+6Nn( z`~3L!HFCK)Kc`9x3KvSu;%Vn-0t;9IPi2769*(b6;jBz<+H6_*HDI@k9!HO$ZCOd{ zh&PprMwP__QJYh|3Y1`YN*CVy8U!#`=muvafBqcU4Cr!GNX#ArRoDeGK?j*F2;z^95n8#e{gRXLb|Ee=6XM(N2}_1*bS{i^*bIdHhMJ z$}v1hG&Wt<@Ix>gC_y^{v*OulBY?-o-}^k$H4QF(fnkES`rwOTtWs3P0&^|9ahc2W z_rJXIh5%B=%0tQ#w=I>L6|K+cV)nUeMvfwOzxi3{G{s5Q;A5xqwqfMZEdg6vJSY)d z=~&@^wp*;C@}E);hp`bgoK&YQ4zT$eM-0C|u{C2; z#=-)iTM9deMkmJ(yWduq%*88&_pAy8j=n<;XA!yaY-(g z+=;u%uRa%Bn`K2JIESZ03$rx0A*x$rO*)-z&8Jk^i!>RqDe7jw&}U9=4%Rnu1Hbqx zY#JIVzve0OP(bxVA|)p3@YS07Ue!w%A76^Bms*cX9uFQU^~3#LGC) zW&<_e*FGJmHC&T@;FUdZJ`Vw~8(15THIA8!pu?l8K$mVkxE>iPy!DlGuiBYefvC|C zs@I_cdsjyTQoC7LSJwwWIt$4@Sp=yy#gWG*dLYs3=AL_{!{lhsfRpDdz|<_*YNsIK zW?{f)DZmY$+J^8kIU`?~?QpYobK=q3R3x$qUouC!jD=cw7Bc1pD`Kv&2m0l-g)lu*KtaWqfRb@*I-N-7v1Bn@%N`I-vro#ki{~k2RSq$M2!|rej*15y zWU!xAMcuz44#6fZz@dHXJMLdT32Ea%0NfjSW+L7}g#69pp8?O(mnYp$zugpznD&+- zwOrN5OU3OcSAOP2!f6eI`rC7z&=OXmYaW6+w)$5~anj77(!nvl%i$c*7Qm;{Z(i;Vk!eE$`Qpv)EX z$Va;9vcaHpB}B1)5{rp~m}`xO@6b3}fOD1XK7i##7in4^8-eNl)k%nz6Mv}eMj#bs z3HDjAKqMM1-1!4&>wR2#k+wC8)zh%ZVdFvM9_VK%~Q-no$z z=O|opV-r2{`28P%U#S6OtynvW`%ls&g(WcYL%Qw$4V*B@WwPzTl~?rGb*6T?DAX+F zTD~S+U#vTK_e*P+L67SRpB`VicI#3FHFRJRy+?ETJ(>C9Hc3xez=V`&bwQmyaXsMD zrLv5O;HzEI8x;lPr?O(t4vP|#cIXBM|9%k%e*gXLH)pv)%B_7=fX%}y8L-|;} zkRAcO^_P{?#~@okz*!Kh*iEUN?EODR0`SG8RHorxcoM<2>NfHqBjMV|Lnolli)et{ zIF&5 zI;S~s0lLnKwpMB2r8Nd_DjJxv%6$H}8W&4xL>_Kf<|Q^_2Zx_vB~Souy#u_1y#y#X zP-8U4if2LXvz3P-jvryg#iM|bJeBM9t>Zd7LBJ4*!QrFlX&=CJQsqF@-+KZ)r#epA z_}6N*vW{~Hdce1;-*BO>X>&8$Je=Z2|E@b>Ia_>s8y!fZPKtN`9FpEpL}w)XhSjO+`bWL44x;>S91Z7VcTO}%c^%}CiqdQ(ZSLo zu%v#yUS;_qBXoVh)p+;G#jcN@KD1%@=)ui9AGE{ji?a17A<&ViI`$`!zW^VuI4bvh$EloHtZsFXi_lA}^Wjhw#a%`RlP>BAY9 zIi{Jdjz%^o8nc7blO>wE*#wF3aD{2hS?CTqD|4S4dT*gN|Jq~XCn+04*ParNuwQ%~ zY(0n5`E3oQ>oq6_*Y1hpcs(h2}GPg;;cf+GCi?7=vM_ideHnFX&!M(3i zq2_2FxP1dsObH?0r))yX;67DZ417w3MsJC=>gn^4Hy;c4WgvUQx>m-qn6+90HJwlZ zZMf1aHJ87mAu`1h6CksfZpf9wv_F8)B)2LJLQF|&z@<5Fp_xJ_df?`Wx5Glm7V{Ut+qWD(jz8jRNL5Km`7 zaplJ+dsmiZLIE@M?oGZqTUqvit%|E!K>%h-OZ&5;qgV*QRsiCOCo9`-18LsN9FT)r zgKv>pV;!_-!C?mnw0^C|FFtPD8<}^=yyo;o!Mr?x#D=EwI*ZKo_`R$O(3fw&_bg(2g3kVt?u-yEVR3sM<95DL!tT)M=1u<;`hcF;M z@!V_7s?I?UHDjR?oB<<9qxWxhWUFbE7H*XT?PdVBf>^TdDg)o@^HDSpYVBgG`|9~) zmR|Of-p~;CnBM*o^#TZ>VF0#hGn+UwOY76)%GlT@Py$4waaAtly4E1~zjD%Gn_yR3 zxKLThH&!C5vez2a7!bSrRg+nmC0nXviuwsZ$Gt?46*7|t>t!uyK>DP_x%K6P7Uc=2 zz4OXI0k@H3$fHv<#hnhQGa!j`+)>%Q z0j`y~=6t@|K;{GzKDn4)-RYkZ@13YU`)ZZ{>pcc*i>GL56IKLpwS nTDb%v`fN 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 +Current page used as a test helper + diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html new file mode 100644 index 00000000000000..a885b8a0a734b2 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html @@ -0,0 +1,14 @@ + +Incumbent page used as a test helper + + + + diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html new file mode 100644 index 00000000000000..df60e72507ffdf --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ + +Request constructor URL parsing, with multiple globals in play + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js b/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js new file mode 100644 index 00000000000000..31a08eaa929bea --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js @@ -0,0 +1,83 @@ +// META: global=window,worker + +// list of bad ports according to +// https://fetch.spec.whatwg.org/#port-blocking +var BLOCKED_PORTS_LIST = [ + 1, // tcpmux + 7, // echo + 9, // discard + 11, // systat + 13, // daytime + 15, // netstat + 17, // qotd + 19, // chargen + 20, // ftp-data + 21, // ftp + 22, // ssh + 23, // telnet + 25, // smtp + 37, // time + 42, // name + 43, // nicname + 53, // domain + 77, // priv-rjs + 79, // finger + 87, // ttylink + 95, // supdup + 101, // hostriame + 102, // iso-tsap + 103, // gppitnp + 104, // acr-nema + 109, // pop2 + 110, // pop3 + 111, // sunrpc + 113, // auth + 115, // sftp + 117, // uucp-path + 119, // nntp + 123, // ntp + 135, // loc-srv / epmap + 139, // netbios + 143, // imap2 + 179, // bgp + 389, // ldap + 427, // afp (alternate) + 465, // smtp (alternate) + 512, // print / exec + 513, // login + 514, // shell + 515, // printer + 526, // tempo + 530, // courier + 531, // chat + 532, // netnews + 540, // uucp + 548, // afp + 556, // remotefs + 563, // nntp+ssl + 587, // smtp (outgoing) + 601, // syslog-conn + 636, // ldap+ssl + 993, // ldap+ssl + 995, // pop3+ssl + 1720, // h323hostcall + 1723, // pptp + 2049, // nfs + 3659, // apple-sasl + 4045, // lockd + 5060, // sip + 5061, // sips + 6000, // x11 + 6665, // irc (alternate) + 6666, // irc (alternate) + 6667, // irc (default) + 6668, // irc (alternate) + 6669, // irc (alternate) + 6697, // irc+tls +]; + +BLOCKED_PORTS_LIST.map(function(a){ + promise_test(function(t){ + return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a)) + }, 'Request on bad port ' + a + ' should throw TypeError.'); +}); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js new file mode 100644 index 00000000000000..c5b2001cc8f6b0 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js @@ -0,0 +1,170 @@ +// META: global=window,worker +// META: title=Request cache - default with conditional requests +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js new file mode 100644 index 00000000000000..dfa8369c9a3719 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js @@ -0,0 +1,39 @@ +// META: global=window,worker +// META: title=Request cache - default +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses', + state: "stale", + request_cache: ["default", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists', + state: "fresh", + request_cache: ["default", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "stale", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "fresh", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js new file mode 100644 index 00000000000000..00dce096c72924 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js @@ -0,0 +1,67 @@ +// META: global=window,worker +// META: title=Request cache - force-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses', + state: "stale", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "stale", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "fresh", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "stale", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "fresh", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "stale", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "fresh", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js new file mode 100644 index 00000000000000..41fc22baf23ddd --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js @@ -0,0 +1,25 @@ +// META: global=window,worker +// META: title=Request cache : no-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache', + state: "stale", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, + { + name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache', + state: "fresh", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js new file mode 100644 index 00000000000000..9a28718bf2292d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: title=Request cache - no store +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "stale", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "fresh", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js new file mode 100644 index 00000000000000..1305787c7c1d66 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js @@ -0,0 +1,66 @@ +// META: global=window,dedicatedworker,sharedworker +// META: title=Request cache - only-if-cached +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +// FIXME: avoid mixed content requests to enable service worker global +var tests = [ + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses', + state: "stale", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found', + state: "fresh", + request_cache: ["only-if-cached"], + response: ["error"], + expected_validation_headers: [], + expected_no_cache_headers: [] + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js new file mode 100644 index 00000000000000..c7bfffb398890d --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js @@ -0,0 +1,51 @@ +// META: global=window,worker +// META: title=Request cache - reload +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "stale", + request_cache: ["reload", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "fresh", + request_cache: ["reload", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "stale", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false, true], + expected_no_cache_headers: [false, true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "fresh", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache.js b/test/fixtures/wpt/fetch/api/request/request-cache.js new file mode 100644 index 00000000000000..f2fbecf4969291 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-cache.js @@ -0,0 +1,223 @@ +/** + * 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_js(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"); + }); +} diff --git a/test/fixtures/wpt/fetch/api/request/request-clone.sub.html b/test/fixtures/wpt/fetch/api/request/request-clone.sub.html new file mode 100644 index 00000000000000..c690bb3dc03653 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-clone.sub.html @@ -0,0 +1,63 @@ + + + + + Request clone + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js b/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js new file mode 100644 index 00000000000000..034a86041a74f5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js @@ -0,0 +1,101 @@ +// META: global=window,worker +// META: title=Request consume empty bodies + +function checkBodyText(test, request) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyBlob(test, request) { + return request.blob().then(function(bodyAsBlob) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); + }); +} + +function checkBodyArrayBuffer(test, request) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyJSON(test, request) { + return request.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormData(test, request) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormDataError(test, request) { + return promise_rejects_js(test, TypeError, request.formData()).then(function() { + assert_false(request.bodyUsed); + }); +} + +function checkRequestWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "headers": headers}); + assert_false(request.bodyUsed); + return checkFunction(test, request); + }, "Consume request's body as " + bodyType); +} + +checkRequestWithNoBody("text", checkBodyText); +checkRequestWithNoBody("blob", checkBodyBlob); +checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkRequestWithNoBody("json (error case)", checkBodyJSON); +checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkRequestWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body}); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return request.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer")); +} + +// FIXME: Add BufferSource, FormData and URLSearchParams. +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkRequestWithEmptyBody("text", "", false); +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkRequestWithEmptyBody("text", "", true); +checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950. +checkRequestWithEmptyBody("FormData", new FormData(), true); +checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/test/fixtures/wpt/fetch/api/request/request-consume.any.js b/test/fixtures/wpt/fetch/api/request/request-consume.any.js new file mode 100644 index 00000000000000..aff5d65244a15e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-consume.any.js @@ -0,0 +1,145 @@ +// META: global=window,worker +// META: title=Request consume +// META: script=../resources/utils.js + +function checkBodyText(request, expectedBody) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as text: bodyUsed turned true"); + }); +} + +function checkBodyBlob(request, expectedBody, checkContentType) { + return request.blob().then(function(bodyAsBlob) { + if (checkContentType) + assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type"); + + var promise = new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as blob: bodyUsed turned true"); + }); + }); +} + +function checkBodyArrayBuffer(request, expectedBody) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); + }); +} + +function checkBodyJSON(request, expectedBody) { + return request.json().then(function(bodyAsJSON) { + var strBody = JSON.stringify(bodyAsJSON) + assert_equals(strBody, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as json: bodyUsed turned true"); + }); +} + +function checkBodyFormData(request, expectedBody) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_true(request.bodyUsed, "body as formData: bodyUsed turned true"); + }); +} + +function checkRequestBody(body, expected, bodyType) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyText(request, expected); + }, "Consume " + bodyType + " request's body as text"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyBlob(request, expected); + }, "Consume " + bodyType + " request's body as blob"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyArrayBuffer(request, expected); + }, "Consume " + bodyType + " request's body as arrayBuffer"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyJSON(request, expected); + }, "Consume " + bodyType + " request's body as JSON"); +} + +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); + +checkRequestBody(textData, textData, "String"); + +var string = "\"123456\""; +function getArrayBuffer() { + var arrayBuffer = new ArrayBuffer(8); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr] = string.charCodeAt(cptr); + return arrayBuffer; +} + +function getArrayBufferWithZeros() { + var arrayBuffer = new ArrayBuffer(10); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr + 1] = string.charCodeAt(cptr); + return arrayBuffer; +} + +checkRequestBody(getArrayBuffer(), string, "ArrayBuffer"); +checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array"); +checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array"); +checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array"); +checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView"); + +promise_test(function(test) { + var formData = new FormData(); + formData.append("name", "value") + var request = new Request("", {"method": "POST", "body": formData }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyFormData(request, formData); +}, "Consume FormData request's body as FormData"); + +function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) { + promise_test(function(test) { + var response = new Response(blobBody); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + return checkFunction(response, blobData); + }, "Consume blob response's body as " + bodyType); +} + +checkBlobResponseBody(blob, textData, "blob", checkBodyBlob); +checkBlobResponseBody(blob, textData, "text", checkBodyText); +checkBlobResponseBody(blob, textData, "json", checkBodyJSON); +checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer); +checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob); + +var goodJSONValues = ["null", "1", "true", "\"string\""]; +goodJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return request.json().then(function(v) { + assert_equals(v, JSON.parse(value)); + }); + }, "Consume JSON from text: '" + JSON.stringify(value) + "'"); +}); + +var badJSONValues = ["undefined", "{", "a", "["]; +badJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return promise_rejects_js(test, SyntaxError, request.json()); + }, "Trying to consume bad JSON text as JSON: '" + value + "'"); +}); diff --git a/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js b/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js new file mode 100644 index 00000000000000..8a11de78ff6e0e --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js @@ -0,0 +1,109 @@ +// META: global=window,worker +// META: title=Request disturbed +// META: script=../resources/utils.js + +var initValuesDict = {"method" : "POST", + "body" : "Request's body" +}; + +var noBodyConsumed = new Request(""); +var bodyConsumed = new Request("", initValuesDict); + +test(() => { + assert_equals(noBodyConsumed.body, null, "body's default value is null"); + assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); + assert_not_equals(bodyConsumed.body, null, "non-null body"); + assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type"); + assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed"); +}, "Request's body: initial state"); + +noBodyConsumed.blob(); +bodyConsumed.blob(); + +test(function() { + assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); + try { + noBodyConsumed.clone(); + } catch (e) { + assert_unreached("Can use request not disturbed for creating or cloning request"); + } +}, "Request without body cannot be disturbed"); + +test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_throws_js(TypeError, function() { bodyConsumed.clone(); }); +}, "Check cloning a disturbed request"); + +test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_throws_js(TypeError, function() { new Request(bodyConsumed); }); +}, "Check creating a new request from a disturbed request"); + +promise_test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + const originalBody = bodyConsumed.body; + const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" }); + assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new"); + assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed"); + return bodyReplaced.text().then(text => { + assert_equals(text, "Replaced body"); + }); +}, "Check creating a new request with a new body from a disturbed request"); + +promise_test(function() { + var bodyRequest = new Request("", initValuesDict); + const originalBody = bodyRequest.body; + assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); + var requestFromRequest = new Request(bodyRequest); + assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_equals(bodyRequest.body, originalBody, "body should not change"); + assert_not_equals(originalBody, undefined, "body should not be undefined"); + assert_not_equals(originalBody, null, "body should not be null"); + assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); + return requestFromRequest.text().then(text => { + assert_equals(text, "Request's body"); + }); +}, "Input request used for creating new request became disturbed"); + +promise_test(() => { + const bodyRequest = new Request("", initValuesDict); + const originalBody = bodyRequest.body; + assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); + const requestFromRequest = new Request(bodyRequest, { body : "init body" }); + assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_equals(bodyRequest.body, originalBody, "body should not change"); + assert_not_equals(originalBody, undefined, "body should not be undefined"); + assert_not_equals(originalBody, null, "body should not be null"); + assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); + + return requestFromRequest.text().then(text => { + assert_equals(text, "init body"); + }); +}, "Input request used for creating new request became disturbed even if body is not used"); + +promise_test(function(test) { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + return promise_rejects_js(test, TypeError, bodyConsumed.blob()); +}, "Check consuming a disturbed request"); + +test(function() { + var req = new Request(URL, {method: 'POST', body: 'hello'}); + assert_false(req.bodyUsed, + 'Request should not be flagged as used if it has not been ' + + 'consumed.'); + assert_throws_js(TypeError, + function() { new Request(req, {method: 'GET'}); }, + 'A get request may not have body.'); + + assert_false(req.bodyUsed, 'After the GET case'); + + assert_throws_js(TypeError, + function() { new Request(req, {method: 'CONNECT'}); }, + 'Request() with a forbidden method must throw.'); + + assert_false(req.bodyUsed, 'After the forbidden method case'); + + var req2 = new Request(req); + assert_true(req.bodyUsed, + 'Request should be flagged as used if it has been consumed.'); +}, 'Request construction failure should not set "bodyUsed"'); diff --git a/test/fixtures/wpt/fetch/api/request/request-error.any.js b/test/fixtures/wpt/fetch/api/request/request-error.any.js new file mode 100644 index 00000000000000..67ed70c5db2f09 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-error.any.js @@ -0,0 +1,48 @@ +// META: global=window,worker +// META: title=Request error +// META: script=request-error.js + +// badRequestArgTests is from response-error.js +for (const { args, testName } of badRequestArgTests) { + test(() => { + assert_throws_js( + TypeError, + () => new Request(...args), + "Expect TypeError exception" + ); + }, testName); +} + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from the init request"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var headers = new Headers([]); + var request = new Request(initialRequest, {"headers" : headers}); + assert_false(request.headers.has("Content-Type")); +}, "Request should not get its content-type from the init request if init headers are provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); +}, "Request should get its content-type from the body if none is provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from init headers if one is provided"); + +test(function() { + var options = {"cache": "only-if-cached", "mode": "same-origin"}; + new Request("test", options); +}, "Request with cache mode: only-if-cached and fetch mode: same-origin"); diff --git a/test/fixtures/wpt/fetch/api/request/request-error.js b/test/fixtures/wpt/fetch/api/request/request-error.js new file mode 100644 index 00000000000000..cf77313f5bc309 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-error.js @@ -0,0 +1,57 @@ +const badRequestArgTests = [ + { + args: ["", { "window": "http://test.url" }], + testName: "RequestInit's window is not null" + }, + { + args: ["http://:not a valid URL"], + testName: "Input URL is not valid" + }, + { + args: ["http://user:pass@test.url"], + testName: "Input URL has credentials" + }, + { + args: ["", { "mode": "navigate" }], + testName: "RequestInit's mode is navigate" + }, + { + args: ["", { "referrer": "http://:not a valid URL" }], + testName: "RequestInit's referrer is invalid" + }, + { + args: ["", { "method": "IN VALID" }], + testName: "RequestInit's method is invalid" + }, + { + args: ["", { "method": "TRACE" }], + testName: "RequestInit's method is forbidden" + }, + { + args: ["", { "mode": "no-cors", "method": "PUT" }], + testName: "RequestInit's mode is no-cors and method is not simple" + }, + { + args: ["", { "mode": "cors", "cache": "only-if-cached" }], + testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin" + }, + { + args: ["test", { "cache": "only-if-cached", "mode": "cors" }], + testName: "Request with cache mode: only-if-cached and fetch mode cors" + }, + { + args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }], + testName: "Request with cache mode: only-if-cached and fetch mode no-cors" + } +]; + +badRequestArgTests.push( + ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => { + const options = {}; + options[optionProp] = "BAD"; + return { + args: ["", options], + testName: `Bad ${optionProp} init parameter value` + }; + }) +); diff --git a/test/fixtures/wpt/fetch/api/request/request-headers.any.js b/test/fixtures/wpt/fetch/api/request/request-headers.any.js new file mode 100644 index 00000000000000..c48e24e5bc63f6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-headers.any.js @@ -0,0 +1,173 @@ +// META: global=window,worker +// META: title=Request Headers + +var validRequestHeaders = [ + ["Content-Type", "OK"], + ["Potato", "OK"], + ["proxy", "OK"], + ["proxya", "OK"], + ["sec", "OK"], + ["secb", "OK"], +]; +var invalidRequestHeaders = [ + ["Accept-Charset", "KO"], + ["accept-charset", "KO"], + ["ACCEPT-ENCODING", "KO"], + ["Accept-Encoding", "KO"], + ["Access-Control-Request-Headers", "KO"], + ["Access-Control-Request-Method", "KO"], + ["Connection", "KO"], + ["Content-Length", "KO"], + ["Cookie", "KO"], + ["Cookie2", "KO"], + ["Date", "KO"], + ["DNT", "KO"], + ["Expect", "KO"], + ["Host", "KO"], + ["Keep-Alive", "KO"], + ["Origin", "KO"], + ["Referer", "KO"], + ["TE", "KO"], + ["Trailer", "KO"], + ["Transfer-Encoding", "KO"], + ["Upgrade", "KO"], + ["Via", "KO"], + ["Proxy-", "KO"], + ["proxy-a", "KO"], + ["Sec-", "KO"], + ["sec-b", "KO"], +]; + +var validRequestNoCorsHeaders = [ + ["Accept", "OK"], + ["Accept-Language", "OK"], + ["content-language", "OK"], + ["content-type", "application/x-www-form-urlencoded"], + ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"], + ["content-type", "multipart/form-data"], + ["content-type", "multipart/form-data;charset=UTF-8"], + ["content-TYPE", "text/plain"], + ["CONTENT-type", "text/plain;charset=UTF-8"], +]; +var invalidRequestNoCorsHeaders = [ + ["Content-Type", "KO"], + ["Potato", "KO"], + ["proxy", "KO"], + ["proxya", "KO"], + ["sec", "KO"], + ["secb", "KO"], +]; + +validRequestHeaders.forEach(function(header) { + test(function() { + var request = new Request(""); + request.headers.set(header[0], header[1]); + assert_equals(request.headers.get(header[0]), header[1]); + }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\""); +}); +invalidRequestHeaders.forEach(function(header) { + test(function() { + var request = new Request(""); + request.headers.set(header[0], header[1]); + assert_equals(request.headers.get(header[0]), null); + }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\""); +}); + +validRequestNoCorsHeaders.forEach(function(header) { + test(function() { + var requestNoCors = new Request("", {"mode": "no-cors"}); + requestNoCors.headers.set(header[0], header[1]); + assert_equals(requestNoCors.headers.get(header[0]), header[1]); + }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\""); +}); +invalidRequestNoCorsHeaders.forEach(function(header) { + test(function() { + var requestNoCors = new Request("", {"mode": "no-cors"}); + requestNoCors.headers.set(header[0], header[1]); + assert_equals(requestNoCors.headers.get(header[0]), null); + }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\""); +}); + +test(function() { + var headers = new Headers([["Cookie2", "potato"]]); + var request = new Request("", {"headers": headers}); + assert_equals(request.headers.get("Cookie2"), null); +}, "Check that request constructor is filtering headers provided as init parameter"); + +test(function() { + var headers = new Headers([["Content-Type", "potato"]]); + var request = new Request("", {"headers": headers, "mode": "no-cors"}); + assert_equals(request.headers.get("Content-Type"), null); +}, "Check that no-cors request constructor is filtering headers provided as init parameter"); + +test(function() { + var headers = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers": headers}); + var request = new Request(initialRequest, {"mode": "no-cors"}); + assert_equals(request.headers.get("Content-Type"), null); +}, "Check that no-cors request constructor is filtering headers provided as part of request parameter"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from the init request"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var headers = new Headers([]); + var request = new Request(initialRequest, {"headers" : headers}); + assert_false(request.headers.has("Content-Type")); +}, "Request should not get its content-type from the init request if init headers are provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); +}, "Request should get its content-type from the body if none is provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from init headers if one is provided"); + +test(function() { + var array = [["hello", "worldAHH"]]; + var object = {"hello": 'worldOOH'}; + var headers = new Headers(array); + + assert_equals(headers.get("hello"), "worldAHH"); + + var request1 = new Request("", {"headers": headers}); + var request2 = new Request("", {"headers": array}); + var request3 = new Request("", {"headers": object}); + + assert_equals(request1.headers.get("hello"), "worldAHH"); + assert_equals(request2.headers.get("hello"), "worldAHH"); + assert_equals(request3.headers.get("hello"), "worldOOH"); +}, "Testing request header creations with various objects"); + +promise_test(function(test) { + var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"}); + return request.blob().then(function(blob) { + assert_equals(blob.type, "", "Blob type should be the empty string"); + }); +}, "Testing empty Request Content-Type header"); + +test(function() { + const request1 = new Request(""); + assert_equals(request1.headers, request1.headers); + + const request2 = new Request("", {"headers": {"X-Foo": "bar"}}); + assert_equals(request2.headers, request2.headers); + const headers = request2.headers; + request2.headers.set("X-Foo", "quux"); + assert_equals(headers, request2.headers); + headers.set("X-Other-Header", "baz"); + assert_equals(headers, request2.headers); +}, "Test that Request.headers has the [SameObject] extended attribute"); diff --git a/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html new file mode 100644 index 00000000000000..ea302d623d541f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html @@ -0,0 +1,100 @@ + + + + + Request init: simple cases + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-init-002.any.js b/test/fixtures/wpt/fetch/api/request/request-init-002.any.js new file mode 100644 index 00000000000000..abb6689f1e844a --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-init-002.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker +// META: title=Request init: headers and body + +test(function() { + var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3" + }; + var headers = new Headers(headerDict); + var request = new Request("", { "headers" : headers }) + for (var name in headerDict) { + assert_equals(request.headers.get(name), headerDict[name], + "request's headers has " + name + " : " + headerDict[name]); + } +}, "Initialize Request with headers values"); + +function makeRequestInit(body, method) { + return {"method": method, "body": body}; +} + +function checkRequestInit(body, bodyType, expectedTextBody) { + promise_test(function(test) { + var request = new Request("", makeRequestInit(body, "POST")); + if (body) { + assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); }); + assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); }); + } else { + new Request("", makeRequestInit(body, "GET")); // should not throw + } + var reqHeaders = request.headers; + var mime = reqHeaders.get("Content-Type"); + assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\""); + return request.text().then(function(bodyAsText) { + //not equals: cannot guess formData exact value + assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body"); + }); + }, `Initialize Request's body with "${body}", ${bodyType}`); +} + +var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); +var formaData = new FormData(); +formaData.append("name", "value"); +var usvString = "This is a USVString" + +checkRequestInit(undefined, undefined, ""); +checkRequestInit(null, null, ""); +checkRequestInit(blob, "application/octet-binary", "This is a blob"); +checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); +checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); +checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!"); + +// Ensure test does not time out in case of missing URLSearchParams support. +if (self.URLSearchParams) { + var urlSearchParams = new URLSearchParams("name=value"); + checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); +} else { + promise_test(function(test) { + return Promise.reject("URLSearchParams not supported"); + }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8"); +} diff --git a/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html new file mode 100644 index 00000000000000..79c91cdfe82af4 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html @@ -0,0 +1,84 @@ + + + + + Request: init with request or url + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js b/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js new file mode 100644 index 00000000000000..8c50c4929e75da --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js @@ -0,0 +1,57 @@ +// META: global=window,worker + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + const request = new Request("...", { method:"POST", body: stream }); + assert_equals(request.body, stream); +}, "Constructing a Request with a stream holds the original object."); + +async function assert_request(test, input, init) { + assert_throws_js(TypeError, () => new Request(input, init), "new Request()"); + await promise_rejects_js(test, TypeError, fetch(input, init), "fetch()"); +} + +promise_test(async (t) => { + const stream = new ReadableStream(); + stream.getReader(); + await assert_request(t, "...", { method:"POST", body: stream }); +}, "Constructing a Request with a stream on which getReader() is called"); + +promise_test(async (t) => { + const stream = new ReadableStream(); + stream.getReader().read(); + await assert_request(t, "...", { method:"POST", body: stream }); +}, "Constructing a Request with a stream on which read() is called"); + +promise_test(async (t) => { + const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }), + reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + await assert_request(t, "...", { method:"POST", body: stream }); +}, "Constructing a Request with a stream on which read() and releaseLock() are called"); + +promise_test(async (t) => { + const request = new Request("...", { method: "POST", body: "..." }); + request.body.getReader(); + await assert_request(t, request); + assert_class_string(new Request(request, { body: "..." }), "Request"); +}, "Constructing a Request with a Request on which body.getReader() is called"); + +promise_test(async (t) => { + const request = new Request("...", { method: "POST", body: "..." }); + request.body.getReader().read(); + await assert_request(t, request); + assert_class_string(new Request(request, { body: "..." }), "Request"); +}, "Constructing a Request with a Request on which body.getReader().read() is called"); + +promise_test(async (t) => { + const request = new Request("...", { method: "POST", body: "..." }), + reader = request.body.getReader(); + await reader.read(); + reader.releaseLock(); + await assert_request(t, request); + assert_class_string(new Request(request, { body: "..." }), "Request"); +}, "Constructing a Request with a Request on which read() and releaseLock() are called"); diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html b/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html new file mode 100644 index 00000000000000..548ab38d7e14d8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html @@ -0,0 +1,97 @@ + + + + + Request Keepalive Quota Tests + + + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js b/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js new file mode 100644 index 00000000000000..cb4506db46c931 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker +// META: title=Request keepalive +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +test(() => { + assert_false(new Request('/').keepalive, 'default'); + assert_true(new Request('/', {keepalive: true}).keepalive, 'true'); + assert_false(new Request('/', {keepalive: false}).keepalive, 'false'); + assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish'); + assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy'); +}, 'keepalive flag'); + +test(() => { + const init = {method: 'POST', keepalive: true, body: new ReadableStream()}; + assert_throws_js(TypeError, () => {new Request('/', init)}); +}, 'keepalive flag with stream body'); diff --git a/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html b/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html new file mode 100644 index 00000000000000..7be3608d737c34 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html @@ -0,0 +1,96 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/request/request-structure.any.js b/test/fixtures/wpt/fetch/api/request/request-structure.any.js new file mode 100644 index 00000000000000..65f1b96b2b9042 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/request-structure.any.js @@ -0,0 +1,128 @@ +// META: global=window,worker +// META: title=Request structure + +var request = new Request(""); +var methods = ["clone", + //Request implements Body + "arrayBuffer", + "blob", + "formData", + "json", + "text" + ]; +var attributes = ["method", + "url", + "headers", + "destination", + "referrer", + "referrerPolicy", + "mode", + "credentials", + "cache", + "redirect", + "integrity", + "isReloadNavigation", + "isHistoryNavigation", + //Request implements Body + "bodyUsed" + ]; + +function IsreadOnly(request, attributeToCheck) { + var defaultValue = undefined; + var newValue = undefined; + switch (attributeToCheck) { + case "method": + defaultValue = "GET"; + newValue = "POST"; + break; + + case "url": + //default value is base url + //i.e http://example.com/fetch/api/request-structure.html + newValue = "http://url.test"; + break; + + case "headers": + request.headers = new Headers ({"name":"value"}); + assert_false(request.headers.has("name"), "Headers attribute is read only"); + return; + break; + + case "destination": + defaultValue = ""; + newValue = "worker"; + break; + + case "referrer": + defaultValue = "about:client"; + newValue = "http://url.test"; + break; + + case "referrerPolicy": + defaultValue = ""; + newValue = "unsafe-url"; + break; + + case "mode": + defaultValue = "cors"; + newValue = "navigate"; + break; + + case "credentials": + defaultValue = "same-origin"; + newValue = "cors"; + break; + + case "cache": + defaultValue = "default"; + newValue = "reload"; + break; + + case "redirect": + defaultValue = "follow"; + newValue = "manual"; + break; + + case "integrity": + newValue = "CannotWriteIntegrity"; + break; + + case "bodyUsed": + defaultValue = false; + newValue = true; + break; + + case "isReloadNavigation": + defaultValue = false; + newValue = true; + break; + + case "isHistoryNavigation": + defaultValue = false; + newValue = true; + break; + + default: + return; + } + + request[attributeToCheck] = newValue; + if (defaultValue === undefined) + assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only"); + else + assert_equals(request[attributeToCheck], defaultValue, + "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue); +} + +for (var idx in methods) { + test(function() { + assert_true(methods[idx] in request, "request has " + methods[idx] + " method"); + }, "Request has " + methods[idx] + " method"); +} + +for (var idx in attributes) { + test(function() { + assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute"); + IsreadOnly(request, attributes[idx]); + }, "Check " + attributes[idx] + " attribute"); +} diff --git a/test/fixtures/wpt/fetch/api/request/resources/hello.txt b/test/fixtures/wpt/fetch/api/request/resources/hello.txt new file mode 100644 index 00000000000000..ce013625030ba8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/resources/hello.txt @@ -0,0 +1 @@ +hello diff --git a/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js b/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js new file mode 100644 index 00000000000000..4b264ca2fec3ba --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js @@ -0,0 +1,19 @@ +self.addEventListener('fetch', (event) => { + const params = new URL(event.request.url).searchParams; + if (params.has('ignore')) { + return; + } + if (!params.has('name')) { + event.respondWith(Promise.reject(TypeError('No name is provided.'))); + return; + } + + const name = params.get('name'); + const old_attribute = event.request[name]; + // If any of |init|'s member is present... + const init = {cache: 'no-store'} + const new_attribute = (new Request(event.request, init))[name]; + + event.respondWith( + new Response(`old: ${old_attribute}, new: ${new_attribute}`)); + }); diff --git a/test/fixtures/wpt/fetch/api/request/url-encoding.html b/test/fixtures/wpt/fetch/api/request/url-encoding.html new file mode 100644 index 00000000000000..31c1ed3920bf9f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/request/url-encoding.html @@ -0,0 +1,25 @@ + + +Fetch: URL encoding + + + diff --git a/test/fixtures/wpt/fetch/api/resources/basic.html b/test/fixtures/wpt/fetch/api/resources/basic.html new file mode 100644 index 00000000000000..e23afd4bf6a7ec --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/basic.html @@ -0,0 +1,5 @@ + + diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt b/test/fixtures/wpt/fetch/api/resources/cors-top.txt new file mode 100644 index 00000000000000..83a3157d14d908 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/cors-top.txt @@ -0,0 +1 @@ +top \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers b/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers new file mode 100644 index 00000000000000..cb762eff806849 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/test/fixtures/wpt/fetch/api/resources/data.json b/test/fixtures/wpt/fetch/api/resources/data.json new file mode 100644 index 00000000000000..76519fa8cc27ab --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/data.json @@ -0,0 +1 @@ +{"key": "value"} diff --git a/test/fixtures/wpt/fetch/api/resources/empty.txt b/test/fixtures/wpt/fetch/api/resources/empty.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html b/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html new file mode 100644 index 00000000000000..47de0da7790618 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html @@ -0,0 +1,25 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-window.html b/test/fixtures/wpt/fetch/api/resources/keepalive-window.html new file mode 100644 index 00000000000000..6ccf484644c9d8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/keepalive-window.html @@ -0,0 +1,34 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html new file mode 100644 index 00000000000000..6e5d5065474d47 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html @@ -0,0 +1,34 @@ + + + + diff --git a/test/fixtures/wpt/fetch/api/resources/sw-intercept.js b/test/fixtures/wpt/fetch/api/resources/sw-intercept.js new file mode 100644 index 00000000000000..b8166b62a5c939 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/sw-intercept.js @@ -0,0 +1,10 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); diff --git a/test/fixtures/wpt/fetch/api/resources/top.txt b/test/fixtures/wpt/fetch/api/resources/top.txt new file mode 100644 index 00000000000000..83a3157d14d908 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/top.txt @@ -0,0 +1 @@ +top \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/resources/utils.js b/test/fixtures/wpt/fetch/api/resources/utils.js new file mode 100644 index 00000000000000..dfd5c1404cb9b6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/resources/utils.js @@ -0,0 +1,103 @@ +var RESOURCES_DIR = "../resources/"; + +function dirname(path) { + return path.replace(/\/[^\/]*$/, '/') +} + +function checkRequest(request, ExpectedValuesDict) { + for (var attribute in ExpectedValuesDict) { + switch(attribute) { + case "headers": + for (var key in ExpectedValuesDict["headers"].keys()) { + assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key), + "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key)); + } + break; + + case "body": + //for checking body's content, a dedicated asyncronous/promise test should be used + assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header") + break; + + case "method": + case "referrer": + case "referrerPolicy": + case "credentials": + case "cache": + case "redirect": + case "integrity": + case "url": + case "destination": + assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute") + break; + + default: + break; + } + } +} + +function stringToArray(str) { + var array = new Uint8Array(str.length); + for (var i=0, strLen = str.length; i < strLen; i++) + array[i] = str.charCodeAt(i); + return array; +} + +function encode_utf8(str) +{ + if (self.TextEncoder) + return (new TextEncoder).encode(str); + return stringToArray(unescape(encodeURIComponent(str))); +} + +function validateBufferFromString(buffer, expectedValue, message) +{ + return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message); +} + +function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { + return reader.read().then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new ArrayBuffer(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromString(reader, expectedValue, newBuffer); + } + validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream"); + }); +} + +function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) { + return reader.read().then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new ArrayBuffer(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromPartialString(reader, expectedValue, newBuffer); + } + + var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer); + return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream"); + }); +} + +// From streams tests +function delay(milliseconds) +{ + return new Promise(function(resolve) { + step_timeout(resolve, milliseconds); + }); +} diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html b/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html new file mode 100644 index 00000000000000..9bb6e0bbf3f8eb --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html @@ -0,0 +1,3 @@ + +Current page used as a test helper + diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html new file mode 100644 index 00000000000000..f63372e64c2bef --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html @@ -0,0 +1,16 @@ + +Incumbent page used as a test helper + + + + + diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html b/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html new file mode 100644 index 00000000000000..44f42eda493c27 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html @@ -0,0 +1,2 @@ + +Relevant page used as a test helper diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html new file mode 100644 index 00000000000000..5f2f42a1cea52f --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ + +Response.redirect URL parsing, with multiple globals in play + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html b/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html new file mode 100644 index 00000000000000..c2c90eaa8bd951 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js new file mode 100644 index 00000000000000..baa46de4039b17 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js @@ -0,0 +1,57 @@ +// META: global=window,worker +// META: title=Response consume blob and http bodies +// META: script=../resources/utils.js + +promise_test(function(test) { + return new Response(new Blob([], { "type" : "text/plain" })).body.cancel(); +}, "Cancelling a starting blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["This is data"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + reader.read(); + return reader.cancel(); +}, "Cancelling a loading blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["T"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + + var closedPromise = reader.closed.then(function() { + return reader.cancel(); + }); + reader.read().then(function readMore({done, value}) { + if (!done) return reader.read().then(readMore); + }); + return closedPromise; +}, "Cancelling a closed blob Response stream"); + +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + return response.body.cancel(); + }); +}, "Cancelling a starting Response stream"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + var reader = response.body.getReader(); + return reader.read().then(function() { + return reader.cancel(); + }); + }); +}, "Cancelling a loading Response stream"); + +promise_test(function() { + async function readAll(reader) { + while (true) { + const {value, done} = await reader.read(); + if (done) + return; + } + } + + return fetch(RESOURCES_DIR + "top.txt").then(function(response) { + var reader = response.body.getReader(); + return readAll(reader).then(() => reader.cancel()); + }); +}, "Cancelling a closed Response stream"); diff --git a/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js b/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js new file mode 100644 index 00000000000000..da54616c376d91 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js @@ -0,0 +1,32 @@ +// Verify that calling Response clone() in a detached iframe doesn't crash. +// Regression test for https://crbug.com/1082688. + +'use strict'; + +promise_test(async () => { + // Wait for the document body to be available. + await new Promise(resolve => { + onload = resolve; + }); + + window.iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.srcdoc = ` + +`; + + await new Promise(resolve => { + onmessage = evt => { + if (evt.data === 'okay') { + resolve(); + } + }; + }); + + // If it got here without crashing, the test passed. +}, 'clone within removed iframe should not crash'); diff --git a/test/fixtures/wpt/fetch/api/response/response-clone.any.js b/test/fixtures/wpt/fetch/api/response/response-clone.any.js new file mode 100644 index 00000000000000..7cc8f205fa55d3 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-clone.any.js @@ -0,0 +1,124 @@ +// META: global=window,worker +// META: title=Response clone +// META: script=../resources/utils.js + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "" +}; + +var response = new Response(); +var clonedResponse = response.clone(); +test(function() { + for (var attributeName in defaultValues) { + var expectedValue = defaultValues[attributeName]; + assert_equals(clonedResponse[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + } +}, "Check Response's clone with default values, without body"); + +var body = "This is response body"; +var headersInit = { "name" : "value" }; +var responseInit = { "status" : 200, + "statusText" : "GOOD", + "headers" : headersInit +}; +var response = new Response(body, responseInit); +var clonedResponse = response.clone(); +test(function() { + assert_equals(clonedResponse.status, responseInit["status"], + "Expect response.status is " + responseInit["status"]); + assert_equals(clonedResponse.statusText, responseInit["statusText"], + "Expect response.statusText is " + responseInit["statusText"]); + assert_equals(clonedResponse.headers.get("name"), "value", + "Expect response.headers has name:value header"); +}, "Check Response's clone has the expected attribute values"); + +promise_test(function(test) { + return validateStreamFromString(response.body.getReader(), body); +}, "Check orginal response's body after cloning"); + +promise_test(function(test) { + return validateStreamFromString(clonedResponse.body.getReader(), body); +}, "Check cloned response's body"); + +promise_test(function(test) { + var disturbedResponse = new Response("data"); + return disturbedResponse.text().then(function() { + assert_true(disturbedResponse.bodyUsed, "response is disturbed"); + assert_throws_js(TypeError, function() { disturbedResponse.clone(); }, + "Expect TypeError exception"); + }); +}, "Cannot clone a disturbed response"); + +promise_test(function(t) { + var clone; + var result; + var response; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + response = res; + return clone.text(); + }).then(function(r) { + assert_equals(r.length, 26); + result = r; + return response.text(); + }).then(function(r) { + assert_equals(r, result, "cloned responses should provide the same data"); + }); + }, 'Cloned responses should provide the same data'); + +promise_test(function(t) { + var clone; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + res.body.cancel(); + assert_true(res.bodyUsed); + assert_false(clone.bodyUsed); + return clone.arrayBuffer(); + }).then(function(r) { + assert_equals(r.byteLength, 26); + assert_true(clone.bodyUsed); + }); +}, 'Cancelling stream should not affect cloned one'); + +function testReadableStreamClone(initialBuffer, bufferType) +{ + promise_test(function(test) { + var response = new Response(new ReadableStream({start : function(controller) { + controller.enqueue(initialBuffer); + controller.close(); + }})); + + var clone = response.clone(); + var stream1 = response.body; + var stream2 = clone.body; + + var buffer; + return stream1.getReader().read().then(function(data) { + assert_false(data.done); + assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer"); + return stream2.getReader().read(); + }).then(function(data) { + assert_false(data.done); + assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); + assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type"); + assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer"); + }); + }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)"); +} + +var arrayBuffer = new ArrayBuffer(16); +testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array"); +testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array"); +testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array"); +testReadableStreamClone(arrayBuffer, "ArrayBuffer"); +testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array"); +testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray"); +testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array"); +testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array"); +testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array"); +testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array"); +testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView"); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js new file mode 100644 index 00000000000000..0fa85ecbcb2d7b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js @@ -0,0 +1,99 @@ +// META: global=window,worker +// META: title=Response consume empty bodies + +function checkBodyText(test, response) { + return response.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyBlob(test, response) { + return response.blob().then(function(bodyAsBlob) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); + }); +} + +function checkBodyArrayBuffer(test, response) { + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyJSON(test, response) { + return response.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormData(test, response) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormDataError(test, response) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_false(response.bodyUsed); + }); +} + +function checkResponseWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var response = new Response(undefined, { "headers": headers }); + assert_false(response.bodyUsed); + return checkFunction(test, response); + }, "Consume response's body as " + bodyType); +} + +checkResponseWithNoBody("text", checkBodyText); +checkResponseWithNoBody("blob", checkBodyBlob); +checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkResponseWithNoBody("json (error case)", checkBodyJSON); +checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkResponseWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var response = new Response(body); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return response.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer")); +} + +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkResponseWithEmptyBody("text", "", false); +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkResponseWithEmptyBody("text", "", true); +checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +checkResponseWithEmptyBody("FormData", new FormData(), true); +checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js new file mode 100644 index 00000000000000..d5b0c388cc75b6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker +// META: title=Response consume +// META: script=../resources/utils.js + +promise_test(function(test) { + var body = ""; + var response = new Response(""); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty text response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(new Blob([], { "type" : "text/plain" })); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty blob response's body as readableStream"); + +var formData = new FormData(); +formData.append("name", "value"); +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); +var urlSearchParamsData = "name=value"; +var urlSearchParams = new URLSearchParams(urlSearchParamsData); + +promise_test(function(test) { + var response = new Response(blob); + return validateStreamFromString(response.body.getReader(), textData); +}, "Read blob response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(textData); + return validateStreamFromString(response.body.getReader(), textData); +}, "Read text response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(urlSearchParams); + return validateStreamFromString(response.body.getReader(), urlSearchParamsData); +}, "Read URLSearchParams response's body as readableStream"); + +promise_test(function(test) { + var arrayBuffer = new ArrayBuffer(textData.length); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < textData.length; cptr++) + int8Array[cptr] = textData.charCodeAt(cptr); + + return validateStreamFromString(new Response(arrayBuffer).body.getReader(), textData); +}, "Read array buffer response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(formData); + return validateStreamFromPartialString(response.body.getReader(), + "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); +}, "Read form data response's body as readableStream"); + +test(function() { + assert_equals(Response.error().body, null); +}, "Getting an error Response stream"); + +test(function() { + assert_equals(Response.redirect("/").body, null); +}, "Getting a redirect Response stream"); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume.html b/test/fixtures/wpt/fetch/api/response/response-consume.html new file mode 100644 index 00000000000000..89fc49fd3c2b11 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-consume.html @@ -0,0 +1,317 @@ + + + + + Response consume + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js new file mode 100644 index 00000000000000..118eb7d5cb398c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker +// META: title=Response Receives Propagated Error from ReadableStream + +function newStreamWithStartError() { + var err = new Error("Start error"); + return [new ReadableStream({ + start(controller) { + controller.error(err); + } + }), + err] +} + +function newStreamWithPullError() { + var err = new Error("Pull error"); + return [new ReadableStream({ + pull(controller) { + controller.error(err); + } + }), + err] +} + +function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) { + promise_test(test => { + return promise_rejects_exactly( + test, + err, + new Response(stream)[responseReaderMethod](), + 'CustomTestError should propagate' + ) + }, testDescription) +} + + +promise_test(test => { + var [stream, err] = newStreamWithStartError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error") + +promise_test(test => { + var [stream, err] = newStreamWithPullError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error") + + +// test start() errors for all Body reader methods +runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise'); + +// test pull() errors for all Body reader methods +runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise'); diff --git a/test/fixtures/wpt/fetch/api/response/response-error.any.js b/test/fixtures/wpt/fetch/api/response/response-error.any.js new file mode 100644 index 00000000000000..a76bc4380286fa --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-error.any.js @@ -0,0 +1,27 @@ +// META: global=window,worker +// META: title=Response error + +var invalidStatus = [0, 100, 199, 600, 1000]; +invalidStatus.forEach(function(status) { + test(function() { + assert_throws_js(RangeError, function() { new Response("", { "status" : status }); }, + "Expect RangeError exception when status is " + status); + },"Throws RangeError when responseInit's status is " + status); +}); + +var invalidStatusText = ["\n", "Ā"]; +invalidStatusText.forEach(function(statusText) { + test(function() { + assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); }, + "Expect TypeError exception " + statusText); + },"Throws TypeError when responseInit's statusText is " + statusText); +}); + +var nullBodyStatus = [204, 205, 304]; +nullBodyStatus.forEach(function(status) { + test(function() { + assert_throws_js(TypeError, + function() { new Response("body", {"status" : status }); }, + "Expect TypeError exception "); + },"Throws TypeError when building a response with body and a body status of " + status); +}); diff --git a/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js new file mode 100644 index 00000000000000..ea5192bfb10dcf --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js @@ -0,0 +1,23 @@ +// META: global=window,worker + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + stream.getReader(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which getReader() is called"); + +test(() => { + const stream = new ReadableStream(); + stream.getReader().read(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() is called"); + +promise_test(async () => { + const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }), + reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() and releaseLock() are called"); diff --git a/test/fixtures/wpt/fetch/api/response/response-init-001.any.js b/test/fixtures/wpt/fetch/api/response/response-init-001.any.js new file mode 100644 index 00000000000000..559e49ad11ffe1 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-init-001.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +// META: title=Response init: simple cases + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "", + "body" : null +}; + +var statusCodes = { "givenValues" : [200, 300, 400, 500, 599], + "expectedValues" : [200, 300, 400, 500, 599] +}; +var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)], + "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)] +}; +var initValuesDict = { "status" : statusCodes, + "statusText" : statusTexts +}; + +function isOkStatus(status) { + return 200 <= status && 299 >= status; +} + +var response = new Response(); +for (var attributeName in defaultValues) { + test(function() { + var expectedValue = defaultValues[attributeName]; + assert_equals(response[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + }, "Check default value for " + attributeName + " attribute"); +} + +for (var attributeName in initValuesDict) { + test(function() { + var valuesToTest = initValuesDict[attributeName]; + for (var valueIdx in valuesToTest["givenValues"]) { + var givenValue = valuesToTest["givenValues"][valueIdx]; + var expectedValue = valuesToTest["expectedValues"][valueIdx]; + var responseInit = {}; + responseInit[attributeName] = givenValue; + var response = new Response("", responseInit); + assert_equals(response[attributeName], expectedValue, + "Expect response." + attributeName + " is " + expectedValue + + " when initialized with " + givenValue); + assert_equals(response.ok, isOkStatus(response.status), + "Expect response.ok is " + isOkStatus(response.status)); + } + }, "Check " + attributeName + " init values and associated getter"); +} + +test(function() { + const response1 = new Response(""); + assert_equals(response1.headers, response1.headers); + + const response2 = new Response("", {"headers": {"X-Foo": "bar"}}); + assert_equals(response2.headers, response2.headers); + const headers = response2.headers; + response2.headers.set("X-Foo", "quux"); + assert_equals(headers, response2.headers); + headers.set("X-Other-Header", "baz"); + assert_equals(headers, response2.headers); +}, "Test that Response.headers has the [SameObject] extended attribute"); diff --git a/test/fixtures/wpt/fetch/api/response/response-init-002.any.js b/test/fixtures/wpt/fetch/api/response/response-init-002.any.js new file mode 100644 index 00000000000000..6c0a46e480406c --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-init-002.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Response init: body and headers +// META: script=../resources/utils.js + +test(function() { + var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3" + }; + var headers = new Headers(headerDict); + var response = new Response("", { "headers" : headers }) + for (var name in headerDict) { + assert_equals(response.headers.get(name), headerDict[name], + "response's headers has " + name + " : " + headerDict[name]); + } +}, "Initialize Response with headers values"); + +function checkResponseInit(body, bodyType, expectedTextBody) { + promise_test(function(test) { + var response = new Response(body); + var resHeaders = response.headers; + var mime = resHeaders.get("Content-Type"); + assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" "); + return response.text().then(function(bodyAsText) { + //not equals: cannot guess formData exact value + assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body"); + }); + }, "Initialize Response's body with " + bodyType); +} + +var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); +var formaData = new FormData(); +formaData.append("name", "value"); +var urlSearchParams = "URLSearchParams are not supported"; +//avoid test timeout if not implemented +if (self.URLSearchParams) + urlSearchParams = new URLSearchParams("name=value"); +var usvString = "This is a USVString" + +checkResponseInit(blob, "application/octet-binary", "This is a blob"); +checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); +checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); +checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); + +promise_test(function(test) { + var body = "This is response body"; + var response = new Response(body); + return validateStreamFromString(response.body.getReader(), body); +}, "Read Response's body as readableStream"); + +promise_test(function(test) { + var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]}); + return response.blob().then(function(blob) { + assert_equals(blob.type, "", "Blob type should be the empty string"); + }); +}, "Testing empty Response Content-Type header"); + +test(function() { + var response = new Response(null, {status: 204}); + assert_equals(response.body, null); +}, "Testing null Response body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-static-error.any.js b/test/fixtures/wpt/fetch/api/response/response-static-error.any.js new file mode 100644 index 00000000000000..4097eab37b4c90 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-static-error.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: title=Response: error static method + +test(function() { + var responseError = Response.error(); + assert_equals(responseError.type, "error", "Network error response's type is error"); + assert_equals(responseError.status, 0, "Network error response's status is 0"); + assert_equals(responseError.statusText, "", "Network error response's statusText is empty"); + assert_equals(responseError.body, null, "Network error response's body is null"); + + assert_true(responseError.headers.entries().next().done, "Headers should be empty"); +}, "Check response returned by static method error()"); + +test(function() { + const headers = Response.error().headers; + + // Avoid false positives if expected API is not available + assert_true(!!headers); + assert_equals(typeof headers.append, 'function'); + + assert_throws_js(TypeError, function () { headers.append('name', 'value'); }); +}, "the 'guard' of the Headers instance should be immutable"); diff --git a/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js b/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js new file mode 100644 index 00000000000000..971baec4909de6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js @@ -0,0 +1,40 @@ +// META: global=window,worker +// META: title=Response: redirect static method + +var url = "http://test.url:1234/"; +test(function() { + redirectResponse = Response.redirect(url); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, 302, "Default redirect status is 302"); + assert_equals(redirectResponse.headers.get("Location"), url, + "redirected response has Location header with the correct url"); + assert_equals(redirectResponse.statusText, ""); +}, "Check default redirect response"); + +[301, 302, 303, 307, 308].forEach(function(status) { + test(function() { + redirectResponse = Response.redirect(url, status); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, status, "Redirect status is " + status); + assert_equals(redirectResponse.headers.get("Location"), url); + assert_equals(redirectResponse.statusText, ""); + }, "Check response returned by static method redirect(), status = " + status); +}); + +test(function() { + var invalidUrl = "http://:This is not an url"; + assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); }, + "Expect TypeError exception"); +}, "Check error returned when giving invalid url to redirect()"); + +var invalidRedirectStatus = [200, 309, 400, 500]; +invalidRedirectStatus.forEach(function(invalidStatus) { + test(function() { + assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); }, + "Expect RangeError exception"); + }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus); +}); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js new file mode 100644 index 00000000000000..d80049a2a455e1 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js @@ -0,0 +1,42 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream + +function createResponseWithReadableStream(callback) { + return fetch("../resources/data.json").then(function(response) { + var reader = response.body.getReader(); + reader.releaseLock(); + return callback(response); + }); +} + +promise_test(function() { + return createResponseWithReadableStream(function(response) { + return response.blob().then(function(blob) { + assert_true(blob instanceof Blob); + }); + }); +}, "Getting blob after getting the Response body - not disturbed, not locked"); + +promise_test(function() { + return createResponseWithReadableStream(function(response) { + return response.text().then(function(text) { + assert_true(text.length > 0); + }); + }); +}, "Getting text after getting the Response body - not disturbed, not locked"); + +promise_test(function() { + return createResponseWithReadableStream(function(response) { + return response.json().then(function(json) { + assert_equals(typeof json, "object"); + }); + }); +}, "Getting json after getting the Response body - not disturbed, not locked"); + +promise_test(function() { + return createResponseWithReadableStream(function(response) { + return response.arrayBuffer().then(function(arrayBuffer) { + assert_true(arrayBuffer.byteLength > 0); + }); + }); +}, "Getting arrayBuffer after getting the Response body - not disturbed, not locked"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js new file mode 100644 index 00000000000000..ccff547556e724 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js @@ -0,0 +1,33 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream + +function createResponseWithLockedReadableStream(callback) { + return fetch("../resources/data.json").then(function(response) { + var reader = response.body.getReader(); + return callback(response); + }); +} + +promise_test(function(test) { + return createResponseWithLockedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); +}, "Getting blob after getting a locked Response body"); + +promise_test(function(test) { + return createResponseWithLockedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); +}, "Getting text after getting a locked Response body"); + +promise_test(function(test) { + return createResponseWithLockedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); +}, "Getting json after getting a locked Response body"); + +promise_test(function(test) { + return createResponseWithLockedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); +}, "Getting arrayBuffer after getting a locked Response body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js new file mode 100644 index 00000000000000..32c11625eb4542 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js @@ -0,0 +1,34 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream + +function createResponseWithDisturbedReadableStream(callback) { + return fetch("../resources/data.json").then(function(response) { + var reader = response.body.getReader(); + reader.read(); + return callback(response); + }); +} + +promise_test(function(test) { + return createResponseWithDisturbedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); +}, "Getting blob after reading the Response body"); + +promise_test(function(test) { + return createResponseWithDisturbedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); +}, "Getting text after reading the Response body"); + +promise_test(function(test) { + return createResponseWithDisturbedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); +}, "Getting json after reading the Response body"); + +promise_test(function(test) { + return createResponseWithDisturbedReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); +}, "Getting arrayBuffer after reading the Response body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js new file mode 100644 index 00000000000000..58331ae1d0c8a7 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js @@ -0,0 +1,33 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream + +function createResponseWithCancelledReadableStream(callback) { + return fetch("../resources/data.json").then(function(response) { + response.body.cancel(); + return callback(response); + }); +} + +promise_test(function(test) { + return createResponseWithCancelledReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); +}, "Getting blob after cancelling the Response body"); + +promise_test(function(test) { + return createResponseWithCancelledReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); +}, "Getting text after cancelling the Response body"); + +promise_test(function(test) { + return createResponseWithCancelledReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); +}, "Getting json after cancelling the Response body"); + +promise_test(function(test) { + return createResponseWithCancelledReadableStream(function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); +}, "Getting arrayBuffer after cancelling the Response body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js new file mode 100644 index 00000000000000..9be0c93f35a07b --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js @@ -0,0 +1,34 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + response.blob(); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function() { response.body.getReader(); }); + }); +}, "Getting a body reader after consuming as blob"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + response.text(); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function() { response.body.getReader(); }); + }); +}, "Getting a body reader after consuming as text"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + response.json(); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function() { response.body.getReader(); }); + }); +}, "Getting a body reader after consuming as json"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + response.arrayBuffer(); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function() { response.body.getReader(); }); + }); +}, "Getting a body reader after consuming as arrayBuffer"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js new file mode 100644 index 00000000000000..61d8544f0786c8 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js @@ -0,0 +1,76 @@ +// META: global=window,worker +// META: title=ReadableStream disturbed tests, via Response's bodyUsed property + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A non-closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel(); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "A non-closed stream on which cancel() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.close(); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "An errored stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "An errored stream on which cancel() has been called"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js new file mode 100644 index 00000000000000..5341b75271ead5 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +test(() => { + const r = new Response(new ReadableStream()); + // highWaterMark: 0 means that nothing will actually be read from the body. + r.body.pipeTo(new WritableStream({}, {highWaterMark: 0})); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeTo on Response body should disturb it synchronously'); + +test(() => { + const r = new Response(new ReadableStream()); + r.body.pipeThrough({ + writable: new WritableStream({}, {highWaterMark: 0}), + readable: new ReadableStream() + }); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeThrough on Response body should disturb it synchronously'); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js new file mode 100644 index 00000000000000..8fef66c8a281c6 --- /dev/null +++ b/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js @@ -0,0 +1,117 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(async () => { + // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so + // these tests use add_completion_callback for cleanup instead. + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: undefined}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject value: undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(undefined); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(8.2); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject 8.2 via Object.prototype.then.'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const resp = new Response(hello); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' + + 'should not be possible'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const u8a123 = new Uint8Array([1, 2, 3]); + const u8a456 = new Uint8Array([4, 5, 6]); + const resp = new Response(u8a123); + const writtenBytes = []; + const ws = new WritableStream({ + write(chunk) { + writtenBytes.push(...Array.from(chunk)); + } + }); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: u8a456}); + }; + await resp.body.pipeTo(ws); + delete Object.prototype.then; + assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]'); +}, 'intercepting arraybuffer to body readable stream conversion via ' + + 'Object.prototype.then should not be possible'); diff --git a/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html b/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html new file mode 100644 index 00000000000000..60a784cd84ed92 --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html @@ -0,0 +1,264 @@ + + + + + Connection partitioning by site + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html new file mode 100644 index 00000000000000..7a8b61323752d2 --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html @@ -0,0 +1,35 @@ + + + + + about:blank Network Partition Checker + + + + + + + diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html new file mode 100644 index 00000000000000..b058f611242bb8 --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html @@ -0,0 +1,30 @@ + + + + + Network Partition Checker + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html new file mode 100644 index 00000000000000..f76ed1844719c9 --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html @@ -0,0 +1,22 @@ + + + + + Iframe Network Partition Checker + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js new file mode 100644 index 00000000000000..bd66109380f21e --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js @@ -0,0 +1,47 @@ +// Runs multiple fetches that validate connections see only a single partition_id. +// Requests are run in parallel so that they use multiple connections to maximize the +// chance of exercising all matching connections in the connection pool. Only returns +// once all requests have completed to make cleaning up server state non-racy. +function check_partition_ids(location) { + const NUM_FETCHES = 20; + + var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition'; + + // Not a perfect parse of the query string, but good enough for this test. + var include_credentials = base_url.search('include_credentials=true') != -1; + var exclude_credentials = base_url.search('include_credentials=false') != -1; + if (include_credentials != !exclude_credentials) + throw new Exception('Credentials mode not specified'); + + + // Run NUM_FETCHES in parallel. + var fetches = []; + for (i = 0; i < NUM_FETCHES; ++i) { + var fetch_params = { + credentials: 'omit', + mode: 'cors', + headers: { + 'Header-To-Force-CORS': 'cors' + }, + }; + + // Use a unique URL for each request, in case the caching layer serializes multiple + // requests for the same URL. + var url = `${base_url}&${token()}`; + + fetches.push(fetch(url, fetch_params).then( + function (response) { + return response.text().then(function(text) { + assert_equals(text, 'ok', `Socket unexpectedly reused`); + }); + })); + } + + // Wait for all promises to complete. + return Promise.allSettled(fetches).then(function (results) { + results.forEach(function (result) { + if (result.status != 'fulfilled') + throw result.reason; + }); + }); +} diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html new file mode 100644 index 00000000000000..e6b7ea7673fe79 --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html @@ -0,0 +1,24 @@ + + + + + Worker Network Partition Checker + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js new file mode 100644 index 00000000000000..1745edfacb130f --- /dev/null +++ b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js @@ -0,0 +1,15 @@ +// This tests the partition key of fetches to subresouce_origin made by the worker and +// imported scripts from subresource_origin. +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js'); + +async function fetch_and_reply() { + try { + await check_partition_ids(); + self.postMessage({result: 'success'}); + } catch (e) { + self.postMessage({result: 'error', details: e.message}); + } +} +fetch_and_reply(); diff --git a/test/fixtures/wpt/fetch/content-encoding/bad-gzip-body.any.js b/test/fixtures/wpt/fetch/content-encoding/bad-gzip-body.any.js new file mode 100644 index 00000000000000..17bc1261a3f5c3 --- /dev/null +++ b/test/fixtures/wpt/fetch/content-encoding/bad-gzip-body.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker + +promise_test((test) => { + return fetch("resources/bad-gzip-body.py").then(res => { + assert_equals(res.status, 200); + }); +}, "Fetching a resource with bad gzip content should still resolve"); + +[ + "arrayBuffer", + "blob", + "formData", + "json", + "text" +].forEach(method => { + promise_test(t => { + return fetch("resources/bad-gzip-body.py").then(res => { + assert_equals(res.status, 200); + return promise_rejects_js(t, TypeError, res[method]()); + }); + }, "Consuming the body of a resource with bad gzip content with " + method + "() should reject"); +}); diff --git a/test/fixtures/wpt/fetch/content-length/content-length.html b/test/fixtures/wpt/fetch/content-length/content-length.html new file mode 100644 index 00000000000000..cda9b5b5237015 --- /dev/null +++ b/test/fixtures/wpt/fetch/content-length/content-length.html @@ -0,0 +1,14 @@ + + +Content-Length Test + + + +PASS +but FAIL if this is in the body. \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/content-length/content-length.html.headers b/test/fixtures/wpt/fetch/content-length/content-length.html.headers new file mode 100644 index 00000000000000..25389b7c0fab38 --- /dev/null +++ b/test/fixtures/wpt/fetch/content-length/content-length.html.headers @@ -0,0 +1 @@ +Content-Length: 403 diff --git a/test/fixtures/wpt/fetch/content-type/README.md b/test/fixtures/wpt/fetch/content-type/README.md new file mode 100644 index 00000000000000..f553b7ee8e6c9b --- /dev/null +++ b/test/fixtures/wpt/fetch/content-type/README.md @@ -0,0 +1,20 @@ +# `resources/content-types.json` + +An array of tests. Each test has these fields: + +* `contentType`: an array of values for the `Content-Type` header. A harness needs to run the test twice if there are multiple values. One time with the values concatenated with `,` followed by a space and one time with multiple `Content-Type` declarations, each on their own line with one of the values, in order. +* `encoding`: the expected encoding, null for the default. +* `mimeType`: the result of extracting a MIME type and serializing it. +* `documentContentType`: the MIME type expected to be exposed in DOM documents. + +(These tests are currently somewhat geared towards browser use, but could be generalized easily enough if someone wanted to contribute tests for MIME types that would cause downloads in the browser or some such.) + +# `resources/script-content-types.json` + +An array of tests, surprise. Each test has these fields: + +* `contentType`: see above. +* `executes`: whether the script is expected to execute. +* `encoding`: how the script is expected to be decoded. + +These tests are expected to be loaded through ` + +
+ diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html new file mode 100644 index 00000000000000..a771ed6a653eaa --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 00000000000000..82adc47b0cf31c --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html new file mode 100644 index 00000000000000..ebb337dba80560 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html new file mode 100644 index 00000000000000..1ae4cfcaa7ca24 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 00000000000000..cea80f2f89fac4 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,24 @@ + + + + + +
+ + + + + diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css new file mode 100644 index 00000000000000..afd2b92975dc8a --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers new file mode 100644 index 00000000000000..0f228f94ecb1bc --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css new file mode 100644 index 00000000000000..afd2b92975dc8a --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css b/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css new file mode 100644 index 00000000000000..7db6f5c6d36042 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css @@ -0,0 +1,3 @@ +)]}' +{} +#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png b/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers b/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers new file mode 100644 index 00000000000000..e7be84a714bee6 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html new file mode 100644 index 00000000000000..7bad71bfbd850e --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html @@ -0,0 +1,10 @@ + + + + + Page Title + + +

Page body

+ + diff --git a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js new file mode 100644 index 00000000000000..db45bb4acc9251 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js @@ -0,0 +1,9 @@ + diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js new file mode 100644 index 00000000000000..faae1b7682b547 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js @@ -0,0 +1,10 @@ + diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js new file mode 100644 index 00000000000000..a880a5bc724a85 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers new file mode 100644 index 00000000000000..0f228f94ecb1bc --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js new file mode 100644 index 00000000000000..a880a5bc724a85 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png b/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf2a999badd34d86f5bd3817579599feb6caa57 GIT binary patch literal 1092 zcmaFAe{X=FJ1>_M7Xt$WucwDg5Rgs)VGd>>`F{UiHy{Q{G8PB9JAnj%NCt?3LM-Wy zzK&o;Kt4ARC$a)X6_P!Id>I(3)L=4T8e$k&U#S6DB!Pj!3d|QT08&5{Xv4t3%n;xc z;tJCN)HDi4Ltq4k05CfkXGH-$E>YqdQ4*Y=R#Ki=l*$m0n3-3i=jR%tP-d)WYzE0U zKvET`(!tZkF(kto$h*)83bDfrfXv`{Ai)8|0f|5Yh!~mKcq9xGNa6q$+Z(7PFfd9R S0-5O`!#!R7T!3sRkU;?4v4%hZ literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers new file mode 100644 index 00000000000000..e7be84a714bee6 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf2a999badd34d86f5bd3817579599feb6caa57 GIT binary patch literal 1092 zcmaFAe{X=FJ1>_M7Xt$WucwDg5Rgs)VGd>>`F{UiHy{Q{G8PB9JAnj%NCt?3LM-Wy zzK&o;Kt4ARC$a)X6_P!Id>I(3)L=4T8e$k&U#S6DB!Pj!3d|QT08&5{Xv4t3%n;xc z;tJCN)HDi4Ltq4k05CfkXGH-$E>YqdQ4*Y=R#Ki=l*$m0n3-3i=jR%tP-d)WYzE0U zKvET`(!tZkF(kto$h*)83bDfrfXv`{Ai)8|0f|5Yh!~mKcq9xGNa6q$+Z(7PFfd9R S0-5O`!#!R7T!3sRkU;?4v4%hZ literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers new file mode 100644 index 00000000000000..0f228f94ecb1bc --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf2a999badd34d86f5bd3817579599feb6caa57 GIT binary patch literal 1092 zcmaFAe{X=FJ1>_M7Xt$WucwDg5Rgs)VGd>>`F{UiHy{Q{G8PB9JAnj%NCt?3LM-Wy zzK&o;Kt4ARC$a)X6_P!Id>I(3)L=4T8e$k&U#S6DB!Pj!3d|QT08&5{Xv4t3%n;xc z;tJCN)HDi4Ltq4k05CfkXGH-$E>YqdQ4*Y=R#Ki=l*$m0n3-3i=jR%tP-d)WYzE0U zKvET`(!tZkF(kto$h*)83bDfrfXv`{Ai)8|0f|5Yh!~mKcq9xGNa6q$+Z(7PFfd9R S0-5O`!#!R7T!3sRkU;?4v4%hZ literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers new file mode 100644 index 00000000000000..156209f9c81ff7 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html b/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html new file mode 100644 index 00000000000000..67b3ad5a600bda --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html @@ -0,0 +1,16 @@ + + + diff --git a/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html new file mode 100644 index 00000000000000..8f4d7679e3d749 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html @@ -0,0 +1,30 @@ + + + + + +
+ diff --git a/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html b/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html new file mode 100644 index 00000000000000..9a272d63ffc30e --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html @@ -0,0 +1,32 @@ + + + + + +
+ diff --git a/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html b/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html new file mode 100644 index 00000000000000..c8a90c79b3f7f5 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html @@ -0,0 +1,38 @@ + + + + + +
+ diff --git a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 00000000000000..b6bc90964deb14 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,33 @@ + + + + + +
+ + + + + + + diff --git a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html new file mode 100644 index 00000000000000..44cb1f8659d81d --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html @@ -0,0 +1,25 @@ + + + + + +
+ + + + + + + diff --git a/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html new file mode 100644 index 00000000000000..1a8095ec658197 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html @@ -0,0 +1,83 @@ + + + + + +
+ diff --git a/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html new file mode 100644 index 00000000000000..6d490d55bce25f --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html @@ -0,0 +1,84 @@ + + + + + + +
+ diff --git a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 00000000000000..8fef0dc59e437b --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,42 @@ + + + +CSS is not applied (because of nosniff + non-text/css headers) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html new file mode 100644 index 00000000000000..4f0b4c22f56681 --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html @@ -0,0 +1,36 @@ + + + +CSS is not applied (because of strict content-type enforcement for cross-origin stylesheets) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html b/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html new file mode 100644 index 00000000000000..29ed586a4f0f2f --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html @@ -0,0 +1,38 @@ + + + +CORB doesn't block a stylesheet that has a proper Content-Type and begins with a JSON parser breaker + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html b/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html new file mode 100644 index 00000000000000..cdefcd2d2c9a5f --- /dev/null +++ b/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html @@ -0,0 +1,41 @@ + + + +CSS is not applied (because of mismatched Content-Type header) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/idlharness.tentative.any.js b/test/fixtures/wpt/fetch/cors-rfc1918/idlharness.tentative.any.js new file mode 100644 index 00000000000000..1dad943072589a --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/idlharness.tentative.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +// https://wicg.github.io/cors-rfc1918/ + +idl_test( + ['cors-rfc1918'], + ['html', 'dom'], + idlArray => { + if (self.GLOBAL.isWorker()) { + idlArray.add_objects({ + WorkerGlobalScope: ['self'], + }); + } else { + idlArray.add_objects({ + Document: ['document'], + }); + } + } +); diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/non-secure-context.window.js b/test/fixtures/wpt/fetch/cors-rfc1918/non-secure-context.window.js new file mode 100644 index 00000000000000..8f49a5cbedda7e --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/non-secure-context.window.js @@ -0,0 +1,31 @@ +// META: script=resources/support.js +// +// Spec: https://wicg.github.io/cors-rfc1918/#integration-fetch +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: secure-context.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(async t => { + return fetch("/common/blank.html") + .catch(reason => {unreached_func(reason)}); +}, "Local non secure page fetches local page."); + +// For the following tests, we go through an iframe, because it is not possible +// to directly import the test harness from a secured public page. +promise_test(async t => { + let iframe = await appendIframe(t, document, + "resources/treat-as-public-address.html"); + let reply = futureMessage(); + iframe.contentWindow.postMessage("/common/blank.html", "*"); + assert_equals(await reply, "failure"); +}, "Public non secure page fetches local page."); + +// TODO(https://github.com/web-platform-tests/wpt/issues/26166): +// Add tests for public variations when we are able to fetch resources using a +// mechanism compatible with WPT guidelines regarding being self-contained. + diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/resources/support.js b/test/fixtures/wpt/fetch/cors-rfc1918/resources/support.js new file mode 100644 index 00000000000000..be49c515ef5422 --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/resources/support.js @@ -0,0 +1,34 @@ +// Creates a new iframe in |doc|, calls |func| on it and appends it as a child +// of |doc|. +// Returns a promise that resolves to the iframe once loaded (successfully or +// not). +// The iframe is removed from |doc| once test |t| is done running. +// +// NOTE: Because iframe elements always invoke the onload event handler, even +// in case of error, we cannot wire onerror to a promise rejection. The Promise +// constructor requires users to resolve XOR reject the promise. +function appendIframeWith(t, doc, func) { + return new Promise(resolve => { + const child = doc.createElement("iframe"); + func(child); + child.onload = () => { resolve(child); }; + doc.body.appendChild(child); + t.add_cleanup(() => { doc.body.removeChild(child); }); + }); +} + +// Appends a child iframe to |doc| sourced from |src|. +// +// See append_child_frame_with() for more details. +function appendIframe(t, doc, src) { + return appendIframeWith(t, doc, child => { child.src = src; }); +} + +// Register an event listener that will resolve this promise when this +// window receives a message posted to it. +function futureMessage() { + return new Promise(resolve => { + window.addEventListener("message", e => resolve(e.data)); + }); +}; + diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html new file mode 100644 index 00000000000000..7a8f6f09a517f0 --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html @@ -0,0 +1,8 @@ + + diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html.headers b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html.headers new file mode 100644 index 00000000000000..76371c6209e46f --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: treat-as-public-address; diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html new file mode 100644 index 00000000000000..7a8f6f09a517f0 --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html @@ -0,0 +1,8 @@ + + diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html.headers b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html.headers new file mode 100644 index 00000000000000..76371c6209e46f --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/resources/treat-as-public-address.https.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: treat-as-public-address; diff --git a/test/fixtures/wpt/fetch/cors-rfc1918/secure-context.https.window.js b/test/fixtures/wpt/fetch/cors-rfc1918/secure-context.https.window.js new file mode 100644 index 00000000000000..8ed028a390c19c --- /dev/null +++ b/test/fixtures/wpt/fetch/cors-rfc1918/secure-context.https.window.js @@ -0,0 +1,31 @@ +// META: script=resources/support.js +// +// Spec: https://wicg.github.io/cors-rfc1918/#integration-fetch +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: non-secure-context.window.js + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +promise_test(async t => { + return fetch("/common/blank.html") + .catch(reason => {unreached_func(reason)}); +}, "Local secure page fetches local page."); + +// For the following tests, we go through an iframe, because it is not possible +// to directly import the test harness from a secured public page. +promise_test(async t => { + let iframe = await appendIframe(t, document, + "resources/treat-as-public-address.https.html"); + let reply = futureMessage(); + iframe.contentWindow.postMessage("/common/blank.html", "*"); + assert_equals(await reply, "success"); +}, "Public secure page fetches local page."); + +// TODO(https://github.com/web-platform-tests/wpt/issues/26166): +// Add tests for public variations when we are able to fetch resources using a +// mechanism compatible with WPT guidelines regarding being self-contained. + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html new file mode 100644 index 00000000000000..cc6a3a81bcf4cb --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js new file mode 100644 index 00000000000000..64a7bfeb864e4e --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js @@ -0,0 +1,76 @@ +// META: timeout=long +// META: global=window,dedicatedworker,sharedworker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTP_ORIGIN + path; +const sameSiteBaseURL = "http://" + host.ORIGINAL_HOST + ":" + host.HTTP_PORT2 + path; +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + path; +const httpsBaseURL = host.HTTPS_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-scheme (HTTP to HTTPS) no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const remoteSameSiteURL = sameSiteBaseURL + "resources/hello.py?corp=same-site"; + + await fetch(remoteSameSiteURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(sameSiteBaseURL + "resources/hello.py?corp=same-origin", { mode: "no-cors" })); +}, "Valid cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js new file mode 100644 index 00000000000000..c9b5b7502f48ac --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js @@ -0,0 +1,56 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTPS_ORIGIN + path; +const notSameSiteBaseURL = host.HTTPS_NOTSAMESITE_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html new file mode 100644 index 00000000000000..63902c302b7ce6 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html new file mode 100644 index 00000000000000..060b7551ea5168 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html @@ -0,0 +1,54 @@ + + + + + + + + +
+ + + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png new file mode 100644 index 0000000000000000000000000000000000000000..a644887c4385975737b0de212b9310b7282619e7 GIT binary patch literal 114 zcmaFAe{X=FJ1>_M7Xt$WucwDg5Rgs*Vk1UoAo+g(UN{1BnFD-6TtR{iCUFpPpdi1e ki(`m}XmWxCD^M&UgNcC&q=Hczu7bhS)z1aUb4mab0LVuxivR!s literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html new file mode 100644 index 00000000000000..257185805d96d2 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html @@ -0,0 +1,19 @@ + + + + + + +

The iframe making a same origin fetch call.

+ + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js new file mode 100644 index 00000000000000..8f6338176a37f2 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js @@ -0,0 +1,7 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + return promise_rejects_js(t, + TypeError, + fetch(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=same-site", { mode: "no-cors" })); +}, "Cross-Origin-Resource-Policy: same-site blocks retrieving HTTPS from HTTP"); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js new file mode 100644 index 00000000000000..4c7457187419e0 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js @@ -0,0 +1,13 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + const img = new Image(); + img.src = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/image.py?corp=same-site"; + return new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + document.body.appendChild(img); + }).finally(() => { + img.remove(); + }); +}, "Cross-Origin-Resource-Policy does not block Mixed Content "); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html new file mode 100644 index 00000000000000..a9690fc70be138 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html @@ -0,0 +1,52 @@ + + + + + + + + +
+ + + diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js new file mode 100644 index 00000000000000..dc874977a63e51 --- /dev/null +++ b/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js @@ -0,0 +1,19 @@ +// META: script=/common/get-host-info.sub.js + +const crossOriginURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp="; + +[ + "same", + "same, same-origin", + "SAME-ORIGIN", + "Same-Origin", + "same-origin, <>", + "same-origin, same-origin", + "https://www.example.com", // See https://github.com/whatwg/fetch/issues/760 +].forEach(incorrectHeaderValue => { + // Note: an incorrect value results in a successful load, so this test is only meaningful in + // implementations with support for the header. + promise_test(t => { + return fetch(crossOriginURL + encodeURIComponent(incorrectHeaderValue), { mode: "no-cors" }); + }, "Parsing Cross-Origin-Resource-Policy: " + incorrectHeaderValue); +}); diff --git a/test/fixtures/wpt/fetch/data-urls/README.md b/test/fixtures/wpt/fetch/data-urls/README.md new file mode 100644 index 00000000000000..1ce5b18b538250 --- /dev/null +++ b/test/fixtures/wpt/fetch/data-urls/README.md @@ -0,0 +1,11 @@ +## data: URLs + +`resources/data-urls.json` contains `data:` URL tests. The tests are encoded as a JSON array. Each value in the array is an array of two or three values. The first value describes the input, the second value describes the expected MIME type, null if the input is expected to fail somehow, or the empty string if the expected value is `text/plain;charset=US-ASCII`. The third value, if present, describes the expected body as an array of integers representing bytes. + +These tests are used for `data:` URLs in this directory (see `processing.any.js`). + +## Forgiving-base64 decode + +`resources/base64.json` contains [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) tests. The tests are encoded as a JSON array. Each value in the array is an array of two values. The first value describes the input, the second value describes the output as an array of integers representing bytes or null if the input cannot be decoded. + +These tests are used for `data:` URLs in this directory (see `base64.any.js`) and `window.atob()` in `../../html/webappapis/atob/base64.html`. diff --git a/test/fixtures/wpt/fetch/data-urls/base64.any.js b/test/fixtures/wpt/fetch/data-urls/base64.any.js new file mode 100644 index 00000000000000..83f34db1777d99 --- /dev/null +++ b/test/fixtures/wpt/fetch/data-urls/base64.any.js @@ -0,0 +1,18 @@ +// META: global=window,worker + +promise_test(() => fetch("resources/base64.json").then(res => res.json()).then(runBase64Tests), "Setup."); +function runBase64Tests(tests) { + for(let i = 0; i < tests.length; i++) { + const input = tests[i][0], + output = tests[i][1], + dataURL = "data:;base64," + input; + promise_test(t => { + if(output === null) { + return promise_rejects_js(t, TypeError, fetch(dataURL)); + } + return fetch(dataURL).then(res => res.arrayBuffer()).then(body => { + assert_array_equals(new Uint8Array(body), output); + }); + }, "data: URL base64 handling: " + format_value(input)); + } +} diff --git a/test/fixtures/wpt/fetch/data-urls/processing.any.js b/test/fixtures/wpt/fetch/data-urls/processing.any.js new file mode 100644 index 00000000000000..cec97bd6be2b2e --- /dev/null +++ b/test/fixtures/wpt/fetch/data-urls/processing.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker + +promise_test(() => fetch("resources/data-urls.json").then(res => res.json()).then(runDataURLTests), "Setup."); +function runDataURLTests(tests) { + for(let i = 0; i < tests.length; i++) { + const input = tests[i][0], + expectedMimeType = tests[i][1], + expectedBody = expectedMimeType !== null ? tests[i][2] : null; + promise_test(t => { + if(expectedMimeType === null) { + return promise_rejects_js(t, TypeError, fetch(input)); + } else { + return fetch(input).then(res => { + return res.arrayBuffer().then(body => { + assert_array_equals(new Uint8Array(body), expectedBody); + assert_equals(res.headers.get("content-type"), expectedMimeType); // We could assert this earlier, but this fails often + }); + }); + } + }, format_value(input)); + } +} diff --git a/test/fixtures/wpt/fetch/data-urls/resources/base64.json b/test/fixtures/wpt/fetch/data-urls/resources/base64.json new file mode 100644 index 00000000000000..01f981a6502aec --- /dev/null +++ b/test/fixtures/wpt/fetch/data-urls/resources/base64.json @@ -0,0 +1,82 @@ +[ + ["", []], + ["abcd", [105, 183, 29]], + [" abcd", [105, 183, 29]], + ["abcd ", [105, 183, 29]], + [" abcd===", null], + ["abcd=== ", null], + ["abcd ===", null], + ["a", null], + ["ab", [105]], + ["abc", [105, 183]], + ["abcde", null], + ["𐀀", null], + ["=", null], + ["==", null], + ["===", null], + ["====", null], + ["=====", null], + ["a=", null], + ["a==", null], + ["a===", null], + ["a====", null], + ["a=====", null], + ["ab=", null], + ["ab==", [105]], + ["ab===", null], + ["ab====", null], + ["ab=====", null], + ["abc=", [105, 183]], + ["abc==", null], + ["abc===", null], + ["abc====", null], + ["abc=====", null], + ["abcd=", null], + ["abcd==", null], + ["abcd===", null], + ["abcd====", null], + ["abcd=====", null], + ["abcde=", null], + ["abcde==", null], + ["abcde===", null], + ["abcde====", null], + ["abcde=====", null], + ["=a", null], + ["=a=", null], + ["a=b", null], + ["a=b=", null], + ["ab=c", null], + ["ab=c=", null], + ["abc=d", null], + ["abc=d=", null], + ["ab\u000Bcd", null], + ["ab\u3000cd", null], + ["ab\u3001cd", null], + ["ab\tcd", [105, 183, 29]], + ["ab\ncd", [105, 183, 29]], + ["ab\fcd", [105, 183, 29]], + ["ab\rcd", [105, 183, 29]], + ["ab cd", [105, 183, 29]], + ["ab\u00a0cd", null], + ["ab\t\n\f\r cd", [105, 183, 29]], + [" \t\n\f\r ab\t\n\f\r cd\t\n\f\r ", [105, 183, 29]], + ["ab\t\n\f\r =\t\n\f\r =\t\n\f\r ", [105]], + ["A", null], + ["/A", [252]], + ["//A", [255, 240]], + ["///A", [255, 255, 192]], + ["////A", null], + ["/", null], + ["A/", [3]], + ["AA/", [0, 15]], + ["AAAA/", null], + ["AAA/", [0, 0, 63]], + ["\u0000nonsense", null], + ["abcd\u0000nonsense", null], + ["YQ", [97]], + ["YR", [97]], + ["~~", null], + ["..", null], + ["--", null], + ["__", null] +] diff --git a/test/fixtures/wpt/fetch/data-urls/resources/data-urls.json b/test/fixtures/wpt/fetch/data-urls/resources/data-urls.json new file mode 100644 index 00000000000000..be1d1e74cf5f51 --- /dev/null +++ b/test/fixtures/wpt/fetch/data-urls/resources/data-urls.json @@ -0,0 +1,208 @@ +[ + ["data://test/,X", + "text/plain;charset=US-ASCII", + [88]], + ["data://test:test/,X", + null], + ["data:,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:", + null], + ["data:text/html", + null], + ["data:text/html ;charset=x ", + null], + ["data:,", + "text/plain;charset=US-ASCII", + []], + ["data:,X#X", + "text/plain;charset=US-ASCII", + [88]], + ["data:,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/plain,X", + "text/plain", + [88]], + ["data:text/plain ,X", + "text/plain", + [88]], + ["data:text/plain%20,X", + "text/plain%20", + [88]], + ["data:text/plain\f,X", + "text/plain%0c", + [88]], + ["data:text/plain%0C,X", + "text/plain%0c", + [88]], + ["data:text/plain;,X", + "text/plain", + [88]], + ["data:;x=x;charset=x,X", + "text/plain;x=x;charset=x", + [88]], + ["data:;x=x,X", + "text/plain;x=x", + [88]], + ["data:text/plain;charset=windows-1252,%C2%B1", + "text/plain;charset=windows-1252", + [194, 177]], + ["data:text/plain;Charset=UTF-8,%C2%B1", + "text/plain;charset=UTF-8", + [194, 177]], + ["data:image/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif;hi=x,%C2%B1", + "image/gif;hi=x", + [194, 177]], + ["data:IMAGE/gif;CHARSET=x,%C2%B1", + "image/gif;charset=x", + [194, 177]], + ["data: ,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%20,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\f,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%1F,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\u0000,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%00,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/html ,X", + "text/html", + [88]], + ["data:text / html,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†/†,X", + "%e2%80%a0/%e2%80%a0", + [88]], + ["data:X,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:image/png,X X", + "image/png", + [88, 32, 88]], + ["data:application/javascript,X X", + "application/javascript", + [88, 32, 88]], + ["data:application/xml,X X", + "application/xml", + [88, 32, 88]], + ["data:text/javascript,X X", + "text/javascript", + [88, 32, 88]], + ["data:text/plain,X X", + "text/plain", + [88, 32, 88]], + ["data:unknown/unknown,X X", + "unknown/unknown", + [88, 32, 88]], + ["data:text/plain;a=\",\",X", + "text/plain;a=\"\"", + [34, 44, 88]], + ["data:text/plain;a=%2C,X", + "text/plain;a=%2C", + [88]], + ["data:;base64;base64,WA", + "text/plain", + [88]], + ["data:x/x;base64;base64,WA", + "x/x", + [88]], + ["data:x/x;base64;charset=x,WA", + "x/x;charset=x", + [87, 65]], + ["data:x/x;base64;charset=x;base64,WA", + "x/x;charset=x", + [88]], + ["data:x/x;base64;base64x,WA", + "x/x", + [87, 65]], + ["data:;base64,W%20A", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64,W%0CA", + "text/plain;charset=US-ASCII", + [88]], + ["data:x;base64x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64;x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64=x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data: ;charset=x ; base64,WA", + "text/plain;charset=x", + [88]], + ["data:;base64;,WA", + "text/plain", + [87, 65]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base 64,WA", + "text/plain", + [87, 65]], + ["data:;BASe64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;%62ase64,WA", + "text/plain", + [87, 65]], + ["data:%3Bbase64,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:;charset=x,X", + "text/plain;charset=x", + [88]], + ["data:; charset=x,X", + "text/plain;charset=x", + [88]], + ["data:;charset =x,X", + "text/plain", + [88]], + ["data:;charset= x,X", + "text/plain;charset=\" x\"", + [88]], + ["data:;charset=,X", + "text/plain", + [88]], + ["data:;charset,X", + "text/plain", + [88]], + ["data:;charset=\"x\",X", + "text/plain;charset=x", + [88]], + ["data:;CHARSET=\"X\",X", + "text/plain;charset=X", + [88]] +] diff --git a/test/fixtures/wpt/fetch/h1-parsing/README.md b/test/fixtures/wpt/fetch/h1-parsing/README.md new file mode 100644 index 00000000000000..487a892dcffb09 --- /dev/null +++ b/test/fixtures/wpt/fetch/h1-parsing/README.md @@ -0,0 +1,5 @@ +This directory tries to document "rough consensus" on where HTTP/1 parsing should end up between browsers. + +Any tests that browsers currently fail should have associated bug reports. + +[whatwg/fetch issue #1156](https://github.com/whatwg/fetch/issues/1156) provides context for this effort and pointers to the various issues, pull requests, and bug reports that are associated with it. diff --git a/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js b/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js new file mode 100644 index 00000000000000..6b46ed632f4377 --- /dev/null +++ b/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js @@ -0,0 +1,23 @@ +// These tests expect that a network error is returned if there's a CR that is not immediately +// followed by LF before reaching message-body. +// +// No browser does this currently, but Firefox does treat it equivalently to a space which gives +// hope. + +[ + "HTTP/1.1\r200 OK\n\nBODY", + "HTTP/1.1 200\rOK\n\nBODY", + "HTTP/1.1 200 OK\n\rHeader: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader\r: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader:\r Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\r\n\nBody", + "HTTP/1.1 200 OK\nHeader: Value\r\r\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\rHeader2: Value2\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\rBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\r" +].forEach(input => { + promise_test(t => { + const message = encodeURIComponent(input); + return promise_rejects_js(t, TypeError, fetch(`resources/message.py?message=${message}`)); + }, `Parsing response with a lone CR before message-body (${input})`); +}); diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js b/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js new file mode 100644 index 00000000000000..f1afeeb740b1d7 --- /dev/null +++ b/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js @@ -0,0 +1,31 @@ +async_test(t => { + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + script.src = "resources/script-with-0x00-in-header.py"; + script.onerror = t.step_func_done(); + script.onload = t.unreached_func(); + document.body.append(script); +}, "Expect network error for script with 0x00 in a header"); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "resources/document-with-0x00-in-header.py"; + // If network errors result in load events for frames per + // https://github.com/whatwg/html/issues/125 and https://github.com/whatwg/html/issues/1230 this + // should be changed to use the load event instead. + t.step_timeout(() => { + assert_equals(frame.contentDocument, null); + t.done(); + }, 1000); + document.body.append(frame); +}, "Expect network error for frame navigation to resource with 0x00 in a header"); + +async_test(t => { + const img = document.createElement("img"); + t.add_cleanup(() => img.remove()); + img.src = "resources/blue-with-0x00-in-a-header.asis"; + img.onerror = t.step_func_done(); + img.onload = t.unreached_func(); + document.body.append(img); +}, "Expect network error for image with 0x00 in a header"); diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/README.md b/test/fixtures/wpt/fetch/h1-parsing/resources/README.md new file mode 100644 index 00000000000000..2175d274088bda --- /dev/null +++ b/test/fixtures/wpt/fetch/h1-parsing/resources/README.md @@ -0,0 +1,6 @@ +`blue-with-0x00-in-a-header.asis` is a copy from `../../images/blue.png` with the following prepended using Control Pictures to signify actual newlines and 0x00: +``` +HTTP/1.1 200 AN IMAGE␍␊ +Content-Type: image/png␍␊ +Custom: ␀␍␊␍␊ +``` diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis b/test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis new file mode 100644 index 0000000000000000000000000000000000000000..973d4dd7b313ffdf78dbe62021efcf706ddd08b0 GIT binary patch literal 890 zcmeYW2?@|Q)H75tGB8kZ^i%Nkb#!;-<#Nu?D@n~O(G96ANVQVP%uP&B)i20P2TGI{ zm*nSKDKPMI@p8T2zc;|ootI0Bi-CcG*VDr#2uK43fn*j36Ofbza#(GE494OhcPEe( z5XnGPu%tWsI)ar0`P@L9$O=@*S>O>_%)r10Rs$j#fjA$CQ-Fq4dAc};RKx*!K;+2F zz`z4x0!ec?tI(5Gzyr>MsxZ2+s4&bniY>|nBI6FE93V`4AeAAR0pS2iIbCL5DIk4e zaVn5>1!DOsAOSK6q{tbFAxI2J)kBy-J N)78%f$a6{n5&(^<{4f9j literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js b/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js new file mode 100644 index 00000000000000..5776cf4050f01e --- /dev/null +++ b/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js @@ -0,0 +1,98 @@ +[ + { + input: "", + expected: null + }, + { + input: "BLAH", + expected: null + }, + { + input: "0 OK", + expected: { + status: 0, + statusText: "OK" + } + }, + { + input: "1 OK", + expected: { + status: 1, + statusText: "OK" + } + }, + { + input: "99 NOT OK", + expected: { + status: 99, + statusText: "NOT OK" + } + }, + { + input: "077 77", + expected: { + status: 77, + statusText: "77" + } + }, + { + input: "099 HELLO", + expected: { + status: 99, + statusText: "HELLO" + } + }, + { + input: "200", + expected: { + status: 200, + statusText: "" + } + }, + { + input: "999 DOES IT MATTER", + expected: { + status: 999, + statusText: "DOES IT MATTER" + } + }, + { + input: "1000 BOO", + expected: null + }, + { + input: "0200 BOO", + expected: null + }, + { + input: "65736 NOT 200 OR SOME SUCH", + expected: null + }, + { + input: "131072 HI", + expected: null + }, + { + input: "-200 TEST", + expected: null + }, + { + input: "0xA", + expected: null + }, + { + input: "C8", + expected: null + } +].forEach(({ description, input, expected }) => { + promise_test(async t => { + if (expected !== null) { + const response = await fetch("resources/status-code.py?input=" + input); + assert_equals(response.status, expected.status); + assert_equals(response.statusText, expected.statusText); + assert_equals(response.headers.get("header-parsing"), "is sad"); + } else { + await promise_rejects_js(t, TypeError, fetch("resources/status-code.py?input=" + input)); + } + }, `HTTP/1.1 ${input} ${expected === null ? "(network error)" : ""}`); +}); diff --git a/test/fixtures/wpt/fetch/http-cache/304-update.any.js b/test/fixtures/wpt/fetch/http-cache/304-update.any.js new file mode 100644 index 00000000000000..15484f01eb32be --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/304-update.any.js @@ -0,0 +1,146 @@ +// META: global=window,worker +// META: title=HTTP Cache - 304 Updates +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache updates returned headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates returned headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "ABC"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["ETag", "ABC"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "DEF"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "DEF"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "Content-* header", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "GHI"], + ["Content-Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "GHI"], + ["Content-Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/README.md b/test/fixtures/wpt/fetch/http-cache/README.md new file mode 100644 index 00000000000000..a32d500a7ef07f --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/README.md @@ -0,0 +1,71 @@ +## HTTP Caching Tests + +These tests cover HTTP-specified behaviours for caches, primarily from +[RFC7234](http://httpwg.org/specs/rfc7234.html), but as seen through the +lens of Fetch. + +A few notes: + +* By its nature, caching is optional; some tests expecting a response to be + cached might fail because the client chose not to cache it, or chose to + race the cache with a network request. + +* Likewise, some tests might fail because there is a separate document-level + cache that's ill-defined; see [this + issue](https://github.com/whatwg/fetch/issues/354). + +* [Partial content tests](partial.html) (a.k.a. Range requests) are not specified + in Fetch; tests are included here for interest only. + +* Some browser caches will behave differently when reloading / + shift-reloading, despite the `cache mode` staying the same. + +* At the moment, Edge doesn't appear to using HTTP caching in conjunction + with Fetch at all. + + +## Test Format + +Each test run gets its own URL and randomized content and operates independently. + +Each test is an an array of objects, with the following members: + +- `name` - The name of the test. +- `requests` - a list of request objects (see below). + +Possible members of a request object: + +- template - A template object for the request, by name. +- request_method - A string containing the HTTP method to be used. +- request_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the request. +- request_body - A string to use as the request body. +- mode - The mode string to pass to `fetch()`. +- credentials - The credentials string to pass to `fetch()`. +- cache - The cache string to pass to `fetch()`. +- pause_after - Boolean controlling a 3-second pause after the request completes. +- response_status - A `[number, string]` array containing the HTTP status code + and phrase to return. +- response_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the response. These values will also be checked like + expected_response_headers, unless there is a third value that is + `false`. See below for special handling considerations. +- response_body - String to send as the response body. If not set, it will contain + the test identifier. +- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]` +- expected_status - A number representing a HTTP status code to check the response for. + If not set, the value of `response_status[0]` will be used; if that + is not set, 200 will be used. +- expected_request_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the request for. +- expected_response_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the response for. See also response_headers. +- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking. + +Some headers in `response_headers` are treated specially: + +* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server. +* For URL-carrying headers, the value will be appended as a query parameter for `target`. + +See the source for exact details. + diff --git a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html new file mode 100644 index 00000000000000..905facdc8883bd --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html new file mode 100644 index 00000000000000..a8979baf548dc8 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js b/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js new file mode 100644 index 00000000000000..8f406d5a6a5003 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Fetch - Cache Mode +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache", + requests: [ + { + cache: "no-cache", + expected_request_headers: [['cache-control', 'max-age=0']] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present", + requests: [ + { + cache: "no-cache", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store", + requests: [ + { + cache: "no-store", + expected_request_headers: [ + ['cache-control', 'no-cache'], + ['pragma', 'no-cache'] + ] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present", + requests: [ + { + cache: "no-store", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present", + requests: [ + { + cache: "no-store", + request_headers: [['pragma', 'foo']], + expected_request_headers: [['pragma', 'foo']] + } + ] + } +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/cc-request.any.js b/test/fixtures/wpt/fetch/http-cache/cc-request.any.js new file mode 100644 index 00000000000000..d55656684144f8 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/cc-request.any.js @@ -0,0 +1,202 @@ +// META: global=window,worker +// META: title=HTTP Cache - Cache-Control Request Directives +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=0"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=1"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "1800"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-age=600"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1"] + ], + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "2000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=2000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "1000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=1000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Last-Modified", -10000] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "lm_validate" + } + ] + }, + { + name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["ETag", http_content("abc")] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "etag_validate" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-store"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached", + requests: [ + { + request_headers: [ + ["Cache-Control", "only-if-cached"] + ], + expected_status: 504, + expected_response_text: null + } + ] + } +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/freshness.any.js b/test/fixtures/wpt/fetch/http-cache/freshness.any.js new file mode 100644 index 00000000000000..6b97c8244f647c --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/freshness.any.js @@ -0,0 +1,215 @@ +// META: global=window,worker +// META: title=HTTP Cache - Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + // response directives + { + name: "HTTP cache reuses a response with a future Expires", + requests: [ + { + response_headers: [ + ["Expires", (30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a past Expires", + requests: [ + { + response_headers: [ + ["Expires", (-30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a present Expires", + requests: [ + { + response_headers: [ + ["Expires", 0] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with an invalid Expires", + requests: [ + { + response_headers: [ + ["Expires", "0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", -10000] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", "0"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1, s-maxage=3600"] + ], + pause_after: true, + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "12000"] + ], + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-store"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-store"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-cache"], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-cache"], + ["Expires", 10000], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/heuristic.any.js b/test/fixtures/wpt/fetch/http-cache/heuristic.any.js new file mode 100644 index 00000000000000..d846131888288c --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/heuristic.any.js @@ -0,0 +1,93 @@ +// META: global=window,worker +// META: title=HTTP Cache - Heuristic Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)], + ["Cache-Control", "public"] + ], + }, + { + expected_type: "cached", + response_status: [299, "Whatever"] + } + ] + }, + { + name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + }, + { + expected_type: "not_cached" + } + ] + } +]; + +function check_status(status) { + var succeed = status[0]; + var code = status[1]; + var phrase = status[2]; + var body = status[3]; + if (body === undefined) { + body = http_content(code); + } + var expected_type = "not_cached"; + var desired = "does not use" + if (succeed === true) { + expected_type = "cached"; + desired = "reuses"; + } + tests.push( + { + name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness", + requests: [ + { + response_status: [code, phrase], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + response_body: body + }, + { + expected_type: expected_type, + response_status: [code, phrase], + response_body: body + } + ] + } + ) +} +[ + [true, 200, "OK"], + [true, 203, "Non-Authoritative Information"], + [true, 204, "No Content", ""], + [true, 404, "Not Found"], + [true, 405, "Method Not Allowed"], + [true, 410, "Gone"], + [true, 414, "URI Too Long"], + [true, 501, "Not Implemented"] +].forEach(check_status); +[ + [false, 201, "Created"], + [false, 202, "Accepted"], + [false, 403, "Forbidden"], + [false, 502, "Bad Gateway"], + [false, 503, "Service Unavailable"], + [false, 504, "Gateway Timeout"], +].forEach(check_status); +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/http-cache.js b/test/fixtures/wpt/fetch/http-cache/http-cache.js new file mode 100644 index 00000000000000..c0d682d2cba654 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/http-cache.js @@ -0,0 +1,272 @@ +/* global btoa fetch token promise_test step_timeout */ +/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */ + +const templates = { + 'fresh': { + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'stale': { + 'response_headers': [ + ['Expires', -5000], + ['Last-Modified', -100000] + ] + }, + 'lcl_response': { + 'response_headers': [ + ['Location', 'location_target'], + ['Content-Location', 'content_location_target'] + ] + }, + 'location': { + 'query_arg': 'location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'content_location': { + 'query_arg': 'content_location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + } +} + +const noBodyStatus = new Set([204, 304]) + +function makeTest (test) { + return function () { + var uuid = token() + var requests = expandTemplates(test) + var fetchFunctions = makeFetchFunctions(requests, uuid) + return runTest(fetchFunctions, requests, uuid) + } +} + +function makeFetchFunctions(requests, uuid) { + var fetchFunctions = [] + for (let i = 0; i < requests.length; ++i) { + fetchFunctions.push({ + code: function (idx) { + var config = requests[idx] + var url = makeTestUrl(uuid, config) + var init = fetchInit(requests, config) + return fetch(url, init) + .then(makeCheckResponse(idx, config)) + .then(makeCheckResponseBody(config, uuid), function (reason) { + if ('expected_type' in config && config.expected_type === 'error') { + assert_throws_js(TypeError, function () { throw reason }) + } else { + throw reason + } + }) + }, + pauseAfter: 'pause_after' in requests[i] + }) + } + return fetchFunctions +} + +function runTest(fetchFunctions, requests, uuid) { + var idx = 0 + function runNextStep () { + if (fetchFunctions.length) { + var nextFetchFunction = fetchFunctions.shift() + if (nextFetchFunction.pauseAfter === true) { + return nextFetchFunction.code(idx++) + .then(pause) + .then(runNextStep) + } else { + return nextFetchFunction.code(idx++) + .then(runNextStep) + } + } else { + return Promise.resolve() + } + } + + return runNextStep() + .then(function () { + return getServerState(uuid) + }).then(function (testState) { + checkRequests(requests, testState) + return Promise.resolve() + }) +} + +function expandTemplates (test) { + var rawRequests = test.requests + var requests = [] + for (let i = 0; i < rawRequests.length; i++) { + var request = rawRequests[i] + request.name = test.name + if ('template' in request) { + var template = templates[request['template']] + for (let member in template) { + if (!request.hasOwnProperty(member)) { + request[member] = template[member] + } + } + } + requests.push(request) + } + return requests +} + +function fetchInit (requests, config) { + var init = { + 'headers': [] + } + if ('request_method' in config) init.method = config['request_method'] + if ('request_headers' in config) init.headers = config['request_headers'] + if ('name' in config) init.headers.push(['Test-Name', config.name]) + if ('request_body' in config) init.body = config['request_body'] + if ('mode' in config) init.mode = config['mode'] + if ('credentials' in config) init.mode = config['credentials'] + if ('cache' in config) init.cache = config['cache'] + init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))]) + return init +} + +function makeCheckResponse (idx, config) { + return function checkResponse (response) { + var reqNum = idx + 1 + var resNum = parseInt(response.headers.get('Server-Request-Count')) + if ('expected_type' in config) { + if (config.expected_type === 'error') { + assert_true(false, `Request ${reqNum} doesn't throw an error`) + return response.text() + } + if (config.expected_type === 'cached') { + assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`) + } + if (config.expected_type === 'not_cached') { + assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`) + } + } + if ('expected_status' in config) { + assert_equals(response.status, config.expected_status, + `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`) + } else if ('response_status' in config) { + assert_equals(response.status, config.response_status[0], + `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`) + } else { + assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`) + } + if ('response_headers' in config) { + config.response_headers.forEach(function (header) { + if (header.len < 3 || header[2] === true) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + } + }) + } + if ('expected_response_headers' in config) { + config.expected_response_headers.forEach(function (header) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + }) + } + return response.text() + } +} + +function makeCheckResponseBody (config, uuid) { + return function checkResponseBody (resBody) { + var statusCode = 200 + if ('response_status' in config) { + statusCode = config.response_status[0] + } + if ('expected_response_text' in config) { + if (config.expected_response_text !== null) { + assert_equals(resBody, config.expected_response_text, + `Response body is "${resBody}", not expected "${config.expected_response_text}"`) + } + } else if ('response_body' in config && config.response_body !== null) { + assert_equals(resBody, config.response_body, + `Response body is "${resBody}", not sent "${config.response_body}"`) + } else if (!noBodyStatus.has(statusCode)) { + assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`) + } + } +} + +function checkRequests (requests, testState) { + var testIdx = 0 + for (let i = 0; i < requests.length; ++i) { + var expectedValidatingHeaders = [] + var config = requests[i] + var serverRequest = testState[testIdx] + var reqNum = i + 1 + if ('expected_type' in config) { + if (config.expected_type === 'cached') continue // the server will not see the request + if (config.expected_type === 'etag_validated') { + expectedValidatingHeaders.push('if-none-match') + } + if (config.expected_type === 'lm_validated') { + expectedValidatingHeaders.push('if-modified-since') + } + } + testIdx++ + expectedValidatingHeaders.forEach(vhdr => { + assert_own_property(serverRequest.request_headers, vhdr, + `request ${reqNum} doesn't have ${vhdr} header`) + }) + if ('expected_request_headers' in config) { + config.expected_request_headers.forEach(expectedHdr => { + assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1], + `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`) + }) + } + } +} + +function pause () { + return new Promise(function (resolve, reject) { + step_timeout(function () { + return resolve() + }, 3000) + }) +} + +function makeTestUrl (uuid, config) { + var arg = '' + var base_url = '' + if ('base_url' in config) { + base_url = config.base_url + } + if ('query_arg' in config) { + arg = `&target=${config.query_arg}` + } + return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}` +} + +function getServerState (uuid) { + return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`) + .then(function (response) { + return response.text() + }).then(function (text) { + return JSON.parse(text) || [] + }) +} + +function run_tests (tests) { + tests.forEach(function (test) { + promise_test(makeTest(test), test.name) + }) +} + +var contentStore = {} +function http_content (csKey) { + if (csKey in contentStore) { + return contentStore[csKey] + } else { + var content = btoa(Math.random() * Date.now()) + contentStore[csKey] = content + return content + } +} diff --git a/test/fixtures/wpt/fetch/http-cache/invalidate.any.js b/test/fixtures/wpt/fetch/http-cache/invalidate.any.js new file mode 100644 index 00000000000000..9f8090ace659d2 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/invalidate.any.js @@ -0,0 +1,235 @@ +// META: global=window,worker +// META: title=HTTP Cache - Invalidation +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: 'HTTP cache invalidates after a successful response from a POST', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate after a failed response from an unsafe request', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a PUT', + requests: [ + { + template: "fresh" + }, { + template: "fresh", + request_method: "PUT", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a DELETE', + requests: [ + { + template: "fresh" + }, { + request_method: "DELETE", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from an unknown method', + requests: [ + { + template: "fresh" + }, { + request_method: "FOO", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + + + { + name: 'HTTP cache invalidates Location URL after a successful response from a POST', + requests: [ + { + template: "location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a PUT', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a DELETE', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from an unknown method', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + + + + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST', + requests: [ + { + template: "content_location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "content_location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + } + +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/partial.any.js b/test/fixtures/wpt/fetch/http-cache/partial.any.js new file mode 100644 index 00000000000000..a75b8115f5cb54 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/partial.any.js @@ -0,0 +1,187 @@ +// META: global=window,worker +// META: title=HTTP Cache - Partial Content +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache stores partial content and reuses it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234", + expected_request_headers: [ + ["Range", "bytes=-5"] + ] + }, + { + request_headers: [ + ["Range", "bytes=-5"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01234" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=0-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01" + }, + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=1-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "1234567890" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "0123456789A" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "A" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=6-8"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ["Range", "bytes=6-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "4" + } + ] + }, + { + name: "HTTP cache stores partial content and completes it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 0-4/10"] + ], + response_body: "01234" + }, + { + expected_request_headers: [ + ["range", "bytes=5-"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/post-patch.any.js b/test/fixtures/wpt/fetch/http-cache/post-patch.any.js new file mode 100644 index 00000000000000..0a69baa5c6646c --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/post-patch.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: title=HTTP Cache - Caching POST and PATCH responses +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "PATCH", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "POST", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + } +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html new file mode 100644 index 00000000000000..48b16180cfdfa5 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html @@ -0,0 +1,34 @@ + + + + + HTTP Cache - helper + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html new file mode 100644 index 00000000000000..edb579479414fe --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html @@ -0,0 +1,28 @@ + + + + + HTTP Cache - helper + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/http-cache/split-cache.html b/test/fixtures/wpt/fetch/http-cache/split-cache.html new file mode 100644 index 00000000000000..4b04c9ff79bf95 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/split-cache.html @@ -0,0 +1,142 @@ + + + + + HTTP Cache - Partioning by site + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/http-cache/status.any.js b/test/fixtures/wpt/fetch/http-cache/status.any.js new file mode 100644 index 00000000000000..10c83a25a26dc0 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/status.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker +// META: title=HTTP Cache - Status Codes +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = []; +function check_status(status) { + var code = status[0]; + var phrase = status[1]; + var body = status[2]; + if (body === undefined) { + body = http_content(code); + } + tests.push({ + name: "HTTP cache goes to the network if it has a stale " + code + " response", + requests: [ + { + template: "stale", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "not_cached", + response_status: [code, phrase], + response_body: body + } + ] + }) + tests.push({ + name: "HTTP cache avoids going to the network if it has a fresh " + code + " response", + requests: [ + { + template: "fresh", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "cached", + response_status: [code, phrase], + response_body: body + } + ] + }) +} +[ + [200, "OK"], + [203, "Non-Authoritative Information"], + [204, "No Content", null], + [299, "Whatever"], + [400, "Bad Request"], + [404, "Not Found"], + [410, "Gone"], + [499, "Whatever"], + [500, "Internal Server Error"], + [502, "Bad Gateway"], + [503, "Service Unavailable"], + [504, "Gateway Timeout"], + [599, "Whatever"] +].forEach(check_status); +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/vary.any.js b/test/fixtures/wpt/fetch/http-cache/vary.any.js new file mode 100644 index 00000000000000..2cfd226af81173 --- /dev/null +++ b/test/fixtures/wpt/fetch/http-cache/vary.any.js @@ -0,0 +1,313 @@ +// META: global=window,worker +// META: title=HTTP Cache - Vary +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "1"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "2"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't invalidate existing Vary response", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + response_body: http_content('foo_1') + }, + { + request_headers: [ + ["Foo", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + expected_type: "not_cached", + response_body: http_content('foo_2'), + }, + { + request_headers: [ + ["Foo", "1"] + ], + response_body: http_content('foo_1'), + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't pay attention to headers not listed in Vary", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Other", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + }, + { + request_headers: [ + ["Foo", "1"], + ["Other", "3"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses two-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses three-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc4"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response with a field value of '*'", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "*"] + ] + }, + { + request_headers: [ + ["*", "1"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + } +]; +run_tests(tests); diff --git a/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html b/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html new file mode 100644 index 00000000000000..4a887f3d331cf2 --- /dev/null +++ b/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html @@ -0,0 +1,28 @@ + + +Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin + + + + diff --git a/test/fixtures/wpt/fetch/metadata/META.yml b/test/fixtures/wpt/fetch/metadata/META.yml new file mode 100644 index 00000000000000..85f0a7d2ee1261 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/META.yml @@ -0,0 +1,4 @@ +spec: https://w3c.github.io/webappsec-fetch-metadata/ +suggested_reviewers: + - mikewest + - iVanlIsh diff --git a/test/fixtures/wpt/fetch/metadata/README.md b/test/fixtures/wpt/fetch/metadata/README.md new file mode 100644 index 00000000000000..34864d4a4b6bd9 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/README.md @@ -0,0 +1,9 @@ +Fetch Metadata Tests +==================== + +This directory contains tests related to the Fetch Metadata proposal: + +: Explainer +:: +: "Spec" +:: diff --git a/test/fixtures/wpt/fetch/metadata/download.https.sub.html b/test/fixtures/wpt/fetch/metadata/download.https.sub.html new file mode 100644 index 00000000000000..6f2a0434d497f6 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/download.https.sub.html @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html new file mode 100644 index 00000000000000..1900dbdf08139c --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html @@ -0,0 +1,63 @@ + + + + + + + + + +

Relevant issue: +<embed> should support loading random HTML documents, like <object> +

+ + diff --git a/test/fixtures/wpt/fetch/metadata/favicon.https.sub.html b/test/fixtures/wpt/fetch/metadata/favicon.https.sub.html new file mode 100644 index 00000000000000..50ea5b27415826 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/favicon.https.sub.html @@ -0,0 +1,67 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js new file mode 100644 index 00000000000000..d52474353ba557 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch with preflight"); +}, "Same-site fetch with preflight"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch with preflight"); +}, "Cross-site fetch with preflight"); diff --git a/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--fallback.https.sub.html b/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--fallback.https.sub.html new file mode 100644 index 00000000000000..9f494461372d3a --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--fallback.https.sub.html @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--respondWith.https.sub.html b/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--respondWith.https.sub.html new file mode 100644 index 00000000000000..03d5fd1cc31b6f --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/fetch-via-serviceworker--respondWith.https.sub.html @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js new file mode 100644 index 00000000000000..aeec5cdf2dc11d --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js @@ -0,0 +1,58 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-origin fetch"); +}, "Same-origin fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch"); +}, "Same-site fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch"); +}, "Cross-site fetch"); + +// Mode +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "same-origin"}, { + "site": "same-origin", + "user": "", + "mode": "same-origin", + "dest": "empty" + }, "Same-origin mode"); +}, "Same-origin mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "cors"}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "CORS mode"); +}, "CORS mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "no-cors"}, { + "site": "same-origin", + "user": "", + "mode": "no-cors", + "dest": "empty" + }, "no-CORS mode"); +}, "no-CORS mode"); diff --git a/test/fixtures/wpt/fetch/metadata/fetch.sub.html b/test/fixtures/wpt/fetch/metadata/fetch.sub.html new file mode 100644 index 00000000000000..1659a9fb6bf274 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/fetch.sub.html @@ -0,0 +1,29 @@ + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/font.https.sub.html b/test/fixtures/wpt/fetch/metadata/font.https.sub.html new file mode 100644 index 00000000000000..2c705085d6a30c --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/font.https.sub.html @@ -0,0 +1,78 @@ + + + + + + + +
1
+
2
+
3
+ + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/form.https.sub.html b/test/fixtures/wpt/fetch/metadata/form.https.sub.html new file mode 100644 index 00000000000000..2e6332c747f554 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/form.https.sub.html @@ -0,0 +1,85 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/history.https.sub.html b/test/fixtures/wpt/fetch/metadata/history.https.sub.html new file mode 100644 index 00000000000000..f0119e6c754d36 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/history.https.sub.html @@ -0,0 +1,79 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/iframe.https.sub.html b/test/fixtures/wpt/fetch/metadata/iframe.https.sub.html new file mode 100644 index 00000000000000..1c793fd7e45990 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/iframe.https.sub.html @@ -0,0 +1,85 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/iframe.sub.html b/test/fixtures/wpt/fetch/metadata/iframe.sub.html new file mode 100644 index 00000000000000..e0d5b0b8cb824e --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/iframe.sub.html @@ -0,0 +1,82 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/img.https.sub.html b/test/fixtures/wpt/fetch/metadata/img.https.sub.html new file mode 100644 index 00000000000000..93acffef11ae23 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/img.https.sub.html @@ -0,0 +1,75 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html b/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html new file mode 100644 index 00000000000000..32c9cf77f90ee9 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/object.https.sub.html b/test/fixtures/wpt/fetch/metadata/object.https.sub.html new file mode 100644 index 00000000000000..fae5b37b592d93 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/object.https.sub.html @@ -0,0 +1,62 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/portal.https.sub.html b/test/fixtures/wpt/fetch/metadata/portal.https.sub.html new file mode 100644 index 00000000000000..55b555a1b8ec44 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/portal.https.sub.html @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/prefetch.https.sub.html b/test/fixtures/wpt/fetch/metadata/prefetch.https.sub.html new file mode 100644 index 00000000000000..a0ff73c0d0868e --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/prefetch.https.sub.html @@ -0,0 +1,36 @@ + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/preload.https.sub.html b/test/fixtures/wpt/fetch/metadata/preload.https.sub.html new file mode 100644 index 00000000000000..29042a854740eb --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/preload.https.sub.html @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/cross-site-redirect.https.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/cross-site-redirect.https.sub.html new file mode 100644 index 00000000000000..827eb982e47f3f --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/cross-site-redirect.https.sub.html @@ -0,0 +1,77 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-cross-site.https.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-cross-site.https.sub.html new file mode 100644 index 00000000000000..fc986aaf4fdcff --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-cross-site.https.sub.html @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade-prefetch.optional.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade-prefetch.optional.sub.html new file mode 100644 index 00000000000000..970eb3373a04b6 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade-prefetch.optional.sub.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html new file mode 100644 index 00000000000000..907cf5c617df56 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html @@ -0,0 +1,73 @@ + + + + + + + + + + +
Downgraded then upgraded font
+ + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-same-site.https.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-same-site.https.sub.html new file mode 100644 index 00000000000000..f5483ac3fa2f3f --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-same-site.https.sub.html @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade-prefetch.optional.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade-prefetch.optional.sub.html new file mode 100644 index 00000000000000..c69f0e92592723 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade-prefetch.optional.sub.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html new file mode 100644 index 00000000000000..133576cb0d7236 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html @@ -0,0 +1,67 @@ + + + + + + + + + + +
Upgraded font
+ + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade-prefetch.optional.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade-prefetch.optional.sub.html new file mode 100644 index 00000000000000..a446cbad840515 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade-prefetch.optional.sub.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html new file mode 100644 index 00000000000000..9eb04822b683bf --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html @@ -0,0 +1,69 @@ + + + + + + + + + + +
Downgraded font
+ + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/same-origin-redirect.https.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/same-origin-redirect.https.sub.html new file mode 100644 index 00000000000000..38921a71314a54 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/same-origin-redirect.https.sub.html @@ -0,0 +1,80 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/redirect/same-site-redirect.https.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/same-site-redirect.https.sub.html new file mode 100644 index 00000000000000..f8709a14c0f0ff --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/redirect/same-site-redirect.https.sub.html @@ -0,0 +1,80 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/report.https.sub.html b/test/fixtures/wpt/fetch/metadata/report.https.sub.html new file mode 100644 index 00000000000000..1041094cd8861f --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/report.https.sub.html @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers b/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers new file mode 100644 index 00000000000000..1ec5df78f30183 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers @@ -0,0 +1,3 @@ +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri /fetch/metadata/resources/record-header.py?file=report-same-origin +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-same-site +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-cross-site diff --git a/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js b/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js new file mode 100644 index 00000000000000..18626d3d8458b9 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js @@ -0,0 +1 @@ +self.postMessage("Loaded"); diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--frame.html b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--frame.html new file mode 100644 index 00000000000000..98798025005497 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--frame.html @@ -0,0 +1,3 @@ + + +Page Title diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js new file mode 100644 index 00000000000000..09858b2663f084 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + // Empty event handler - will fallback to the network. +}); diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--frame.html b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--frame.html new file mode 100644 index 00000000000000..98798025005497 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--frame.html @@ -0,0 +1,3 @@ + + +Page Title diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js new file mode 100644 index 00000000000000..8bf8d8f22175f9 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); diff --git a/test/fixtures/wpt/fetch/metadata/resources/go-back.html b/test/fixtures/wpt/fetch/metadata/resources/go-back.html new file mode 100644 index 00000000000000..002c4cea473885 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/go-back.html @@ -0,0 +1,3 @@ + + + diff --git a/test/fixtures/wpt/fetch/metadata/resources/helper.js b/test/fixtures/wpt/fetch/metadata/resources/helper.js new file mode 100644 index 00000000000000..608a5297881b91 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/helper.js @@ -0,0 +1,59 @@ +function validate_expectations(key, expected, tag) { + return fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +function validate_expectations_custom_url(url, header, expected, tag) { + return fetch(url, header) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +/** + * @param {object} value + * @param {object} expected + * @param {string} tag + **/ +function assert_header_equals(value, expected, tag) { + if (typeof(value) === "string"){ + assert_not_equals(value, "No header has been recorded"); + value = JSON.parse(value); + } + + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); +} + +/** + * @param {object} value + * @param {string} tag + **/ +function assert_no_headers(value, tag) { + if (typeof(value) === "string"){ + if (value == "No header has been recorded") return; + value = JSON.parse(value); + } + + test(t => assert_equals(value.mode, ""), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, ""), `${tag}: sec-fetch-site`); + if (expected.hasOwnProperty("user")) + test(t => assert_equals(value.user, ""), `${tag}: sec-fetch-user`); + test(t => assert_equals(value.dest, ""), `${tag}: sec-fetch-dest`); +} diff --git a/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js b/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js new file mode 100644 index 00000000000000..0aec19d4ee78b1 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js @@ -0,0 +1,226 @@ +function createVideoElement() { + let el = document.createElement('video'); + el.src = '/media/movie_5.mp4'; + el.setAttribute('controls', ''); + el.setAttribute('crossorigin', ''); + return el; +} + +function createTrack() { + let el = document.createElement('track'); + el.setAttribute('default', ''); + el.setAttribute('kind', 'captions'); + el.setAttribute('srclang', 'en'); + return el; +} + +let secureRedirectURL = 'https://{{host}}:{{ports[https][0]}}/fetch/api/resources/redirect.py?location='; +let insecureRedirectURL = 'http://{{host}}:{{ports[http][0]}}/fetch/api/resources/redirect.py?location='; +let secureTestURL = 'https://{{host}}:{{ports[https][0]}}/fetch/metadata/'; +let insecureTestURL = 'http://{{host}}:{{ports[http][0]}}/fetch/metadata/'; + +// Helper to craft an URL that will go from HTTPS => HTTP => HTTPS to +// simulate us downgrading then upgrading again during the same redirect chain. +function MultipleRedirectTo(partialPath) { + let finalURL = insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); + return secureRedirectURL + encodeURIComponent(finalURL); +} + +// Helper to craft an URL that will go from HTTP => HTTPS to simulate upgrading a +// given request. +function upgradeRedirectTo(partialPath) { + return insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); +} + +// Helper to craft an URL that will go from HTTPS => HTTP to simulate downgrading a +// given request. +function downgradeRedirectTo(partialPath) { + return secureRedirectURL + encodeURIComponent(insecureTestURL + partialPath); +} + +// Helper to test the behavior of the `prefetch` Link type [1]. Because the the +// behavior under test is optional [2], this function should only be used in +// tests which have been denoted as "optional" [3]. +// +// [1] https://html.spec.whatwg.org/#link-type-prefetch +// [2] https://w3c.github.io/resource-hints/#load-and-error-events +// [3] https://web-platform-tests.org/writing-tests/file-names.html +function testPrefetch(nonce, testNamePrefix, urlHelperMethod, expectedResults) { + async_test(t => { + let key = 'prefetch' + nonce; + let e = document.createElement('link'); + e.rel = 'prefetch'; + e.crossOrigin = 'anonymous'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key) + '&simple=true'; + e.onload = t.step_func(e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(t.step_func(response => response.text())) + .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' prefetch => No headers'))) + .catch(t.unreached_func('Fetching and verifying the results should succeed.')); + }); + e.onerror = t.unreached_func(); + document.head.appendChild(e); + }, testNamePrefix + ' prefetch => No headers'); +} + +// Helper to run common redirect test cases that don't require special setup on +// the test page itself. +function RunCommonRedirectTests(testNamePrefix, urlHelperMethod, expectedResults) { + async_test(t => { + let i = document.createElement('iframe'); + i.src = urlHelperMethod('resources/post-to-owner.py?iframe-navigation' + nonce); + window.addEventListener('message', t.step_func(e => { + if (e.source != i.contentWindow) { + return; + } + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'iframe'; + assert_header_equals(e.data, expectation, testNamePrefix + ' iframe'); + t.done(); + })); + + document.body.appendChild(i); + }, testNamePrefix + ' iframe'); + + async_test(t => { + let testWindow = window.open(urlHelperMethod('resources/post-to-owner.py?top-level-navigation' + nonce)); + t.add_cleanup(_ => testWindow.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != testWindow) { + return; + } + + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'document'; + assert_header_equals(e.data, expectation, testNamePrefix + ' top level navigation'); + t.done(); + })); + }, testNamePrefix + ' top level navigation'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'embed-https-redirect' + nonce; + let e = document.createElement('embed'); + e.src = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'embed'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' embed'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' embed'); + + promise_test(t => { + let key = 'fetch-redirect' + nonce; + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'empty'; + return fetch(urlHelperMethod('resources/echo-as-json.py?' + key)) + .then(r => r.json()) + .then(j => {assert_header_equals(j, expectation, testNamePrefix + ' fetch() api');}); + }, testNamePrefix + ' fetch() api'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'object-https-redirect' + nonce; + let e = document.createElement('object'); + e.data = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'object'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' object'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' object'); + + if (document.createElement('link').relList.supports('preload')) { + async_test(t => { + let key = 'preload' + nonce; + let e = document.createElement('link'); + e.rel = 'preload'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.setAttribute('as', 'track'); + e.onload = e.onerror = t.step_func_done(e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(t.step_func(response => response.text())) + .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' preload'))) + .catch(t.unreached_func()); + }); + document.head.appendChild(e); + }, testNamePrefix + ' preload'); + } + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'style-https-redirect' + nonce; + let e = document.createElement('link'); + e.rel = 'stylesheet'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'no-cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'style'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' stylesheet'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' stylesheet'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'track-https-redirect' + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = urlHelperMethod('resources/record-header.py?file=' + key); + el.onload = t.step_func(_ => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'track'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' track'))) + .then(resolve); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, testNamePrefix + ' track'); +} diff --git a/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js b/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js new file mode 100644 index 00000000000000..5eb89cb4f68a09 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js @@ -0,0 +1,9 @@ +onconnect = function(e) { + var port = e.ports[0]; + + port.addEventListener('message', function(e) { + port.postMessage("Ready"); + }); + + port.start(); +} diff --git a/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html b/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html new file mode 100644 index 00000000000000..b00c9a5776a45a --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html @@ -0,0 +1,12 @@ + + diff --git a/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml b/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml new file mode 100644 index 00000000000000..acb478ab6414b5 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/script.https.sub.html b/test/fixtures/wpt/fetch/metadata/script.https.sub.html new file mode 100644 index 00000000000000..f66f663433443e --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/script.https.sub.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/script.sub.html b/test/fixtures/wpt/fetch/metadata/script.sub.html new file mode 100644 index 00000000000000..9d6059abc177eb --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/script.sub.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/serviceworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/serviceworker.https.sub.html new file mode 100644 index 00000000000000..51bc7f29e59308 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/serviceworker.https.sub.html @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html new file mode 100644 index 00000000000000..4df858208a7457 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/style.https.sub.html b/test/fixtures/wpt/fetch/metadata/style.https.sub.html new file mode 100644 index 00000000000000..a30d81d70ddbae --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/style.https.sub.html @@ -0,0 +1,86 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/track.https.sub.html b/test/fixtures/wpt/fetch/metadata/track.https.sub.html new file mode 100644 index 00000000000000..346798fdc0e5ae --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/track.https.sub.html @@ -0,0 +1,119 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js new file mode 100644 index 00000000000000..5e32fc4e7f6a44 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js @@ -0,0 +1,30 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same origin, but spelled with a trailing dot."); +}, "Fetching a resource from the same origin, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same site, but spelled with a trailing dot."); +}, "Fetching a resource from the same site, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from a cross-site host, spelled with a trailing dot."); +}, "Fetching a resource from a cross-site host, spelled with a trailing dot."); diff --git a/test/fixtures/wpt/fetch/metadata/unload.https.sub.html b/test/fixtures/wpt/fetch/metadata/unload.https.sub.html new file mode 100644 index 00000000000000..bc26048c810725 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/unload.https.sub.html @@ -0,0 +1,64 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html b/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html new file mode 100644 index 00000000000000..94ba76a19ff28d --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html @@ -0,0 +1,199 @@ + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/worker.https.sub.html b/test/fixtures/wpt/fetch/metadata/worker.https.sub.html new file mode 100644 index 00000000000000..20a4fe54166056 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/worker.https.sub.html @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html b/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html new file mode 100644 index 00000000000000..dc72d7b8a67883 --- /dev/null +++ b/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/test/fixtures/wpt/fetch/nosniff/image.html b/test/fixtures/wpt/fetch/nosniff/image.html new file mode 100644 index 00000000000000..9dfdb94cf62a4c --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/image.html @@ -0,0 +1,39 @@ + + +
+ diff --git a/test/fixtures/wpt/fetch/nosniff/importscripts.html b/test/fixtures/wpt/fetch/nosniff/importscripts.html new file mode 100644 index 00000000000000..920b6bdd40910e --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/importscripts.html @@ -0,0 +1,14 @@ + + +
+ diff --git a/test/fixtures/wpt/fetch/nosniff/importscripts.js b/test/fixtures/wpt/fetch/nosniff/importscripts.js new file mode 100644 index 00000000000000..18952805bb71bb --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/importscripts.js @@ -0,0 +1,28 @@ +// Testing importScripts() +function log(w) { this.postMessage(w) } +function f() { log("FAIL") } +function p() { log("PASS") } + +const get_url = (mime, outcome) => { + let url = "resources/js.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + if (outcome) { + url += "&outcome=p" + } + return url +} + +[null, "", "x", "x/x", "text/html", "text/json"].forEach(function(mime) { + try { + importScripts(get_url(mime)) + } catch(e) { + (e.name == "NetworkError") ? p() : log("FAIL (no NetworkError exception): " + mime) + } + +}) +importScripts(get_url("text/javascript", true)) +importScripts(get_url("text/ecmascript", true)) +importScripts(get_url("text/ecmascript;blah", true)) +log("END") diff --git a/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js b/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js new file mode 100644 index 00000000000000..2a2648653cabe9 --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js @@ -0,0 +1,27 @@ +promise_test(() => fetch("resources/x-content-type-options.json").then(res => res.json()).then(runTests), "Loading JSON…"); + +function runTests(allTestData) { + for (let i = 0; i < allTestData.length; i++) { + const testData = allTestData[i], + input = encodeURIComponent(testData.input); + promise_test(t => { + let resolve; + const promise = new Promise(r => resolve = r); + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + // A + +
+ diff --git a/test/fixtures/wpt/fetch/nosniff/stylesheet.html b/test/fixtures/wpt/fetch/nosniff/stylesheet.html new file mode 100644 index 00000000000000..8f2b5476e9063b --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/stylesheet.html @@ -0,0 +1,60 @@ + + + +
+ diff --git a/test/fixtures/wpt/fetch/nosniff/worker.html b/test/fixtures/wpt/fetch/nosniff/worker.html new file mode 100644 index 00000000000000..c8c1076df5cffe --- /dev/null +++ b/test/fixtures/wpt/fetch/nosniff/worker.html @@ -0,0 +1,28 @@ + + +
+ diff --git a/test/fixtures/wpt/fetch/origin/assorted.window.js b/test/fixtures/wpt/fetch/origin/assorted.window.js new file mode 100644 index 00000000000000..fc6dd1a02870e2 --- /dev/null +++ b/test/fixtures/wpt/fetch/origin/assorted.window.js @@ -0,0 +1,211 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const origins = get_host_info(); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + // Cross-origin -> same-origin will result in setting the tainted origin flag for the second + // request. + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await fetch(url, { mode: "no-cors", method: "POST" }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and 308 redirect"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + frame.src = url; + frame.onload = () => { + resolve(); + frame.remove(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], "no Origin header"); + assert_equals(json[1], "no Origin header"); +}, "Origin header and GET navigation"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + self.addEventListener("message", e => { + if (e.data === "loaded") { + resolve(); + frame.remove(); + } + }, { once: true }); + frame.onload = () => { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = url; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and POST navigation"); + +function navigationReferrerPolicy(referrerPolicy, destination, expectedOrigin) { + return async function () { + const stash = token(); + const referrerPolicyPath = "/fetch/origin/resources/referrer-policy.py"; + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let postUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash; + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = origins.HTTP_ORIGIN + referrerPolicyPath + + "?referrerPolicy=" + referrerPolicy; + self.addEventListener("message", function listener(e) { + if (e.data === "loaded") { + resolve(); + frame.remove(); + self.removeEventListener("message", listener); + } else if (e.data === "action") { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = postUrl; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + }); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function fetchReferrerPolicy(referrerPolicy, destination, fetchMode, expectedOrigin, httpMethod) { + return async function () { + const stash = token(); + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let fetchUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash; + + await fetch(fetchUrl, { mode: fetchMode, method: httpMethod , "referrerPolicy": referrerPolicy}); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function referrerPolicyTestString(referrerPolicy, method, destination) { + return "Origin header and " + method + " " + destination + " with Referrer-Policy " + + referrerPolicy; +} + +[ + { + "policy": "no-referrer", + "expectedOriginForSameOrigin": "null", + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "same-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "origin-when-cross-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "no-referrer-when-downgrade", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "unsafe-url", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, +].forEach(testObj => { + [ + { + "name": "same-origin", + "expectedOrigin": testObj.expectedOriginForSameOrigin + }, + { + "name": "cross-origin", + "expectedOrigin": testObj.expectedOriginForCrossOrigin + } + ].forEach(destination => { + // Test form POST navigation + promise_test(navigationReferrerPolicy(testObj.policy, + destination.name, + destination.expectedOrigin), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " navigation")); + // Test fetch + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "no-cors", + destination.expectedOrigin, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch no-cors mode")); + + // Test cors mode POST + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + (destination.name == "same-origin") ? destination.expectedOrigin : origins.HTTP_ORIGIN, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch cors mode")); + + // Test cors mode GET + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + (destination.name == "same-origin") ? "no Origin header" : origins.HTTP_ORIGIN, + "GET"), + referrerPolicyTestString(testObj.policy, "GET", + destination.name + " fetch cors mode")); + }); +}); diff --git a/test/fixtures/wpt/fetch/range/general.any.js b/test/fixtures/wpt/fetch/range/general.any.js new file mode 100644 index 00000000000000..80791c3847eac7 --- /dev/null +++ b/test/fixtures/wpt/fetch/range/general.any.js @@ -0,0 +1,91 @@ +// META: global=window,worker +// META: script=/common/utils.js + +// Helpers that return headers objects with a particular guard +function headersGuardNone(fill) { + if (fill) return new Headers(fill); + return new Headers(); +} + +function headersGuardResponse(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Response('', opts).headers; +} + +function headersGuardRequest(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +function headersGuardRequestNoCors(fill) { + const opts = { mode: 'no-cors' }; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +const headerGuardTypes = [ + ['none', headersGuardNone], + ['response', headersGuardResponse], + ['request', headersGuardRequest] +]; + +for (const [guardType, createHeaders] of headerGuardTypes) { + test(() => { + // There are three ways to set headers. + // Filling, appending, and setting. Test each: + let headers = createHeaders({ Range: 'foo' }); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + }, `Range header setting allowed for guard type: ${guardType}`); +} + +test(() => { + let headers = headersGuardRequestNoCors({ Range: 'foo' }); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.append('Range', 'foo'); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.set('Range', 'foo'); + assert_false(headers.has('Range')); +}, `Privileged header not allowed for guard type: request-no-cors`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=0-10', + 'foo=0-10', + 'foo', + '' + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + + await fetch(wavURL, { + headers: { Range: rangeHeader } + }); + + const response = await fetch(stashTakeURL); + assert_equals(await response.json(), 'identity', `Expect identity accept-encoding if range header is ${JSON.stringify(rangeHeader)}`); + } +}, `Fetch with range header will be sent with Accept-Encoding: identity`); diff --git a/test/fixtures/wpt/fetch/range/general.window.js b/test/fixtures/wpt/fetch/range/general.window.js new file mode 100644 index 00000000000000..afe80d63a6b263 --- /dev/null +++ b/test/fixtures/wpt/fetch/range/general.window.js @@ -0,0 +1,29 @@ +// META: script=resources/utils.js +// META: script=/common/utils.js + +const onload = new Promise(r => window.addEventListener('load', r)); + +// It's weird that browsers do this, but it should continue to work. +promise_test(async t => { + await loadScript('resources/partial-script.py?pretend-offset=90000'); + assert_true(self.scriptExecuted); +}, `Script executed from partial response`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + + // The testing framework waits for window onload. If the audio element + // is appended before onload, it extends it, and the test times out. + await onload; + + const audio = appendAudio(document, wavURL); + await new Promise(r => audio.addEventListener('progress', r)); + audio.remove(); + + const response = await fetch(stashTakeURL); + assert_equals(await response.json(), 'identity', `Expect identity accept-encoding on media request`); +}, `Fetch with range header will be sent with Accept-Encoding: identity`); diff --git a/test/fixtures/wpt/fetch/range/resources/basic.html b/test/fixtures/wpt/fetch/range/resources/basic.html new file mode 100644 index 00000000000000..0e76edd65b7baf --- /dev/null +++ b/test/fixtures/wpt/fetch/range/resources/basic.html @@ -0,0 +1 @@ + diff --git a/test/fixtures/wpt/fetch/range/resources/range-sw.js b/test/fixtures/wpt/fetch/range/resources/range-sw.js new file mode 100644 index 00000000000000..3680c0c471d3d5 --- /dev/null +++ b/test/fixtures/wpt/fetch/range/resources/range-sw.js @@ -0,0 +1,159 @@ +importScripts('/resources/testharness.js'); + +setup({ explicit_done: true }); + +function assert_range_request(request, expectedRangeHeader, name) { + assert_equals(request.headers.get('Range'), expectedRangeHeader, name); +} + +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', event => { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'range-header-filter-test': + rangeHeaderFilterTest(request); + return; + case 'range-header-passthrough-test': + rangeHeaderPassthroughTest(event); + return; + case 'store-ranged-response': + storeRangedResponse(event); + return; + case 'use-stored-ranged-response': + useStoredRangeResponse(event); + return; + case 'broadcast-accept-encoding': + broadcastAcceptEncoding(event); + return; + } +}); + +/** + * @param {Request} request + */ +function rangeHeaderFilterTest(request) { + const rangeValue = request.headers.get('Range'); + + test(() => { + assert_range_request(new Request(request), rangeValue, `Untampered`); + assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`); + assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`); + assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`); + assert_range_request(request.clone(), rangeValue, `Clone`); + }, "Range headers correctly preserved"); + + test(() => { + assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`); + assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`); + assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`); + assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`); + }, "Range headers correctly removed"); + + test(() => { + let headers; + + headers = new Request(request).headers; + headers.delete('does-not-exist'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`); + + headers = new Request(request).headers; + headers.append('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`); + + headers = new Request(request).headers; + headers.set('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully set`); + + headers = new Request(request).headers; + headers.delete('Accept'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`); + + headers = new Request(request).headers; + headers.delete('Range'); + assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`); + }, "Headers correctly filtered"); + + done(); +} + +function rangeHeaderPassthroughTest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const key = url.searchParams.get('range-received-key'); + + event.waitUntil(new Promise(resolve => { + promise_test(async () => { + await fetch(event.request); + const response = await fetch('stash-take.py?key=' + key); + assert_equals(await response.json(), 'range-header-received'); + resolve(); + }, `Include range header in network request`); + + done(); + })); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +let storedRangeResponseP; + +function storeRangedResponse(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + storedRangeResponseP = fetch(event.request); + broadcast({ id }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +function useStoredRangeResponse(event) { + event.respondWith(async function() { + const response = await storedRangeResponseP; + if (!response) throw Error("Expected stored range response"); + return response.clone(); + }()); +} + +function broadcastAcceptEncoding(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + broadcast({ + id, + acceptEncoding: request.headers.get('Accept-Encoding') + }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} diff --git a/test/fixtures/wpt/fetch/range/resources/utils.js b/test/fixtures/wpt/fetch/range/resources/utils.js new file mode 100644 index 00000000000000..16ed737f63e8ee --- /dev/null +++ b/test/fixtures/wpt/fetch/range/resources/utils.js @@ -0,0 +1,24 @@ +function loadScript(url, { doc = document }={}) { + return new Promise((resolve, reject) => { + const script = doc.createElement('script'); + script.onload = () => resolve(); + script.onerror = () => reject(Error("Script load failed")); + script.src = url; + doc.body.appendChild(script); + }) +} + +/** + * + * @param {Document} document + * @param {string|URL} url + * @returns {HTMLAudioElement} + */ +function appendAudio(document, url) { + const audio = document.createElement('audio'); + audio.muted = true; + audio.src = url; + audio.preload = true; + document.body.appendChild(audio); + return audio; +} diff --git a/test/fixtures/wpt/fetch/range/sw.https.window.js b/test/fixtures/wpt/fetch/range/sw.https.window.js new file mode 100644 index 00000000000000..76f80e9416c615 --- /dev/null +++ b/test/fixtures/wpt/fetch/range/sw.https.window.js @@ -0,0 +1,151 @@ +// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/utils.js + +const { REMOTE_HOST } = get_host_info(); +const BASE_SCOPE = 'resources/basic.html?'; + +async function cleanup() { + for (const iframe of document.querySelectorAll('.test-iframe')) { + iframe.parentNode.removeChild(iframe); + } + + for (const reg of await navigator.serviceWorker.getRegistrations()) { + await reg.unregister(); + } +} + +async function setupRegistration(t, scope) { + await cleanup(); + const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope }); + await wait_for_state(t, reg.installing, 'activated'); + return reg; +} + +function awaitMessage(obj, id) { + return new Promise(resolve => { + obj.addEventListener('message', function listener(event) { + if (event.data.id !== id) return; + obj.removeEventListener('message', listener); + resolve(event.data); + }); + }); +} + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py?action=range-header-filter-test', w.location); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderFilterTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header filter tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'range-header-passthrough-test'); + url.searchParams.set('range-received-key', token()); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderPassthroughTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header passthrough tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a cross-origin range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + url.hostname = REMOTE_HOST; + + appendAudio(w.document, url); + + await storedRangeResponse; + + // Fetching should reject + const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' }); + await promise_rejects_js(t, w.TypeError, fetchPromise); + + // Script loading should error too + const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document }); + await promise_rejects_js(t, Error, loadScriptPromise); + + await loadScriptPromise.catch(() => {}); + + assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`); +}, `Ranged response not allowed following no-cors ranged request`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + + appendAudio(w.document, url); + + await storedRangeResponse; + + // This should not throw + await w.fetch('?action=use-stored-ranged-response'); + + // This shouldn't throw either + await loadScript('?action=use-stored-ranged-response', { doc: w.document }); + + assert_true(w.scriptExecuted, `Partial response should be executed`); +}, `Non-opaque ranged response executed`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const fetchId = Math.random() + ''; + const fetchBroadcast = awaitMessage(w.navigator.serviceWorker, fetchId); + const audioId = Math.random() + ''; + const audioBroadcast = awaitMessage(w.navigator.serviceWorker, audioId); + + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'broadcast-accept-encoding'); + url.searchParams.set('id', fetchId); + + await w.fetch(url, { + headers: { Range: 'bytes=0-10' } + }); + + assert_equals((await fetchBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for fetch"); + + url.searchParams.set('id', audioId); + appendAudio(w.document, url); + + assert_equals((await audioBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for media"); +}, `Accept-Encoding should not appear in a service worker`); diff --git a/test/fixtures/wpt/fetch/redirect-navigate/302-found-post.html b/test/fixtures/wpt/fetch/redirect-navigate/302-found-post.html new file mode 100644 index 00000000000000..854cd329a8f120 --- /dev/null +++ b/test/fixtures/wpt/fetch/redirect-navigate/302-found-post.html @@ -0,0 +1,20 @@ + + +HTTP 302 Found POST Navigation Test + + + + + diff --git a/test/fixtures/wpt/fetch/redirect-navigate/preserve-fragment.html b/test/fixtures/wpt/fetch/redirect-navigate/preserve-fragment.html new file mode 100644 index 00000000000000..682539a7445f2d --- /dev/null +++ b/test/fixtures/wpt/fetch/redirect-navigate/preserve-fragment.html @@ -0,0 +1,202 @@ + + + + + Ensure fragment is kept across redirects + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/redirect-navigate/resources/destination.html b/test/fixtures/wpt/fetch/redirect-navigate/resources/destination.html new file mode 100644 index 00000000000000..f98c5a8cd77717 --- /dev/null +++ b/test/fixtures/wpt/fetch/redirect-navigate/resources/destination.html @@ -0,0 +1,28 @@ + + + + + + + + +

Target

+

Target

+ + diff --git a/test/fixtures/wpt/fetch/redirects/data.window.js b/test/fixtures/wpt/fetch/redirects/data.window.js new file mode 100644 index 00000000000000..eeb41966b447cd --- /dev/null +++ b/test/fixtures/wpt/fetch/redirects/data.window.js @@ -0,0 +1,25 @@ +// See ../api/redirect/redirect-to-dataurl.any.js for fetch() tests + +async_test(t => { + const img = document.createElement("img"); + img.onload = t.unreached_func(); + img.onerror = t.step_func_done(); + img.src = "../api/resources/redirect.py?location=data:image/png%3Bbase64,iVBORw0KGgoAAAANSUhEUgAAAIUAAABqCAIAAAAdqgU8AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVHhe7dNBDQAADIPA%2Bje92eBxSQUQSLedlQzo0TLQonFWPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceJQMPIOzeGc0PIDEAAAAASUVORK5CYII"; +}, " fetch that redirects to data: URL"); + +globalThis.globalTest = null; +async_test(t => { + globalThis.globalTest = t; + const script = document.createElement("script"); + script.src = "../api/resources/redirect.py?location=data:text/javascript,(globalThis.globalTest.unreached_func())()"; + script.onerror = t.step_func_done(); + document.body.append(script); +}, " + + + diff --git a/test/fixtures/wpt/fetch/security/dangling-markup-mitigation.tentative.html b/test/fixtures/wpt/fetch/security/dangling-markup-mitigation.tentative.html new file mode 100644 index 00000000000000..61a931608ba5f3 --- /dev/null +++ b/test/fixtures/wpt/fetch/security/dangling-markup-mitigation.tentative.html @@ -0,0 +1,147 @@ + + + + + diff --git a/test/fixtures/wpt/fetch/security/embedded-credentials.tentative.sub.html b/test/fixtures/wpt/fetch/security/embedded-credentials.tentative.sub.html new file mode 100644 index 00000000000000..ca5ee1c87bd7c6 --- /dev/null +++ b/test/fixtures/wpt/fetch/security/embedded-credentials.tentative.sub.html @@ -0,0 +1,89 @@ + + + + + diff --git a/test/fixtures/wpt/fetch/security/redirect-to-url-with-credentials.https.html b/test/fixtures/wpt/fetch/security/redirect-to-url-with-credentials.https.html new file mode 100644 index 00000000000000..b06464805c2b0e --- /dev/null +++ b/test/fixtures/wpt/fetch/security/redirect-to-url-with-credentials.https.html @@ -0,0 +1,68 @@ + +
+ + + +
+ + + + diff --git a/test/fixtures/wpt/fetch/security/support/embedded-credential-window.sub.html b/test/fixtures/wpt/fetch/security/support/embedded-credential-window.sub.html new file mode 100644 index 00000000000000..20d307e9188405 --- /dev/null +++ b/test/fixtures/wpt/fetch/security/support/embedded-credential-window.sub.html @@ -0,0 +1,19 @@ + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/fetch-sw.https.html b/test/fixtures/wpt/fetch/stale-while-revalidate/fetch-sw.https.html new file mode 100644 index 00000000000000..efcebc24a63f40 --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/fetch-sw.https.html @@ -0,0 +1,65 @@ + + + + + Stale Revalidation Requests don't get sent to service worker + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/fetch.any.js b/test/fixtures/wpt/fetch/stale-while-revalidate/fetch.any.js new file mode 100644 index 00000000000000..3682b9d2c3176e --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/fetch.any.js @@ -0,0 +1,32 @@ +// META: global=window,worker +// META: title=Tests Stale While Revalidate is executed for fetch API +// META: script=/common/utils.js + +function wait25ms(test) { + return new Promise(resolve => { + test.step_timeout(() => { + resolve(); + }, 25); + }); +} + +promise_test(async (test) => { + var request_token = token(); + + const response = await fetch(`resources/stale-script.py?token=` + request_token); + // Wait until resource is completely fetched to allow caching before next fetch. + const body = await response.text(); + const response2 = await fetch(`resources/stale-script.py?token=` + request_token); + + assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id')); + const body2 = await response2.text(); + assert_equals(body, body2); + + while(true) { + const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token); + if (revalidation_check.headers.get('Count') == '2') { + break; + } + await wait25ms(test); + } +}, 'Second fetch returns same response'); diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html b/test/fixtures/wpt/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html new file mode 100644 index 00000000000000..9f31ef759a3ace --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html @@ -0,0 +1,67 @@ + + +Test revalidations requests aren't blocked by CSP. + + + + + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/stale-css.html b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-css.html new file mode 100644 index 00000000000000..603a60c8bbadde --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-css.html @@ -0,0 +1,51 @@ + + +Tests Stale While Revalidate works for css + + + + + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/stale-image.html b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-image.html new file mode 100644 index 00000000000000..d86bdfbde2cf84 --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-image.html @@ -0,0 +1,55 @@ + + +Tests Stale While Revalidate works for images + + + + + + + + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/stale-script.html b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-script.html new file mode 100644 index 00000000000000..f5317482c488bc --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/stale-script.html @@ -0,0 +1,59 @@ + + +Tests Stale While Revalidate works for scripts + + + + + + diff --git a/test/fixtures/wpt/fetch/stale-while-revalidate/sw-intercept.js b/test/fixtures/wpt/fetch/stale-while-revalidate/sw-intercept.js new file mode 100644 index 00000000000000..dca7de51b0b8c5 --- /dev/null +++ b/test/fixtures/wpt/fetch/stale-while-revalidate/sw-intercept.js @@ -0,0 +1,14 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +self.addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', event => { + self.clients.claim(); +}); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 31d7041697db26..1c144d9d10e38c 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -42,5 +42,9 @@ "FileAPI": { "commit": "d9d921b8f9235e0d2ec92672040c0ccfc8262e21", "path": "FileAPI" + }, + "fetch": { + "commit": "5c46bbe8d09ff44e4f9186f137f28a3c5574a483", + "path": "fetch" } } \ No newline at end of file diff --git a/test/wpt/status/fetch/api/headers.json b/test/wpt/status/fetch/api/headers.json new file mode 100644 index 00000000000000..0967ef424bce67 --- /dev/null +++ b/test/wpt/status/fetch/api/headers.json @@ -0,0 +1 @@ +{} diff --git a/test/wpt/test-fetch-headers.js b/test/wpt/test-fetch-headers.js new file mode 100644 index 00000000000000..6231a2d77dfded --- /dev/null +++ b/test/wpt/test-fetch-headers.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); +const { WPTRunner } = require('../common/wpt'); + +const runner = new WPTRunner('fetch/api/headers'); + +runner.setFlags(['--expose-internals']); + +runner.setInitScript(` + const { Headers } = require('internal/fetch/headers'); + global.Headers = Headers; +`); + +runner.runJsTests(); From a6911ad809c6c01ef26942384914facb74dfbc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Sat, 13 Feb 2021 16:14:27 +0100 Subject: [PATCH 2/3] fixup! lib: implement fetch's Headers class --- lib/http.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http.js b/lib/http.js index ee360a852ba4f4..491162f9c4a172 100644 --- a/lib/http.js +++ b/lib/http.js @@ -72,7 +72,7 @@ module.exports = { validateHeaderName, validateHeaderValue, get, - request, + request }; ObjectDefineProperty(module.exports, 'maxHeaderSize', { From 4ba1b934531da3e40669f7947d14491efbe37298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Tue, 16 Feb 2021 17:49:03 +0100 Subject: [PATCH 3/3] fixup! lib: implement fetch's Headers class remove private fields --- lib/internal/fetch/headers.js | 177 +++++++++++++++++----------------- 1 file changed, 88 insertions(+), 89 deletions(-) diff --git a/lib/internal/fetch/headers.js b/lib/internal/fetch/headers.js index eee41b808de3d2..abf9121de7982e 100644 --- a/lib/internal/fetch/headers.js +++ b/lib/internal/fetch/headers.js @@ -251,126 +251,61 @@ class HeaderList { } // https://fetch.spec.whatwg.org/#headers-class +const kHeaderList = Symbol('headerList'); +const kGuard = Symbol('guard'); class Headers { - #headerList; - #guard; - - // https://fetch.spec.whatwg.org/#concept-headers-append - #append(name, value) { - name = normalizeName(name); - value = normalizeValue(value); - - const lowerName = StringPrototypeToLowerCase(name); - - if (this.#guard === 'immutable') { - throw new TypeError('this Headers object is immutable'); - } else if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { - return; - } else if (this.#guard === 'request-no-cors') { - let temporaryValue = this.#headerList.get(name); - if (temporaryValue === null) { - temporaryValue = value; - } else { - temporaryValue = `${temporaryValue}, ${value}`; - } - if (!isNoCORSSafelistedRequestHeader(name, value)) { - return; - } - } else if (this.#guard === 'response' && forbiddenResponseHeaderNames.has(name)) { - return; - } - - this.#headerList.append(name, value); - - if (this.#guard === 'request-no-cors') { - this.#removePrivilegedNoCORSRequestHeaders(); - } - } - - // https://fetch.spec.whatwg.org/#concept-headers-fill - #fill(init) { - if (typeof init !== 'object' || init === null) { - throw new ERR_INVALID_ARG_TYPE('init', 'object', init); - } - if (typeof init[SymbolIterator] === 'function') { - for (const header of init) { - if (header === null || - typeof header[SymbolIterator] !== 'function' || - typeof header === 'string') { - throw new ERR_INVALID_ARG_TYPE('init.header', 'Iterable', header); - } - const pair = ArrayFrom(header); - if (pair.length !== 2) { - throw new ERR_INVALID_ARG_TYPE('init.header', 'of length two', pair); - } - this.#append(header[0], header[1]); - } - } else { - ArrayPrototypeForEach(ObjectEntries(init), (header) => { - this.#append(header[0], header[1]); - }) - } - } - - // https://fetch.spec.whatwg.org/#concept-headers-remove-privileged-no-cors-request-headers - #removePrivilegedNoCORSRequestHeaders() { - for (const headerName of privilegedNoCORSRequestHeaderNames) { - this.#headerList.delete(headerName); - } - } - // https://fetch.spec.whatwg.org/#dom-headers constructor(init) { - this.#headerList = new HeaderList(); - this.#guard = 'none'; + this[kHeaderList] = new HeaderList(); + this[kGuard] = 'none'; if (init !== undefined){ - this.#fill(init); + fillHeaders(this, init); } } // https://fetch.spec.whatwg.org/#dom-headers-append append(name, value) { - this.#append(name, value); + appendHeader(this, name, value); } // https://fetch.spec.whatwg.org/#dom-headers-delete delete(name) { name = normalizeName(name); const lowerName = StringPrototypeToLowerCase(name); - if (this.#guard === 'immutable') { + if (this[kGuard] === 'immutable') { throw new TypeError('this Headers object is immutable'); } - if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { + if (this[kGuard] === 'request' && forbiddenHeaderNames.has(lowerName)) { return; } - if (this.#guard === 'request-no-cors' && + if (this[kGuard] === 'request-no-cors' && !noCORSSafelistedRequestHeaderNames.has(lowerName) && !privilegedNoCORSRequestHeaderNames.has(lowerName)) { return; } - if (this.#guard === 'response' && + if (this[kGuard] === 'response' && forbiddenResponseHeaderNames.has(lowerName)) { return; } - if (!this.#headerList.contains(lowerName)) { + if (!this[kHeaderList].contains(lowerName)) { return; } - this.#headerList.delete(lowerName); - if (this.#guard === 'request-no-cors') { - this.#removePrivilegedNoCORSRequestHeaders(); + this[kHeaderList].delete(lowerName); + if (this[kGuard] === 'request-no-cors') { + removePrivilegedNoCORSRequestHeaders(this); } } // https://fetch.spec.whatwg.org/#dom-headers-get get(name) { name = normalizeName(name); - return this.#headerList.get(name); + return this[kHeaderList].get(name); } // https://fetch.spec.whatwg.org/#dom-headers-has has(name) { name = normalizeName(name); - return this.#headerList.contains(name); + return this[kHeaderList].contains(name); } // https://fetch.spec.whatwg.org/#dom-headers-set @@ -378,28 +313,28 @@ class Headers { name = normalizeName(name); value = normalizeValue(value); const lowerName = StringPrototypeToLowerCase(name); - if (this.#guard === 'immutable') { + if (this[kGuard] === 'immutable') { throw new TypeError('this Headers object is immutable'); } - if (this.#guard === 'request' && forbiddenHeaderNames.has(lowerName)) { + if (this[kGuard] === 'request' && forbiddenHeaderNames.has(lowerName)) { return; } - if (this.#guard === 'request-no-cors' && + if (this[kGuard] === 'request-no-cors' && !noCORSSafelistedRequestHeaderNames.has(lowerName)) { return; } - if (this.#guard === 'response' && + if (this[kGuard] === 'response' && forbiddenResponseHeaderNames.has(lowerName)) { return; } - this.#headerList.set(name, value); - if (this.#guard === 'request-no-cors') { - this.#removePrivilegedNoCORSRequestHeaders(); + this[kHeaderList].set(name, value); + if (this[kGuard] === 'request-no-cors') { + removePrivilegedNoCORSRequestHeaders(this); } } *entries() { - const headers = this.#headerList.sortAndCombine(); + const headers = this[kHeaderList].sortAndCombine(); yield* headers; } @@ -427,6 +362,70 @@ class Headers { } } +// https://fetch.spec.whatwg.org/#concept-headers-append +function appendHeader(headers, name, value) { + name = normalizeName(name); + value = normalizeValue(value); + + const lowerName = StringPrototypeToLowerCase(name); + + if (headers[kGuard] === 'immutable') { + throw new TypeError('this Headers object is immutable'); + } else if (headers[kGuard] === 'request' && forbiddenHeaderNames.has(lowerName)) { + return; + } else if (headers[kGuard] === 'request-no-cors') { + let temporaryValue = headers[kHeaderList].get(name); + if (temporaryValue === null) { + temporaryValue = value; + } else { + temporaryValue = `${temporaryValue}, ${value}`; + } + if (!isNoCORSSafelistedRequestHeader(name, value)) { + return; + } + } else if (headers[kGuard] === 'response' && forbiddenResponseHeaderNames.has(name)) { + return; + } + + headers[kHeaderList].append(name, value); + + if (headers[kGuard] === 'request-no-cors') { + removePrivilegedNoCORSRequestHeaders(this); + } +} + +// https://fetch.spec.whatwg.org/#concept-headers-fill +function fillHeaders(headers, init) { + if (typeof init !== 'object' || init === null) { + throw new ERR_INVALID_ARG_TYPE('init', 'object', init); + } + if (typeof init[SymbolIterator] === 'function') { + for (const header of init) { + if (header === null || + typeof header[SymbolIterator] !== 'function' || + typeof header === 'string') { + throw new ERR_INVALID_ARG_TYPE('init.header', 'Iterable', header); + } + const pair = ArrayFrom(header); + if (pair.length !== 2) { + throw new ERR_INVALID_ARG_TYPE('init.header', 'of length two', pair); + } + appendHeader(headers, header[0], header[1]); + } + } else { + ArrayPrototypeForEach(ObjectEntries(init), (header) => { + appendHeader(headers, header[0], header[1]); + }) + } +} + +// https://fetch.spec.whatwg.org/#concept-headers-remove-privileged-no-cors-request-headers +function removePrivilegedNoCORSRequestHeaders(headers) { + for (const headerName of privilegedNoCORSRequestHeaderNames) { + headers[kHeaderList].delete(headerName); + } +} + function normalizeName(name) { name = `${name}`; if (!/^[!#$%&'*+\-.^_`|~0-9a-z]+$/i.test(name)) {