Skip to content

Commit 7290242

Browse files
feat: require explicit environment and add Spring Boot starter module (#36)
* feat!: require explicit environment in AnsConfiguration * fix: update tests for required environment across all modules * feat: add Spring Boot starter module with auto-configuration * refactor: inject AnsApiClient into RegistrationService for testability * test: add coverage for error handling edge cases in registration module Cover 409 conflict, unexpected status codes, and missing self-link paths to bring instruction coverage from 89% to 91%. * Addressed PR comments: #36 * test: add discovery module tests to meet 90% JaCoCo coverage threshold Cover Builder.configuration() with pre-built config and edge cases in extractAgentDetailsLink (missing links field, no matching rel).
1 parent 5262494 commit 7290242

22 files changed

Lines changed: 1114 additions & 20 deletions

File tree

ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public boolean isRetryEnabled() {
120120
*/
121121
public static final class Builder {
122122

123-
private Environment environment = Environment.OTE;
123+
private Environment environment;
124124
private String baseUrl;
125125
private AnsCredentialsProvider credentialsProvider;
126126
private Duration connectTimeout;
@@ -206,6 +206,9 @@ public Builder enableRetry(int maxRetries) {
206206
* @throws NullPointerException if required fields are not set
207207
*/
208208
public AnsConfiguration build() {
209+
if (this.environment == null) {
210+
throw new IllegalStateException("Environment is required");
211+
}
209212
return new AnsConfiguration(this);
210213
}
211214
}

ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,20 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() {
115115
}
116116

117117
@Test
118-
@DisplayName("Should use default OTE environment when not specified")
119-
void shouldUseDefaultOteEnvironment() {
120-
AnsConfiguration config = AnsConfiguration.builder()
118+
@DisplayName("Should throw when environment is not set")
119+
void shouldThrowWhenEnvironmentNotSet() {
120+
assertThatThrownBy(() -> AnsConfiguration.builder()
121121
.credentialsProvider(testProvider)
122-
.build();
123-
124-
assertThat(config.getEnvironment()).isEqualTo(Environment.OTE);
125-
assertThat(config.getBaseUrl()).isEqualTo("https://api.ote-godaddy.com");
122+
.build())
123+
.isInstanceOf(IllegalStateException.class)
124+
.hasMessageContaining("Environment is required");
126125
}
127126

128127
@Test
129-
@DisplayName("Should allow custom base URL with default environment")
130-
void shouldAllowCustomBaseUrlWithDefaultEnvironment() {
128+
@DisplayName("Should allow custom base URL with explicit environment")
129+
void shouldAllowCustomBaseUrlWithExplicitEnvironment() {
131130
AnsConfiguration config = AnsConfiguration.builder()
131+
.environment(Environment.OTE)
132132
.baseUrl("http://custom-url.com")
133133
.credentialsProvider(testProvider)
134134
.build();

ans-sdk-core/src/test/java/com/godaddy/ans/sdk/http/HttpClientFactoryTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
44
import com.godaddy.ans.sdk.config.AnsConfiguration;
5+
import com.godaddy.ans.sdk.config.Environment;
56
import org.junit.jupiter.api.Test;
67

78
import java.net.http.HttpClient;
@@ -17,6 +18,7 @@ class HttpClientFactoryTest {
1718
@Test
1819
void createWithConfigurationShouldReturnConfiguredClient() {
1920
AnsConfiguration config = AnsConfiguration.builder()
21+
.environment(Environment.OTE)
2022
.credentialsProvider(new JwtCredentialsProvider("test-token"))
2123
.connectTimeout(Duration.ofSeconds(30))
2224
.build();
@@ -40,6 +42,7 @@ void createDefaultShouldReturnClientWithDefaults() {
4042
@Test
4143
void createShouldConfigureRedirectPolicy() {
4244
AnsConfiguration config = AnsConfiguration.builder()
45+
.environment(Environment.OTE)
4346
.credentialsProvider(new JwtCredentialsProvider("test-token"))
4447
.build();
4548

ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/DiscoveryClient.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,25 @@ public AnsConfiguration getConfiguration() {
136136
public static final class Builder {
137137

138138
private final AnsConfiguration.Builder configBuilder = AnsConfiguration.builder();
139+
private AnsConfiguration prebuiltConfiguration;
139140

140141
private Builder() {
141142
}
142143

144+
/**
145+
* Uses a pre-built configuration directly.
146+
*
147+
* <p>When set, this configuration is used as-is and any values set via
148+
* other builder methods are ignored.</p>
149+
*
150+
* @param configuration the pre-built configuration
151+
* @return this builder
152+
*/
153+
public Builder configuration(AnsConfiguration configuration) {
154+
this.prebuiltConfiguration = configuration;
155+
return this;
156+
}
157+
143158
/**
144159
* Sets the environment.
145160
*
@@ -212,7 +227,10 @@ public Builder enableRetry(int maxRetries) {
212227
* @return a new DiscoveryClient instance
213228
*/
214229
public DiscoveryClient build() {
215-
return new DiscoveryClient(configBuilder.build());
230+
AnsConfiguration config = (prebuiltConfiguration != null)
231+
? prebuiltConfiguration
232+
: configBuilder.build();
233+
return new DiscoveryClient(config);
216234
}
217235
}
218236
}

ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
44
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
55
import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
6+
import com.godaddy.ans.sdk.config.AnsConfiguration;
67
import com.godaddy.ans.sdk.config.Environment;
78
import com.godaddy.ans.sdk.model.generated.AgentDetails;
89
import com.godaddy.ans.sdk.model.generated.AgentLifecycleStatus;
@@ -71,6 +72,7 @@ void shouldBuildClientWithCustomBaseUrl(WireMockRuntimeInfo wmRuntimeInfo) {
7172
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();
7273

7374
DiscoveryClient client = DiscoveryClient.builder()
75+
.environment(Environment.OTE)
7476
.baseUrl(baseUrl)
7577
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
7678
.build();
@@ -115,6 +117,24 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() {
115117
.isInstanceOf(NullPointerException.class);
116118
}
117119

120+
@Test
121+
@DisplayName("Should build client with pre-built configuration")
122+
void shouldBuildClientWithPreBuiltConfiguration() {
123+
AnsConfiguration prebuilt = AnsConfiguration.builder()
124+
.environment(Environment.PROD)
125+
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
126+
.baseUrl("https://custom.example.com")
127+
.build();
128+
129+
DiscoveryClient client = DiscoveryClient.builder()
130+
.environment(Environment.OTE)
131+
.configuration(prebuilt)
132+
.build();
133+
134+
assertThat(client.getConfiguration()).isSameAs(prebuilt);
135+
assertThat(client.getConfiguration().getBaseUrl()).isEqualTo("https://custom.example.com");
136+
}
137+
118138
// ==================== Resolution Success Tests ====================
119139

120140
@Test
@@ -137,6 +157,7 @@ void shouldResolveAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
137157
.withBody(agentDetailsResponse())));
138158

139159
DiscoveryClient client = DiscoveryClient.builder()
160+
.environment(Environment.OTE)
140161
.baseUrl(baseUrl)
141162
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
142163
.build();
@@ -196,6 +217,7 @@ void shouldResolveAgentWithoutVersion(WireMockRuntimeInfo wmRuntimeInfo) {
196217
.withBody(agentDetailsResponse())));
197218

198219
DiscoveryClient client = DiscoveryClient.builder()
220+
.environment(Environment.OTE)
199221
.baseUrl(baseUrl)
200222
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
201223
.build();
@@ -228,6 +250,7 @@ void shouldResolveAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception
228250
.withBody(agentDetailsResponse())));
229251

230252
DiscoveryClient client = DiscoveryClient.builder()
253+
.environment(Environment.OTE)
231254
.baseUrl(baseUrl)
232255
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
233256
.build();
@@ -253,6 +276,7 @@ void shouldThrowNotFoundExceptionWhen404(WireMockRuntimeInfo wmRuntimeInfo) {
253276
.withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}")));
254277

255278
DiscoveryClient client = DiscoveryClient.builder()
279+
.environment(Environment.OTE)
256280
.baseUrl(baseUrl)
257281
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
258282
.build();
@@ -274,6 +298,7 @@ void shouldThrowAuthExceptionWhen401(WireMockRuntimeInfo wmRuntimeInfo) {
274298
.withBody("{\"status\":\"error\",\"code\":\"UNAUTHORIZED\",\"message\":\"Invalid credentials\"}")));
275299

276300
DiscoveryClient client = DiscoveryClient.builder()
301+
.environment(Environment.OTE)
277302
.baseUrl(baseUrl)
278303
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
279304
.build();
@@ -295,6 +320,7 @@ void shouldThrowAuthExceptionWhen403(WireMockRuntimeInfo wmRuntimeInfo) {
295320
.withBody("{\"status\":\"error\",\"code\":\"FORBIDDEN\",\"message\":\"Access denied\"}")));
296321

297322
DiscoveryClient client = DiscoveryClient.builder()
323+
.environment(Environment.OTE)
298324
.baseUrl(baseUrl)
299325
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
300326
.build();
@@ -317,6 +343,7 @@ void shouldThrowValidationExceptionWhen422(WireMockRuntimeInfo wmRuntimeInfo) {
317343
+ "\"message\":\"Invalid version format\"}")));
318344

319345
DiscoveryClient client = DiscoveryClient.builder()
346+
.environment(Environment.OTE)
320347
.baseUrl(baseUrl)
321348
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
322349
.build();
@@ -338,6 +365,7 @@ void shouldThrowServerExceptionWhen500(WireMockRuntimeInfo wmRuntimeInfo) {
338365
.withBody("{\"status\":\"error\",\"code\":\"INTERNAL_ERROR\",\"message\":\"Internal server error\"}")));
339366

340367
DiscoveryClient client = DiscoveryClient.builder()
368+
.environment(Environment.OTE)
341369
.baseUrl(baseUrl)
342370
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
343371
.build();
@@ -360,6 +388,7 @@ void shouldThrowServerExceptionWhenLinkMissing(WireMockRuntimeInfo wmRuntimeInfo
360388
.withBody("{\"ansName\":\"ans://v1.0.0.booking-agent.example.com\",\"links\":[]}")));
361389

