1818import static org .assertj .core .api .Assertions .assertThat ;
1919import static org .assertj .core .api .Assertions .assertThatThrownBy ;
2020
21+ import java .util .Arrays ;
22+ import java .util .Collections ;
2123import java .util .List ;
2224import java .util .Map ;
2325import java .util .stream .Stream ;
3436import software .amazon .awssdk .regions .Region ;
3537import software .amazon .awssdk .services .restjsonendpointproviders .RestJsonEndpointProvidersClient ;
3638import software .amazon .awssdk .services .restjsonendpointproviders .RestJsonEndpointProvidersClientBuilder ;
39+ import software .amazon .awssdk .utils .StringUtils ;
3740
3841/**
39- * Functional tests to verify that custom User-Agent headers provided via
40- * {@link software.amazon.awssdk.core.client.config.ClientOverrideConfiguration.Builder#putHeader(String, String)}
41- * are preserved and not overwritten by the SDK's default User-Agent generation logic.
42+ * Functional tests verifying custom User-Agent header preservation.
43+ *
44+ * <p>Tests ensure that User-Agent headers provided via
45+ * {@link software.amazon.awssdk.core.client.config.ClientOverrideConfiguration.Builder#putHeader(String, String)} are preserved
46+ * and not overwritten by SDK's default User-Agent generation logic.
4247 */
4348class CustomUserAgentHeaderTest {
4449
4550 private static final String USER_AGENT_HEADER = "User-Agent" ;
4651 private static final String SDK_USER_AGENT_PREFIX = "aws-sdk-java" ;
4752 private static final String TEST_API_NAME = "TestApiName" ;
4853 private static final String TEST_API_VERSION = "1.0" ;
54+ private static final String INTERCEPTOR_STOP_MESSAGE = "stop" ;
4955
5056 private CapturingInterceptor interceptor ;
5157
58+ private static Stream <Arguments > customUserAgentValues () {
59+ return Stream .of (
60+ Arguments .of ("CustomUserAgentHeaderValue" ),
61+ Arguments .of ("MyApplication/1.0.0" ),
62+ Arguments .of ("CustomClient/2.0 (Linux; x86_64)" )
63+ );
64+ }
65+
66+ // ========== Default Behavior Tests ==========
67+
68+ private static Stream <Arguments > customUserAgentListValues () {
69+ return Stream .of (
70+ Arguments .of (Arrays .asList ("Agent1" )),
71+ Arguments .of (Arrays .asList ("Agent1" , "Agent2" )),
72+ Arguments .of (Arrays .asList ("CustomClient/1.0" , "MyApp/2.0" ))
73+ );
74+ }
75+
76+ // ========== Custom User-Agent Preservation Tests ==========
77+
5278 @ BeforeEach
5379 void setUp () {
5480 interceptor = new CapturingInterceptor ();
5581 }
5682
5783 @ Test
58- void execute_withoutCustomUserAgent_shouldAddSdkDefaultUserAgent () {
59-
84+ void executeRequest_withoutCustomUserAgent_shouldAddSdkDefaultUserAgent () {
6085 RestJsonEndpointProvidersClient client = defaultClientBuilder ().build ();
6186 executeRequestExpectingInterception (client );
62- String userAgent = getCapturedUserAgent ();
63- assertThat (userAgent ).contains (SDK_USER_AGENT_PREFIX );
64- }
65-
66- @ Test
67- void execute_withEmptyCustomUserAgent_shouldPreserveEmptyValue () {
6887
69- RestJsonEndpointProvidersClient client = clientWithCustomUserAgent ("" );
70- executeRequestExpectingInterception (client );
71- String userAgent = getCapturedUserAgent ();
72- assertThat (userAgent ).isEmpty ();
88+ assertUserAgentContains (SDK_USER_AGENT_PREFIX );
7389 }
7490
75- @ ParameterizedTest (name = "{index}: userAgent={0}" )
76- @ MethodSource ("customUserAgentValues" )
77- void execute_withCustomUserAgent_shouldPreserveAndNotOverwrite (String customUserAgent ) {
91+ // ========== API Name Handling Tests ==========
7892
93+ @ ParameterizedTest (name = "Custom User-Agent ''{0}'' should be preserved without SDK prefix" )
94+ @ MethodSource ("customUserAgentValues" )
95+ void executeRequest_withCustomUserAgent_shouldPreserveAndNotOverwrite (String customUserAgent ) {
7996 RestJsonEndpointProvidersClient client = clientWithCustomUserAgent (customUserAgent );
8097 executeRequestExpectingInterception (client );
8198
@@ -85,33 +102,120 @@ void execute_withCustomUserAgent_shouldPreserveAndNotOverwrite(String customUser
85102 .doesNotContain (SDK_USER_AGENT_PREFIX );
86103 }
87104
88- private static Stream <Arguments > customUserAgentValues () {
89- return Stream .of (
90- Arguments .of ("CustomUserAgentHeaderValue" ),
91- Arguments .of ("MyApplication/1.0.0" ),
92- Arguments .of ("CustomClient/2.0 (Linux; x86_64)" )
93- );
105+ @ ParameterizedTest (name = "Custom User-Agent list {0} should be preserved" )
106+ @ MethodSource ("customUserAgentListValues" )
107+ void executeRequest_withCustomUserAgentList_shouldPreserveAllValues (List <String > customUserAgentList ) {
108+ RestJsonEndpointProvidersClient client = clientWithCustomUserAgentList (customUserAgentList );
109+ executeRequestExpectingInterception (client );
110+
111+ List <String > userAgentList = getCapturedUserAgentList ();
112+ assertThat (userAgentList ).isEqualTo (customUserAgentList );
94113 }
95114
96- @ Test
97- void execute_withCustomUserAgentAndApiName_shouldNotAppendApiName () {
115+ // ========== Edge Case Tests ==========
98116
117+ @ Test
118+ void executeRequest_withCustomUserAgentAndApiName_shouldNotAppendApiName () {
99119 String customUserAgent = "CustomUserAgentHeaderValue" ;
100120 RestJsonEndpointProvidersClient client = clientWithCustomUserAgent (customUserAgent );
101121 executeRequestWithApiName (client );
122+
102123 String userAgent = getCapturedUserAgent ();
103124 assertThat (userAgent )
104125 .isEqualTo (customUserAgent )
105126 .doesNotContain (TEST_API_NAME );
106127 }
107128
108129 @ Test
109- void execute_withoutCustomUserAgentAndWithApiName_shouldAppendApiName () {
110-
130+ void executeRequest_withoutCustomUserAgentAndWithApiName_shouldAppendApiName () {
111131 RestJsonEndpointProvidersClient client = defaultClientBuilder ().build ();
112132 executeRequestWithApiName (client );
113- String userAgent = getCapturedUserAgent ();
114- assertThat (userAgent ).contains (TEST_API_NAME + "/" + TEST_API_VERSION );
133+
134+ assertUserAgentContains (TEST_API_NAME + "/" + TEST_API_VERSION );
135+ }
136+
137+ /**
138+ * Verifies that null User-Agent list throws NullPointerException.
139+ *
140+ * <p>This ensures the SDK fails fast with clear error rather than allowing
141+ * invalid configuration.
142+ */
143+ @ Test
144+ void buildClient_withNullListUserAgent_shouldThrowNullPointerException () {
145+ assertThatThrownBy (() -> clientWithCustomUserAgentList (null ))
146+ .isInstanceOf (NullPointerException .class )
147+ .hasMessageContaining ("values must not be null" );
148+ }
149+
150+ /**
151+ * Verifies that empty User-Agent list results in SDK default User-Agent.
152+ *
153+ * <p><b>Behavioral Change:</b> Previously as in when UserAgentApplyStage was done before MergeCustomHeaderStage, explicitly
154+ * setting User-Agent Header to empty String or empty list would delete the SDK User-Agent. Current behavior ensures SDK
155+ * User-Agent is always present when User-Agent Header is emptyString/EmptyList/Null.
156+ */
157+ @ Test
158+ void executeRequest_withEmptyListUserAgent_shouldResultInSdkUserAgentHeader () {
159+ RestJsonEndpointProvidersClient client = clientWithCustomUserAgentList (Collections .emptyList ());
160+ executeRequestExpectingInterception (client );
161+
162+ List <String > userAgentList = getCapturedUserAgentList ();
163+ assertThat (userAgentList )
164+ .hasSize (1 )
165+ .anyMatch (ua -> ua .startsWith (SDK_USER_AGENT_PREFIX ));
166+ }
167+
168+ @ Test
169+ void executeRequest_withEmptyCustomUserAgent_shouldStoreSdkUserAgent () {
170+ RestJsonEndpointProvidersClient client = clientWithCustomUserAgent ("" );
171+ executeRequestExpectingInterception (client );
172+
173+ assertUserAgentContains (SDK_USER_AGENT_PREFIX );
174+ }
175+
176+ @ Test
177+ void executeRequest_withNullStringUserAgent_shouldStoreAsSdkUserAgent () {
178+ RestJsonEndpointProvidersClient client = clientWithCustomUserAgent (null );
179+ executeRequestExpectingInterception (client );
180+
181+ List <String > userAgentList = getCapturedUserAgentList ();
182+ assertThat (userAgentList ).hasSize (1 );
183+ assertThat (userAgentList )
184+ .hasSize (1 )
185+ .allSatisfy (ua -> {
186+ assertThat (ua ).isNotNull ();
187+ assertThat (ua ).startsWith (SDK_USER_AGENT_PREFIX );
188+ });
189+
190+ }
191+
192+ private void assertUserAgentContains (String expected ) {
193+ assertThat (getCapturedUserAgent ()).contains (expected );
194+ }
195+
196+ private void executeRequestExpectingInterception (RestJsonEndpointProvidersClient client ) {
197+ assertThatThrownBy (() -> client .allTypes (r -> {
198+ }))
199+ .hasMessageContaining (INTERCEPTOR_STOP_MESSAGE );
200+ }
201+
202+ private void executeRequestWithApiName (RestJsonEndpointProvidersClient client ) {
203+ assertThatThrownBy (() -> client .allTypes (r -> r
204+ .overrideConfiguration (o -> o .addApiName (api -> api
205+ .name (TEST_API_NAME )
206+ .version (TEST_API_VERSION )))))
207+ .hasMessageContaining (INTERCEPTOR_STOP_MESSAGE );
208+ }
209+
210+ private String getCapturedUserAgent () {
211+ Map <String , List <String >> headers = interceptor .context .httpRequest ().headers ();
212+ assertThat (headers ).containsKey (USER_AGENT_HEADER );
213+ return headers .get (USER_AGENT_HEADER ).get (0 );
214+ }
215+
216+ private List <String > getCapturedUserAgentList () {
217+ Map <String , List <String >> headers = interceptor .context .httpRequest ().headers ();
218+ return headers .get (USER_AGENT_HEADER );
115219 }
116220
117221 private RestJsonEndpointProvidersClientBuilder defaultClientBuilder () {
@@ -133,23 +237,15 @@ private RestJsonEndpointProvidersClient clientWithCustomUserAgent(String customU
133237 .build ();
134238 }
135239
136- private void executeRequestExpectingInterception (RestJsonEndpointProvidersClient client ) {
137- assertThatThrownBy (() -> client .allTypes (r -> {}))
138- .hasMessageContaining ("stop" );
139- }
140-
141- private void executeRequestWithApiName (RestJsonEndpointProvidersClient client ) {
142- assertThatThrownBy (() -> client .allTypes (r -> r
143- .overrideConfiguration (o -> o .addApiName (api -> api
144- .name (TEST_API_NAME )
145- .version (TEST_API_VERSION )))))
146- .hasMessageContaining ("stop" );
147- }
148-
149- private String getCapturedUserAgent () {
150- Map <String , List <String >> headers = interceptor .context .httpRequest ().headers ();
151- assertThat (headers ).containsKey (USER_AGENT_HEADER );
152- return headers .get (USER_AGENT_HEADER ).get (0 );
240+ private RestJsonEndpointProvidersClient clientWithCustomUserAgentList (List <String > customUserAgentList ) {
241+ return RestJsonEndpointProvidersClient .builder ()
242+ .region (Region .US_WEST_2 )
243+ .credentialsProvider (StaticCredentialsProvider .create (
244+ AwsBasicCredentials .create ("akid" , "skid" )))
245+ .overrideConfiguration (c -> c
246+ .addExecutionInterceptor (interceptor )
247+ .putHeader (USER_AGENT_HEADER , customUserAgentList ))
248+ .build ();
153249 }
154250
155251 private static class CapturingInterceptor implements ExecutionInterceptor {
@@ -158,7 +254,7 @@ private static class CapturingInterceptor implements ExecutionInterceptor {
158254 @ Override
159255 public void beforeTransmission (Context .BeforeTransmission context , ExecutionAttributes executionAttributes ) {
160256 this .context = context ;
161- throw new RuntimeException ("stop" );
257+ throw new RuntimeException (INTERCEPTOR_STOP_MESSAGE );
162258 }
163259 }
164260}
0 commit comments