fix(tests/redirection): check HTTPS redirects properly - #495
Conversation
…oute After the retriever fix, `httpRedirects` and `httpsRedirects` can differ. Unify both fields under a single `redirects` variable (HTTP preferred, HTTPS fallback) so destination and route always describe the same chain.
Two cases were not covered: - HTTPS chain redirecting back to HTTP while the HTTP chain looked fine - Empty httpRedirects with a live HTTP response producing a spurious pass (vacuous truth on .every() + wrong chain used for pass/fail checks)
Previously `redirects` was computed as httpRedirects OR httpsRedirects (fallback), and then used for both display and all pass/fail logic. This caused three bugs: - When httpRedirects was empty but an HTTP response existed, the OR-fallback ran httpsRedirects through checks designed for an HTTP chain (e.g. route[0].protocol === "http:" is always false for an https: URL), producing a spurious pass. - allRedirectsPreloaded called .every() on an empty httpRedirects array, which vacuously returns true and gave RedirectionAllRedirectsPreloaded for sites with no HTTP redirects. - The "not to HTTPS" check only inspected the end of the HTTP chain. If the independent HTTPS session redirected back to HTTP, it went undetected. Fix: separate httpRoute (used for all pass/fail checks) from displayRoute (httpRoute with httpsRoute fallback, used only for output.destination and output.route). Guard allRedirectsPreloaded with httpRoute.length > 1. Extend the "not to HTTPS" check to also test httpsRoute.at(-1).
| // Check to see if every redirection was covered by the preload list. | ||
| // Guard httpRoute.length > 1 to avoid vacuous truth on an empty array. | ||
| const allRedirectsPreloaded = | ||
| httpRoute.length > 1 && |
There was a problem hiding this comment.
Could we add a test for the "route is 1" condition here?
There was a problem hiding this comment.
Added in 2c43bbc, using a genuinely preloaded host (cloudflare.com) as a single-entry chain and asserting RedirectionMissing. The existing "redirection missing" test used mozilla.org, which isn't preloaded, so the httpRoute.length > 1 guard was never actually exercised.
| it("fails with redirection-missing when http redirects are empty but http response exists", function () { | ||
| // The OR fallback previously caused httpsRedirects to be used for pass/fail | ||
| // logic when httpRedirects is empty, which could produce a spurious pass. | ||
| reqs.responses.httpRedirects = []; |
There was a problem hiding this comment.
Is this possible? Wouldn't a site returning a http response have at least one value in this array with a 200 status?
There was a problem hiding this comment.
Agree, the interceptor records every response (including the final 200) and responses.http is only set on success, so a non-null responses.http always implies a non-empty httpRedirects. Removed in fd0484e.
| // use the http redirect chain | ||
| retrievals.responses.httpRedirects = httpSession.redirectHistory; | ||
| retrievals.responses.httpsRedirects = httpSession.redirectHistory; | ||
| retrievals.responses.httpsRedirects = httpsSession.redirectHistory; |
There was a problem hiding this comment.
Can we add a test for this change specifically?
There was a problem hiding this comment.
Added in 38b5007: the live "test retrieve mdn" case now asserts httpsRedirects[0] starts on https:. Before the fix it reused the HTTP chain, which starts on http:.
| assert.isFalse(res.pass); | ||
| }); | ||
|
|
||
| it("uses https redirects as fallback for destination and route when http redirects are empty", function () { |
There was a problem hiding this comment.
I don't think this is possible in practice, if httpRedirects is empty, then responses.http should be undefined, and that's already covered by the first test in the file.
There was a problem hiding this comment.
Agree, httpRedirects is empty only if responses.http is null. Rewrote in fd0484e to the real no-HTTP case: responses.http = null, expects RedirectionNotNeededNoHttp, and asserts destination falls back to the HTTPS chain end (the only reachable use of httpsRedirects here).
…fallback An HTTP response is only recorded together with at least one entry in `httpRedirects`, so an empty `httpRedirects` implies `responses.http` is `null`. The `displayRoute` fallback to `httpsRedirects` was therefore dead for `route` (the `else` branch always had `httpRoute === displayRoute`); only the destination fallback for the no-HTTP case is reachable. Compute `destination` inline as `httpRoute.at(-1) ?? httpsRoute.at(-1)`, assign `route` from `httpRoute` directly, and replace the test that relied on the impossible empty-`httpRedirects`-with-HTTP-response state with one covering the real no-HTTP destination fallback.
A redirect chain of length 1 means no redirection happened. Without the `httpRoute.length > 1` guard, `[singlePreloadedHost].every(...)` returns `true` and reports `RedirectionAllRedirectsPreloaded` instead of `RedirectionMissing`. Use a genuinely preloaded host (`cloudflare.com`) so the test fails if the guard is removed.
The retriever now assigns `httpsSession.redirectHistory` to `httpsRedirects`. Assert the chain starts on `https:`; a regression that reuses the HTTP session's history would start on `http:`.
| const httpRoute = requests.responses.httpRedirects; | ||
| const httpsRoute = requests.responses.httpsRedirects; |
There was a problem hiding this comment.
@LeoMcA Should we rename http*Redirects and http*Route to avoid confusion altogether?
Description
Fixes an assignment in the
retriever, where the redirects from the HTTP session were assigned to the HTTPS redirects.Motivation
With both chains identical, the redirection test logic produced wrong results for any site where the HTTP and HTTPS redirect chains actually differ.
Additional details
Correcting
httpsRedirectsrevealed three latent bugs in the redirection tests:Wrong chain for pass/fail when
httpRedirectsis empty — thehttpRedirects OR httpsRedirectsfallback was used for both display and all pass/fail checks. With an emptyhttpRedirects, the off-host and initial-redirection checks silently evaluated against anhttps:URL forroute[0], always falling through to a pass.Vacuous truth in
allRedirectsPreloaded—[].every(...)returnstrue, so an emptyhttpRedirectscould produceRedirectionAllRedirectsPreloadedwhen the HTTP server didn't redirect at all.HTTPS chain redirecting back to HTTP went undetected — the "not to HTTPS" check only inspected the end of the HTTP chain. If the independent HTTPS session redirected back to HTTP, it passed.
All three are fixed and covered by new tests.
Related issues and pull requests