Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public boolean isRetryEnabled() {
*/
public static final class Builder {

private Environment environment = Environment.OTE;
private Environment environment;
private String baseUrl;
private AnsCredentialsProvider credentialsProvider;
private Duration connectTimeout;
Expand Down Expand Up @@ -206,6 +206,9 @@ public Builder enableRetry(int maxRetries) {
* @throws NullPointerException if required fields are not set
*/
public AnsConfiguration build() {
if (this.environment == null) {
throw new IllegalStateException("Environment is required");
}
return new AnsConfiguration(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,20 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() {
}

@Test
@DisplayName("Should use default OTE environment when not specified")
void shouldUseDefaultOteEnvironment() {
AnsConfiguration config = AnsConfiguration.builder()
@DisplayName("Should throw when environment is not set")
void shouldThrowWhenEnvironmentNotSet() {
assertThatThrownBy(() -> AnsConfiguration.builder()
.credentialsProvider(testProvider)
.build();

assertThat(config.getEnvironment()).isEqualTo(Environment.OTE);
assertThat(config.getBaseUrl()).isEqualTo("https://api.ote-godaddy.com");
.build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Environment is required");
}

@Test
@DisplayName("Should allow custom base URL with default environment")
void shouldAllowCustomBaseUrlWithDefaultEnvironment() {
@DisplayName("Should allow custom base URL with explicit environment")
void shouldAllowCustomBaseUrlWithExplicitEnvironment() {
AnsConfiguration config = AnsConfiguration.builder()
.environment(Environment.OTE)
.baseUrl("http://custom-url.com")
.credentialsProvider(testProvider)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
import com.godaddy.ans.sdk.config.AnsConfiguration;
import com.godaddy.ans.sdk.config.Environment;
import org.junit.jupiter.api.Test;

import java.net.http.HttpClient;
Expand All @@ -17,6 +18,7 @@ class HttpClientFactoryTest {
@Test
void createWithConfigurationShouldReturnConfiguredClient() {
AnsConfiguration config = AnsConfiguration.builder()
.environment(Environment.OTE)
.credentialsProvider(new JwtCredentialsProvider("test-token"))
.connectTimeout(Duration.ofSeconds(30))
.build();
Expand All @@ -40,6 +42,7 @@ void createDefaultShouldReturnClientWithDefaults() {
@Test
void createShouldConfigureRedirectPolicy() {
AnsConfiguration config = AnsConfiguration.builder()
.environment(Environment.OTE)
.credentialsProvider(new JwtCredentialsProvider("test-token"))
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,25 @@ public AnsConfiguration getConfiguration() {
public static final class Builder {

private final AnsConfiguration.Builder configBuilder = AnsConfiguration.builder();
private AnsConfiguration prebuiltConfiguration;

private Builder() {
}

/**
* Uses a pre-built configuration directly.
*
* <p>When set, this configuration is used as-is and any values set via
* other builder methods are ignored.</p>
*
* @param configuration the pre-built configuration
* @return this builder
*/
public Builder configuration(AnsConfiguration configuration) {
this.prebuiltConfiguration = configuration;
return this;
}

/**
* Sets the environment.
*
Expand Down Expand Up @@ -212,7 +227,10 @@ public Builder enableRetry(int maxRetries) {
* @return a new DiscoveryClient instance
*/
public DiscoveryClient build() {
return new DiscoveryClient(configBuilder.build());
AnsConfiguration config = (prebuiltConfiguration != null)
? prebuiltConfiguration
: configBuilder.build();
return new DiscoveryClient(config);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.godaddy.ans.sdk.auth.JwtCredentialsProvider;
import com.godaddy.ans.sdk.config.AnsConfiguration;
import com.godaddy.ans.sdk.config.Environment;
import com.godaddy.ans.sdk.model.generated.AgentDetails;
import com.godaddy.ans.sdk.model.generated.AgentLifecycleStatus;
Expand Down Expand Up @@ -71,6 +72,7 @@ void shouldBuildClientWithCustomBaseUrl(WireMockRuntimeInfo wmRuntimeInfo) {
String baseUrl = wmRuntimeInfo.getHttpBaseUrl();

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -115,6 +117,24 @@ void shouldThrowExceptionWhenCredentialsProviderIsNull() {
.isInstanceOf(NullPointerException.class);
}

@Test
@DisplayName("Should build client with pre-built configuration")
void shouldBuildClientWithPreBuiltConfiguration() {
AnsConfiguration prebuilt = AnsConfiguration.builder()
.environment(Environment.PROD)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.baseUrl("https://custom.example.com")
.build();

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.configuration(prebuilt)
.build();

assertThat(client.getConfiguration()).isSameAs(prebuilt);
assertThat(client.getConfiguration().getBaseUrl()).isEqualTo("https://custom.example.com");
}

// ==================== Resolution Success Tests ====================

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

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -196,6 +217,7 @@ void shouldResolveAgentWithoutVersion(WireMockRuntimeInfo wmRuntimeInfo) {
.withBody(agentDetailsResponse())));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -228,6 +250,7 @@ void shouldResolveAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception
.withBody(agentDetailsResponse())));

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

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

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

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

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

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

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

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand All @@ -406,6 +436,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) {
.withBody(agentDetailsResponse())));

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

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

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand All @@ -475,6 +508,7 @@ void shouldGetAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
.withBody(agentDetailsResponse())));

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

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -540,6 +575,7 @@ void shouldHandleRelativeHrefInResolutionResponse(WireMockRuntimeInfo wmRuntimeI
.withBody(agentDetailsResponse())));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand All @@ -562,6 +598,7 @@ void shouldThrowServerExceptionForMalformedResolutionJson(WireMockRuntimeInfo wm
.withBody("{ invalid json }")));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -589,6 +626,7 @@ void shouldThrowServerExceptionForMalformedAgentDetailsJson(WireMockRuntimeInfo
.withBody("{ not valid json }")));

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

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -637,6 +676,7 @@ void shouldHandleNullVersionInResolve(WireMockRuntimeInfo wmRuntimeInfo) {
.withBody(agentDetailsResponse())));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -668,6 +708,7 @@ void shouldHandleEmptyVersionStringInResolve(WireMockRuntimeInfo wmRuntimeInfo)
.withBody(agentDetailsResponse())));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down Expand Up @@ -695,6 +736,7 @@ void shouldIncludeRequestIdInErrorResponse(WireMockRuntimeInfo wmRuntimeInfo) {
.withBody("{\"status\":\"error\",\"message\":\"Internal error\"}")));

DiscoveryClient client = DiscoveryClient.builder()
.environment(Environment.OTE)
.baseUrl(baseUrl)
.credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN))
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ void setUp() {
when(mockProvider.resolveCredentials()).thenReturn(mockCredentials);

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

// ==================== Edge Case Tests ====================

@Test
@DisplayName("Should throw when response has no links field")
void shouldThrowWhenResponseHasNoLinksField() {
String responseBody = """
{
"ansName": "ans://v1.0.0.example.com"
}
""";

Throwable thrown = catchThrowable(() -> invokeExtractAgentDetailsLink(responseBody));
assertThat(thrown).isInstanceOf(InvocationTargetException.class);
assertThat(thrown.getCause())
.isInstanceOf(AnsServerException.class)
.hasMessageContaining("missing agent-details link");
}

@Test
@DisplayName("Should throw when links contain no matching rel")
void shouldThrowWhenLinksContainNoMatchingRel() {
String responseBody = """
{
"links": [
{"rel": "self", "href": "/v1/agents/abc123"},
{"href": "/v1/agents/def456"}
]
}
""";

Throwable thrown = catchThrowable(() -> invokeExtractAgentDetailsLink(responseBody));
assertThat(thrown).isInstanceOf(InvocationTargetException.class);
assertThat(thrown.getCause())
.isInstanceOf(AnsServerException.class)
.hasMessageContaining("missing agent-details link");
}

// ==================== Helper Methods ====================

/**
Expand Down
Loading
Loading