362390
DiscoveryClient client = DiscoveryClient.builder()
391+
.environment(Environment.OTE)
363392
.baseUrl(baseUrl)
364393
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
365394
.build();
@@ -381,6 +410,7 @@ void shouldWrapExceptionInAsync(WireMockRuntimeInfo wmRuntimeInfo) {
381410
.withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}")));
382411

383412
DiscoveryClient client = DiscoveryClient.builder()
413+
.environment(Environment.OTE)
384414
.baseUrl(baseUrl)
385415
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
386416
.build();
@@ -406,6 +436,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
406436
.withBody(agentDetailsResponse())));
407437

408438
DiscoveryClient client = DiscoveryClient.builder()
439+
.environment(Environment.OTE)
409440
.baseUrl(baseUrl)
410441
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
411442
.build();
@@ -433,6 +464,7 @@ void shouldThrowNotFoundWhenGettingNonExistentAgent(WireMockRuntimeInfo wmRuntim
433464
.withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}")));
434465

435466
DiscoveryClient client = DiscoveryClient.builder()
467+
.environment(Environment.OTE)
436468
.baseUrl(baseUrl)
437469
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
438470
.build();
@@ -454,6 +486,7 @@ void shouldThrowAuthExceptionWhenUnauthorizedToGetAgent(WireMockRuntimeInfo wmRu
454486
.withBody("{\"status\":\"error\",\"code\":\"UNAUTHORIZED\",\"message\":\"Invalid token\"}")));
455487

