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.
2323package com.microsoft.identity.common.java.nativeauth.providers.interactors
2424
2525import com.microsoft.identity.common.java.nativeauth.providers.NativeAuthRequestInterceptor
@@ -30,91 +30,197 @@ import org.junit.Assert.assertTrue
3030import org.junit.Test
3131import 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+ */
3341class 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