From dc50897b7685af9b273e64951fa8b54b4e1a777f Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Mon, 15 Jun 2026 22:09:33 +0300 Subject: [PATCH 1/2] test: cover redirect userinfo stripping for an IPv6 literal host The redirect step rebuilds a Location URL textually when dropping server-supplied userinfo, appending URI.getHost() verbatim. For an IPv6 literal authority that host is the bracketed form (e.g. [2001:db8::1]), so the rebuild has to carry the brackets through unchanged. That path was previously unexercised: the existing userinfo tests only covered a regular hostname and a percent-encoded path/query. Add a test that follows a redirect to a Location with an IPv6 literal authority carrying userinfo and asserts the reissued request drops the userinfo while preserving the bracketed host, port, path, and query byte-exact. --- .../http/pipeline/steps/RedirectStepTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt index de4260bd..31b1a58a 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt @@ -964,6 +964,39 @@ class RedirectStepTest { assertTrue(target.contains("x=%26"), "encoded query value %26 must be preserved: $target") } + @Test + fun `stripping userinfo preserves an IPv6 literal host with brackets, port, path, and query`() { + // An IPv6 literal authority carries its host inside square brackets in the URI + // (URI.getHost() returns "[2001:db8::1]"), and the userinfo-stripping rebuild appends + // that bracketed host verbatim. Clearing the userinfo must leave the IPv6 host, its + // port, the path, and the query byte-exact — the brackets in particular must survive. + val fake = + FakeHttpClient() + .enqueue { + status(302).header( + "Location", + "https://user:pass@[2001:db8::1]:8443/v2/resource?q=1", + ) + }.enqueue { status(200) } + + val pipeline = + HttpPipelineBuilder(fake) + .append(DefaultRedirectStep()) + .build() + + pipeline.send(getRequest("https://api.example.com/v1")) + + val reissued = fake.requests[1] + assertNull(reissued.url.userInfo, "userinfo must be stripped from an IPv6 Location") + // The bracketed IPv6 literal host and port are preserved exactly. + assertEquals("[2001:db8::1]", reissued.url.host, "IPv6 literal host (with brackets) must be preserved") + assertEquals(8443, reissued.url.port, "port must be preserved") + assertEquals("/v2/resource", reissued.url.path, "path must be preserved") + assertEquals("q=1", reissued.url.query, "query must be preserved") + // The reissued target is byte-exact apart from the dropped userinfo. + assertEquals("https://[2001:db8::1]:8443/v2/resource?q=1", reissued.url.toString()) + } + // ----------------- Other non-3xx status codes don't trigger redirect ----------------- @Test From 8d659a9bc43bb81ca2a85fa3a1cf1dcf356b8bd5 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 16 Jun 2026 22:22:42 +0300 Subject: [PATCH 2/2] test: assert IPv6 redirect is followed and cover the no-userinfo path Capture the reissued response in the IPv6 userinfo-stripping test and assert the redirect was followed exactly once (status 200, two calls) so a regression that skips the reissue fails with a clear message instead of an IndexOutOfBoundsException on fake.requests[1]. Add a sibling test for an IPv6 literal Location with no userinfo, exercising the pass-through (no-rebuild) branch and confirming the bracketed host, port, path, and query survive there too. --- .../http/pipeline/steps/RedirectStepTest.kt | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt index 31b1a58a..3543e252 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/pipeline/steps/RedirectStepTest.kt @@ -984,7 +984,12 @@ class RedirectStepTest { .append(DefaultRedirectStep()) .build() - pipeline.send(getRequest("https://api.example.com/v1")) + val response = pipeline.send(getRequest("https://api.example.com/v1")) + + // Pin that the redirect was actually followed exactly once, so a regression that skips + // the reissue fails on these assertions rather than an IndexOutOfBoundsException below. + assertEquals(200, response.status.code) + assertEquals(2, fake.callCount) val reissued = fake.requests[1] assertNull(reissued.url.userInfo, "userinfo must be stripped from an IPv6 Location") @@ -997,6 +1002,39 @@ class RedirectStepTest { assertEquals("https://[2001:db8::1]:8443/v2/resource?q=1", reissued.url.toString()) } + @Test + fun `IPv6 literal host without userinfo passes through with brackets preserved`() { + // The early-return branch (no userinfo to strip) hands the resolved IPv6 URI through + // unchanged via toURL(); confirm the bracketed host, port, path, and query survive on + // that non-rebuilding path too. + val fake = + FakeHttpClient() + .enqueue { + status(302).header( + "Location", + "https://[2001:db8::1]:8443/v2/resource?q=1", + ) + }.enqueue { status(200) } + + val pipeline = + HttpPipelineBuilder(fake) + .append(DefaultRedirectStep()) + .build() + + val response = pipeline.send(getRequest("https://api.example.com/v1")) + + assertEquals(200, response.status.code) + assertEquals(2, fake.callCount) + + val reissued = fake.requests[1] + assertNull(reissued.url.userInfo, "no userinfo was present") + assertEquals("[2001:db8::1]", reissued.url.host, "IPv6 literal host (with brackets) must be preserved") + assertEquals(8443, reissued.url.port, "port must be preserved") + assertEquals("/v2/resource", reissued.url.path, "path must be preserved") + assertEquals("q=1", reissued.url.query, "query must be preserved") + assertEquals("https://[2001:db8::1]:8443/v2/resource?q=1", reissued.url.toString()) + } + // ----------------- Other non-3xx status codes don't trigger redirect ----------------- @Test