456488
DiscoveryClient client = DiscoveryClient.builder()
489+
.environment(Environment.OTE)
457490
.baseUrl(baseUrl)
458491
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
459492
.build();
@@ -475,6 +508,7 @@ void shouldGetAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
475508
.withBody(agentDetailsResponse())));
476509

477510
DiscoveryClient client = DiscoveryClient.builder()
511+
.environment(Environment.OTE)
478512
.baseUrl(baseUrl)
479513
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
480514
.build();
@@ -498,6 +532,7 @@ void shouldWrapExceptionInAsyncGetAgent(WireMockRuntimeInfo wmRuntimeInfo) {
498532
.withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}")));
499533

500534
DiscoveryClient client = DiscoveryClient.builder()
535+
.environment(Environment.OTE)
501536
.baseUrl(baseUrl)
502537
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
503538
.build();
@@ -540,6 +575,7 @@ void shouldHandleRelativeHrefInResolutionResponse(WireMockRuntimeInfo wmRuntimeI
540575
.withBody(agentDetailsResponse())));
541576

542577
DiscoveryClient client = DiscoveryClient.builder()
578+
.environment(Environment.OTE)
543579
.baseUrl(baseUrl)
544580
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
545581
.build();
@@ -562,6 +598,7 @@ void shouldThrowServerExceptionForMalformedResolutionJson(WireMockRuntimeInfo wm
562598
.withBody("{ invalid json }")));
563599

