Skip to content

Commit 4dd8406

Browse files
spetrescu84Copilot
andcommitted
Consolidate duplicated test logic into RequestInterceptorHeaderUtilsTest
Move shared merge logic tests (reserved filtering, case-insensitive merge, overwrite protection, URL passthrough, null/empty interceptor) out of individual interactor tests into RequestInterceptorHeaderUtilsTest. Each interactor test now only verifies wiring: that the interceptor is called for each public method and that null interceptor passes through unchanged. This eliminates ~370 lines of duplicated test code. Test distribution: - RequestInterceptorHeaderUtilsTest: 9 tests (shared merge contract) - NativeAuthHeaderValidatorTest: 12 tests (validation rules) - 4 interactor tests: 24 tests total (per-method wiring) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ce8c4e7 commit 4dd8406

5 files changed

Lines changed: 236 additions & 610 deletions

File tree

common4j/src/test/com/microsoft/identity/common/java/nativeauth/providers/interactors/JITInteractorRequestInterceptorTest.kt

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ import org.junit.Test
4444
import java.net.URL
4545

4646
/**
47-
* Tests verifying that [JITInteractor] correctly applies custom headers
48-
* from a [NativeAuthRequestInterceptor] to outgoing HTTP requests.
47+
* Tests verifying that [JITInteractor] correctly wires the request interceptor
48+
* to each public method. Merge logic, filtering, and edge cases are covered by
49+
* [RequestInterceptorHeaderUtilsTest] and [com.microsoft.identity.common.java.nativeauth.providers.NativeAuthHeaderValidatorTest].
4950
*/
5051
class JITInteractorRequestInterceptorTest {
5152

@@ -63,10 +64,7 @@ class JITInteractorRequestInterceptorTest {
6364

6465
private val testInterceptor = object : NativeAuthRequestInterceptor {
6566
override fun additionalHeaders(requestUrl: URL): Map<String, String>? {
66-
return mapOf(
67-
"x-akamai-sensor" to "sensor-data-123",
68-
"x-fraud-signal" to "signal-abc"
69-
)
67+
return mapOf("x-akamai-sensor" to "sensor-data-123")
7068
}
7169
}
7270

@@ -136,7 +134,6 @@ class JITInteractorRequestInterceptorTest {
136134

137135
assertTrue(capturedHeaders.isCaptured)
138136
assertEquals("sensor-data-123", capturedHeaders.captured["x-akamai-sensor"])
139-
assertEquals("signal-abc", capturedHeaders.captured["x-fraud-signal"])
140137
assertEquals("MSAL.Android", capturedHeaders.captured["x-client-SKU"])
141138
}
142139

@@ -196,42 +193,4 @@ class JITInteractorRequestInterceptorTest {
196193
assertEquals("sensor-data-123", capturedHeaders.captured["x-akamai-sensor"])
197194
}
198195
// endregion
199-
200-
// region reserved header filtering
201-
@Test
202-
fun testInterceptorReservedHeadersAreFilteredInJIT() {
203-
val filteringInterceptor = object : NativeAuthRequestInterceptor {
204-
override fun additionalHeaders(requestUrl: URL): Map<String, String>? {
205-
return mapOf(
206-
"x-akamai-sensor" to "valid",
207-
"x-ms-evil" to "should-be-filtered",
208-
"x-client-override" to "should-be-filtered",
209-
"x-app-secret" to "should-be-filtered",
210-
"x-broker-bypass" to "should-be-filtered",
211-
"Authorization" to "should-be-filtered"
212-
)
213-
}
214-
}
215-
216-
val mockRequest = mockk<JITIntrospectRequest>(relaxed = true)
217-
every { mockRequest.requestUrl } returns testUrl
218-
every { mockRequest.headers } returns baseHeaders
219-
every { mockRequestProvider.createJITIntrospectRequest(any(), any()) } returns mockRequest
220-
every { mockResponseHandler.getJITIntrospectApiResponseFromHttpResponse(any(), any()) } returns mockk(relaxed = true)
221-
222-
val capturedHeaders = setupHttpClientCapture()
223-
val interactor = createInteractor(interceptor = filteringInterceptor)
224-
225-
interactor.performIntrospect(createJITIntrospectParams())
226-
227-
assertTrue(capturedHeaders.isCaptured)
228-
val headers = capturedHeaders.captured
229-
assertTrue(headers.containsKey("x-akamai-sensor"))
230-
assertFalse("x-ms- prefix should be filtered", headers.containsKey("x-ms-evil"))
231-
assertFalse("x-client- prefix should be filtered", headers.containsKey("x-client-override"))
232-
assertFalse("x-app- prefix should be filtered", headers.containsKey("x-app-secret"))
233-
assertFalse("x-broker- prefix should be filtered", headers.containsKey("x-broker-bypass"))
234-
assertFalse("Non x- prefix should be filtered", headers.containsKey("Authorization"))
235-
}
236-
// endregion
237196
}
Lines changed: 164 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// All rights reserved.
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
33
//
4-
// This code is licensed under the MIT License.
4+
// This code is licensed under the MIT License.
55
//
6-
// Permission is hereby granted, free of charge, to any person obtaining a copy
7-
// of this software and associated documentation files(the "Software"), to deal
8-
// in the Software without restriction, including without limitation the rights
9-
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10-
// copies of the Software, and to permit persons to whom the Software is
11-
// furnished to do so, subject to the following conditions :
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
1212
//
13-
// The above copyright notice and this permission notice shall be included in
14-
// all copies or substantial portions of the Software.
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
1515
//
16-
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17-
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18-
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19-
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20-
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21-
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22-
// THE SOFTWARE.
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
2323
package com.microsoft.identity.common.java.nativeauth.providers.interactors
2424

2525
import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthRequestInterceptor
@@ -30,91 +30,197 @@ import org.junit.Assert.assertTrue
3030
import org.junit.Test
3131
import java.net.URL
3232

33+
/**
34+
* Unit tests for [applyInterceptorHeaders], the shared helper that merges
35+
* interceptor-provided custom headers into base request headers.
36+
*
37+
* These tests cover the helper's contract directly, so interactor-level tests
38+
* only need to verify that each interactor method passes headers through
39+
* to the HTTP client (i.e., the wiring, not the merge logic).
40+
*/
3341
class RequestInterceptorHeaderUtilsTest {
3442

35-
private val requestUrl = URL("https://contoso.ciamlogin.com/oauth2/v2.0/initiate")
43+
private val testUrl = URL("https://contoso.ciamlogin.com/oauth2/v2.0/initiate")
44+
45+
private val baseHeaders = mapOf<String, String?>(
46+
"Content-Type" to "application/x-www-form-urlencoded",
47+
"x-client-SKU" to "MSAL.Android",
48+
"Accept" to "application/json"
49+
)
50+
51+
// region null / empty interceptor scenarios
3652

3753
@Test
38-
fun testApplyInterceptorHeadersReturnsOriginalMapWhenInterceptorIsNull() {
39-
val headers = mapOf("Content-Type" to "application/x-www-form-urlencoded")
54+
fun testNullInterceptorReturnsSameHeaders() {
55+
val result = applyInterceptorHeaders(testUrl, baseHeaders, null)
56+
assertSame("Null interceptor should return the exact same map instance", baseHeaders, result)
57+
}
4058

41-
val result = applyInterceptorHeaders(requestUrl, headers, null)
59+
@Test
60+
fun testInterceptorReturningNullReturnsSameHeaders() {
61+
val interceptor = object : NativeAuthRequestInterceptor {
62+
override fun additionalHeaders(requestUrl: URL): Map<String, String>? = null
63+
}
64+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
65+
assertSame("Interceptor returning null should return the exact same map instance", baseHeaders, result)
66+
}
4267

43-
assertSame(headers, result)
68+
@Test
69+
fun testInterceptorReturningEmptyMapReturnsSameHeaders() {
70+
val interceptor = object : NativeAuthRequestInterceptor {
71+
override fun additionalHeaders(requestUrl: URL): Map<String, String>? = emptyMap()
72+
}
73+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
74+
assertSame("Interceptor returning empty map should return the exact same map instance", baseHeaders, result)
4475
}
4576

77+
// endregion
78+
79+
// region valid header merge
80+
4681
@Test
47-
fun testApplyInterceptorHeadersMergesValidCustomHeaders() {
48-
val headers = mapOf("Content-Type" to "application/x-www-form-urlencoded")
82+
fun testValidCustomHeadersAreMergedWithBaseHeaders() {
4983
val interceptor = object : NativeAuthRequestInterceptor {
5084
override fun additionalHeaders(requestUrl: URL): Map<String, String> {
51-
return mapOf("x-test-header" to "value")
85+
return mapOf(
86+
"x-akamai-sensor" to "sensor-data-123",
87+
"x-fraud-signal" to "signal-abc"
88+
)
5289
}
5390
}
5491

55-
val result = applyInterceptorHeaders(requestUrl, headers, interceptor)
92+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
5693

57-
assertEquals(2, result.size)
94+
assertEquals(5, result.size)
95+
assertEquals("sensor-data-123", result["x-akamai-sensor"])
96+
assertEquals("signal-abc", result["x-fraud-signal"])
5897
assertEquals("application/x-www-form-urlencoded", result["Content-Type"])
59-
assertEquals("value", result["x-test-header"])
98+
assertEquals("MSAL.Android", result["x-client-SKU"])
99+
assertEquals("application/json", result["Accept"])
60100
}
61101

102+
// endregion
103+
104+
// region reserved header filtering (integration with NativeAuthHeaderValidator)
105+
62106
@Test
63-
fun testApplyInterceptorHeadersFiltersReservedAndNonCustomHeaders() {
64-
val headers = mapOf("Content-Type" to "application/x-www-form-urlencoded")
107+
fun testReservedPrefixHeadersAreFiltered() {
65108
val interceptor = object : NativeAuthRequestInterceptor {
66109
override fun additionalHeaders(requestUrl: URL): Map<String, String> {
67110
return mapOf(
68-
"x-ms-client-request-id" to "reserved",
69-
"Authorization" to "rejected",
70-
"x-valid" to "kept"
111+
"x-akamai-sensor" to "valid",
112+
"x-ms-evil" to "should-be-filtered",
113+
"x-client-override" to "should-be-filtered",
114+
"x-app-secret" to "should-be-filtered",
115+
"x-broker-bypass" to "should-be-filtered",
116+
"Authorization" to "should-be-filtered"
71117
)
72118
}
73119
}
74120

75-
val result = applyInterceptorHeaders(requestUrl, headers, interceptor)
121+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
76122

77-
assertEquals(2, result.size)
78-
assertTrue(result.containsKey("x-valid"))
79-
assertFalse(result.containsKey("Authorization"))
80-
assertFalse(result.containsKey("x-ms-client-request-id"))
123+
assertTrue(result.containsKey("x-akamai-sensor"))
124+
assertFalse("x-ms- prefix should be filtered", result.containsKey("x-ms-evil"))
125+
assertFalse("x-client- prefix should be filtered", result.containsKey("x-client-override"))
126+
assertFalse("x-app- prefix should be filtered", result.containsKey("x-app-secret"))
127+
assertFalse("x-broker- prefix should be filtered", result.containsKey("x-broker-bypass"))
128+
assertFalse("Non x- prefix should be filtered", result.containsKey("authorization"))
81129
}
82130

83131
@Test
84-
fun testApplyInterceptorHeadersMergesCaseInsensitiveWithBaseHeaders() {
85-
val headers = mapOf(
86-
"X-Custom-Header" to "base",
87-
"Content-Type" to "application/x-www-form-urlencoded"
132+
fun testInterceptorCannotOverwriteReservedBaseHeaders() {
133+
val interceptor = object : NativeAuthRequestInterceptor {
134+
override fun additionalHeaders(requestUrl: URL): Map<String, String> {
135+
return mapOf(
136+
"x-client-SKU" to "Evil.SDK",
137+
"x-ms-request-id" to "fake-id",
138+
"x-akamai-sensor" to "valid-data"
139+
)
140+
}
141+
}
142+
143+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
144+
145+
// Reserved prefix headers from interceptor should be filtered, preserving base values
146+
assertEquals("MSAL.Android", result["x-client-SKU"])
147+
assertFalse("x-ms- prefix should be filtered", result.containsKey("x-ms-request-id"))
148+
// Valid custom header should be merged
149+
assertEquals("valid-data", result["x-akamai-sensor"])
150+
}
151+
152+
@Test
153+
fun testAllInvalidHeadersReturnsSameBaseSize() {
154+
val interceptor = object : NativeAuthRequestInterceptor {
155+
override fun additionalHeaders(requestUrl: URL): Map<String, String> {
156+
return mapOf(
157+
"x-ms-evil" to "filtered",
158+
"Authorization" to "filtered",
159+
"Content-Type" to "filtered"
160+
)
161+
}
162+
}
163+
164+
val result = applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
165+
166+
assertEquals(baseHeaders.size, result.size)
167+
assertEquals("application/x-www-form-urlencoded", result["Content-Type"])
168+
assertEquals("MSAL.Android", result["x-client-SKU"])
169+
assertEquals("application/json", result["Accept"])
170+
}
171+
172+
// endregion
173+
174+
// region case-insensitive merge
175+
176+
@Test
177+
fun testCaseInsensitiveHeaderMerge() {
178+
val baseHeadersWithCustom = mapOf<String, String?>(
179+
"Content-Type" to "application/x-www-form-urlencoded",
180+
"x-client-SKU" to "MSAL.Android",
181+
"x-existing-custom" to "old-value"
88182
)
183+
89184
val interceptor = object : NativeAuthRequestInterceptor {
90185
override fun additionalHeaders(requestUrl: URL): Map<String, String> {
91-
return mapOf("x-custom-header" to "override")
186+
return mapOf(
187+
"X-Existing-Custom" to "new-value",
188+
"x-new-header" to "new-data"
189+
)
92190
}
93191
}
94192

95-
val result = applyInterceptorHeaders(requestUrl, headers, interceptor)
193+
val result = applyInterceptorHeaders(testUrl, baseHeadersWithCustom, interceptor)
96194

97-
assertEquals(2, result.size)
98-
assertEquals("override", result["x-custom-header"])
99-
assertFalse(result.containsKey("X-Custom-Header"))
195+
// Original casing key should be replaced by the normalized (lowercase) key from validator
196+
assertFalse(
197+
"Original and new casing keys should not both exist",
198+
result.containsKey("x-existing-custom") && result.containsKey("X-Existing-Custom")
199+
)
200+
assertEquals("new-value", result["x-existing-custom"])
201+
assertEquals("new-data", result["x-new-header"])
202+
assertEquals("MSAL.Android", result["x-client-SKU"])
100203
}
101204

205+
// endregion
206+
207+
// region URL passthrough
208+
102209
@Test
103-
fun testApplyInterceptorHeadersPassesRequestUrlToInterceptor() {
104-
var capturedUrl: URL? = null
210+
fun testInterceptorReceivesCorrectRequestUrl() {
211+
val capturedUrls = mutableListOf<URL>()
105212
val interceptor = object : NativeAuthRequestInterceptor {
106213
override fun additionalHeaders(requestUrl: URL): Map<String, String>? {
107-
capturedUrl = requestUrl
108-
return emptyMap()
214+
capturedUrls.add(requestUrl)
215+
return mapOf("x-test" to "value")
109216
}
110217
}
111218

112-
applyInterceptorHeaders(
113-
requestUrl = requestUrl,
114-
headers = mapOf("Content-Type" to "application/x-www-form-urlencoded"),
115-
requestInterceptor = interceptor
116-
)
219+
applyInterceptorHeaders(testUrl, baseHeaders, interceptor)
117220

118-
assertEquals(requestUrl, capturedUrl)
221+
assertEquals(1, capturedUrls.size)
222+
assertEquals(testUrl, capturedUrls[0])
119223
}
224+
225+
// endregion
120226
}

0 commit comments

Comments
 (0)