564600
DiscoveryClient client = DiscoveryClient.builder()
601+
.environment(Environment.OTE)
565602
.baseUrl(baseUrl)
566603
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
567604
.build();
@@ -589,6 +626,7 @@ void shouldThrowServerExceptionForMalformedAgentDetailsJson(WireMockRuntimeInfo
589626
.withBody("{ not valid json }")));
590627

591628
DiscoveryClient client = DiscoveryClient.builder()
629+
.environment(Environment.OTE)
592630
.baseUrl(baseUrl)
593631
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
594632
.build();
@@ -610,6 +648,7 @@ void shouldThrowServerExceptionForUnexpected4xxError(WireMockRuntimeInfo wmRunti
610648
.withBody("{\"status\":\"error\",\"message\":\"Bad request\"}")));
611649

612650
DiscoveryClient client = DiscoveryClient.builder()
651+
.environment(Environment.OTE)
613652
.baseUrl(baseUrl)
614653
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
615654
.build();
@@ -637,6 +676,7 @@ void shouldHandleNullVersionInResolve(WireMockRuntimeInfo wmRuntimeInfo) {
637676
.withBody(agentDetailsResponse())));
638677

639678
DiscoveryClient client = DiscoveryClient.builder()
679+
.environment(Environment.OTE)
640680
.baseUrl(baseUrl)
641681
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
642682
.build();
@@ -668,6 +708,7 @@ void shouldHandleEmptyVersionStringInResolve(WireMockRuntimeInfo wmRuntimeInfo)
668708
.withBody(agentDetailsResponse())));
669709

670710
DiscoveryClient client = DiscoveryClient.builder()
711+
.environment(Environment.OTE)
671712
.baseUrl(baseUrl)
672713
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
673714
.build();
@@ -695,6 +736,7 @@ void shouldIncludeRequestIdInErrorResponse(WireMockRuntimeInfo wmRuntimeInfo) {
695736
.withBody("{\"status\":\"error\",\"message\":\"Internal error\"}")));
696737

697738
DiscoveryClient client = DiscoveryClient.builder()
739+
.environment(Environment.OTE)
698740
.baseUrl(baseUrl)
699741
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
700742
.build();

ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/ResolutionServiceTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ void setUp() {
3232
when(mockProvider.resolveCredentials()).thenReturn(mockCredentials);
3333

3434
AnsConfiguration config = AnsConfiguration.builder()
35+
.environment(com.godaddy.ans.sdk.config.Environment.OTE)
3536
.credentialsProvider(mockProvider)
3637
.baseUrl("https://api.example.com")
3738
.build();
@@ -139,6 +140,43 @@ void shouldRejectPathTraversalAttempts() throws Exception {
139140
.hasMessageContaining("Invalid agent-details link");
140141
}
141142

143+
// ==================== Edge Case Tests ====================
144+
145+
@Test
146+
@DisplayName("Should throw when response has no links field")
147+
void shouldThrowWhenResponseHasNoLinksField() {
148+
String responseBody = """
149+
{
150+
"ansName": "ans://v1.0.0.example.com"
151+
}
152+
""";
153+
154+
Throwable thrown = catchThrowable(() -> invokeExtractAgentDetailsLink(responseBody));
155+
assertThat(thrown).isInstanceOf(InvocationTargetException.class);
156+
assertThat(thrown.getCause())
157+
.isInstanceOf(AnsServerException.class)
158+
.hasMessageContaining("missing agent-details link");
159+
}
160+
161+
@Test
162+
@DisplayName("Should throw when links contain no matching rel")
163+
void shouldThrowWhenLinksContainNoMatchingRel() {
164+
String responseBody = """
165+
{
166+
"links": [
167+
{"rel": "self", "href": "/v1/agents/abc123"},
168+
{"href": "/v1/agents/def456"}
169+
]
170+
}
171+
""";
172+
173+
Throwable thrown = catchThrowable(() -> invokeExtractAgentDetailsLink(responseBody));
174+
assertThat(thrown).isInstanceOf(InvocationTargetException.class);
175+
assertThat(thrown.getCause())
176+
.isInstanceOf(AnsServerException.class)
177+
.hasMessageContaining("missing agent-details link");
178+
}
179+
142180
// ==================== Helper Methods ====================
143181

144182
/**

0 commit comments

Comments
 (0)