diff --git a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java index 698ee82..6764baa 100644 --- a/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java +++ b/ans-sdk-core/src/main/java/com/godaddy/ans/sdk/config/AnsConfiguration.java @@ -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; @@ -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); } } diff --git a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java index 48224f5..ff14821 100644 --- a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java +++ b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/config/AnsConfigurationTest.java @@ -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(); diff --git a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/http/HttpClientFactoryTest.java b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/http/HttpClientFactoryTest.java index a4091d4..49f4cda 100644 --- a/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/http/HttpClientFactoryTest.java +++ b/ans-sdk-core/src/test/java/com/godaddy/ans/sdk/http/HttpClientFactoryTest.java @@ -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; @@ -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(); @@ -40,6 +42,7 @@ void createDefaultShouldReturnClientWithDefaults() { @Test void createShouldConfigureRedirectPolicy() { AnsConfiguration config = AnsConfiguration.builder() + .environment(Environment.OTE) .credentialsProvider(new JwtCredentialsProvider("test-token")) .build(); diff --git a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/DiscoveryClient.java b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/DiscoveryClient.java index 915bb65..12ebcfd 100644 --- a/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/DiscoveryClient.java +++ b/ans-sdk-discovery/src/main/java/com/godaddy/ans/sdk/discovery/DiscoveryClient.java @@ -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. + * + *
When set, this configuration is used as-is and any values set via + * other builder methods are ignored.
+ * + * @param configuration the pre-built configuration + * @return this builder + */ + public Builder configuration(AnsConfiguration configuration) { + this.prebuiltConfiguration = configuration; + return this; + } + /** * Sets the environment. * @@ -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); } } } \ No newline at end of file diff --git a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java index 241ea8f..41d8733 100644 --- a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java +++ b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/DiscoveryClientTest.java @@ -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; @@ -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(); @@ -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 @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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(); diff --git a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/ResolutionServiceTest.java b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/ResolutionServiceTest.java index 22fd0b3..a0ed2db 100644 --- a/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/ResolutionServiceTest.java +++ b/ans-sdk-discovery/src/test/java/com/godaddy/ans/sdk/discovery/ResolutionServiceTest.java @@ -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(); @@ -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 ==================== /** diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java index 5fa3b18..6dc6afa 100644 --- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java +++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationClient.java @@ -56,7 +56,8 @@ public final class RegistrationClient { private RegistrationClient(AnsConfiguration configuration) { this.configuration = configuration; - this.registrationService = new RegistrationService(configuration); + var ansClient = new AnsApiClient(configuration); + this.registrationService = new RegistrationService(ansClient); this.certificateService = new CertificateService(configuration); } @@ -251,10 +252,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. + * + *When set, this configuration is used as-is and any values set via + * other builder methods are ignored.
+ * + * @param configuration the pre-built configuration + * @return this builder + */ + public Builder configuration(AnsConfiguration configuration) { + this.prebuiltConfiguration = configuration; + return this; + } + /** * Sets the environment. * @@ -327,7 +343,10 @@ public Builder enableRetry(int maxRetries) { * @return a new RegistrationClient instance */ public RegistrationClient build() { - return new RegistrationClient(configBuilder.build()); + AnsConfiguration config = (prebuiltConfiguration != null) + ? prebuiltConfiguration + : configBuilder.build(); + return new RegistrationClient(config); } } } \ No newline at end of file diff --git a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java index 3d9d6c2..a360491 100644 --- a/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java +++ b/ans-sdk-registration/src/main/java/com/godaddy/ans/sdk/registration/RegistrationService.java @@ -1,6 +1,5 @@ package com.godaddy.ans.sdk.registration; -import com.godaddy.ans.sdk.config.AnsConfiguration; import com.godaddy.ans.sdk.exception.AnsServerException; import com.godaddy.ans.sdk.model.generated.AgentDetails; import com.godaddy.ans.sdk.model.generated.AgentRegistrationRequest; @@ -20,10 +19,9 @@ class RegistrationService { private final AnsApiClient httpClient; - RegistrationService(AnsConfiguration configuration) { - this.httpClient = new AnsApiClient(configuration); + RegistrationService(final AnsApiClient ansApiClient) { + this.httpClient = ansApiClient; } - /** * Registers a new agent and returns full agent details. * diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java index 442ef87..f566b3d 100644 --- a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java +++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/CertificateServiceTest.java @@ -4,6 +4,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.godaddy.ans.sdk.auth.ApiKeyCredentialsProvider; import com.godaddy.ans.sdk.config.AnsConfiguration; +import com.godaddy.ans.sdk.config.Environment; import com.godaddy.ans.sdk.exception.AnsAuthenticationException; import com.godaddy.ans.sdk.exception.AnsNotFoundException; import com.godaddy.ans.sdk.exception.AnsServerException; @@ -36,6 +37,7 @@ class CertificateServiceTest { private CertificateService createCertificateService(WireMockRuntimeInfo wmRuntimeInfo) { AnsConfiguration config = AnsConfiguration.builder() + .environment(Environment.OTE) .baseUrl(wmRuntimeInfo.getHttpBaseUrl()) .credentialsProvider(new ApiKeyCredentialsProvider(API_KEY, API_SECRET)) .build(); diff --git a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java index 9c90595..be20716 100644 --- a/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java +++ b/ans-sdk-registration/src/test/java/com/godaddy/ans/sdk/registration/RegistrationClientTest.java @@ -5,7 +5,9 @@ import com.godaddy.ans.sdk.auth.JwtCredentialsProvider; import com.godaddy.ans.sdk.config.Environment; import com.godaddy.ans.sdk.exception.AnsAuthenticationException; +import com.godaddy.ans.sdk.exception.AnsConflictException; import com.godaddy.ans.sdk.exception.AnsNotFoundException; +import com.godaddy.ans.sdk.exception.AnsServerException; import com.godaddy.ans.sdk.exception.AnsValidationException; import com.godaddy.ans.sdk.model.generated.AgentDetails; import com.godaddy.ans.sdk.model.generated.AgentEndpoint; @@ -63,6 +65,7 @@ void shouldBuildClientWithCustomBaseUrl(WireMockRuntimeInfo wmRuntimeInfo) { String baseUrl = wmRuntimeInfo.getHttpBaseUrl(); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -140,6 +143,7 @@ void shouldRegisterAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(agentDetailsResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -183,6 +187,7 @@ void shouldThrowValidationExceptionOn422(WireMockRuntimeInfo wmRuntimeInfo) { + "\"message\":\"Invalid version format\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -216,6 +221,7 @@ void shouldVerifyAcmeSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(agentStatusResponse(AgentLifecycleStatus.PENDING_DNS)))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -238,6 +244,7 @@ void shouldThrowNotFoundExceptionForVerifyAcme(WireMockRuntimeInfo wmRuntimeInfo .withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -261,6 +268,7 @@ void shouldVerifyDnsSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(agentStatusResponse(AgentLifecycleStatus.ACTIVE)))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -283,6 +291,7 @@ void shouldThrowAuthExceptionForVerifyDns(WireMockRuntimeInfo wmRuntimeInfo) { .withBody("{\"status\":\"error\",\"code\":\"UNAUTHORIZED\",\"message\":\"Invalid token\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -306,6 +315,7 @@ void shouldRevokeAgentSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(revocationResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -340,6 +350,7 @@ void shouldRevokeAgentWithJustReason(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(revocationResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -366,6 +377,7 @@ void shouldThrowNotFoundExceptionForRevoke(WireMockRuntimeInfo wmRuntimeInfo) { .withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -389,6 +401,7 @@ void shouldThrowValidationExceptionWhenAlreadyRevoked(WireMockRuntimeInfo wmRunt + "\"message\":\"Agent is already in REVOKED state\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -412,6 +425,7 @@ void shouldThrowValidationExceptionForPendingValidation(WireMockRuntimeInfo wmRu + "\"message\":\"Cannot revoke agent in PENDING_VALIDATION state\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -436,6 +450,7 @@ void shouldGetAgentByIdSuccessfully(WireMockRuntimeInfo wmRuntimeInfo) { .withBody(agentDetailsResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -459,6 +474,7 @@ void shouldThrowNotFoundExceptionWhenAgentNotFound(WireMockRuntimeInfo wmRuntime .withBody("{\"status\":\"error\",\"code\":\"NOT_FOUND\",\"message\":\"Agent not found\"}"))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -488,6 +504,7 @@ void shouldRegisterAgentAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exceptio .withBody(agentDetailsResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -520,6 +537,7 @@ void shouldVerifyAcmeAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { .withBody(agentStatusResponse(AgentLifecycleStatus.PENDING_DNS)))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -542,6 +560,7 @@ void shouldVerifyDnsAsync(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { .withBody(agentStatusResponse(AgentLifecycleStatus.ACTIVE)))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -564,6 +583,7 @@ void shouldRevokeAgentAsyncWithRequest(WireMockRuntimeInfo wmRuntimeInfo) throws .withBody(revocationResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -589,6 +609,7 @@ void shouldRevokeAgentAsyncWithReason(WireMockRuntimeInfo wmRuntimeInfo) throws .withBody(revocationResponse()))); RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) .baseUrl(baseUrl) .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) .build(); @@ -600,6 +621,126 @@ void shouldRevokeAgentAsyncWithReason(WireMockRuntimeInfo wmRuntimeInfo) throws assertThat(result.getStatus()).isEqualTo(AgentLifecycleStatus.REVOKED); } + // ==================== Error Handling Edge Cases ==================== + + @Test + @DisplayName("Should throw AnsConflictException on 409") + void shouldThrowConflictExceptionOn409(WireMockRuntimeInfo wmRuntimeInfo) { + String baseUrl = wmRuntimeInfo.getHttpBaseUrl(); + + stubFor(post(urlEqualTo("/v1/agents/register")) + .willReturn(aResponse() + .withStatus(409) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"error\",\"code\":\"CONFLICT\",\"message\":\"Agent already registered\"}"))); + + RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) + .baseUrl(baseUrl) + .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) + .build(); + + AgentRegistrationRequest request = new AgentRegistrationRequest() + .agentDisplayName("Test Agent") + .version("1.0.0") + .agentHost("test-agent.example.com") + .addEndpointsItem(new AgentEndpoint() + .protocol(AgentEndpoint.ProtocolEnum.A2_A) + .agentUrl(URI.create("https://test-agent.example.com/a2a"))) + .identityCsrPEM("test-csr") + .serverCsrPEM("test-csr"); + + assertThatThrownBy(() -> client.registerAgent(request)) + .isInstanceOf(AnsConflictException.class) + .hasMessageContaining("Conflict"); + } + + @Test + @DisplayName("Should throw AnsServerException on unexpected status code") + void shouldThrowServerExceptionOnUnexpectedStatusCode(WireMockRuntimeInfo wmRuntimeInfo) { + String baseUrl = wmRuntimeInfo.getHttpBaseUrl(); + + stubFor(get(urlEqualTo("/v1/agents/" + TEST_AGENT_ID)) + .willReturn(aResponse() + .withStatus(418) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"error\",\"message\":\"I'm a teapot\"}"))); + + RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) + .baseUrl(baseUrl) + .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) + .build(); + + assertThatThrownBy(() -> client.getAgent(TEST_AGENT_ID)) + .isInstanceOf(AnsServerException.class) + .hasMessageContaining("Unexpected error (418)"); + } + + @Test + @DisplayName("Should throw AnsServerException when registration response has no self link") + void shouldThrowWhenRegistrationMissingSelfLink(WireMockRuntimeInfo wmRuntimeInfo) { + String baseUrl = wmRuntimeInfo.getHttpBaseUrl(); + + stubFor(post(urlEqualTo("/v1/agents/register")) + .willReturn(aResponse() + .withStatus(202) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"PENDING_VALIDATION\",\"links\":[]}"))); + + RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) + .baseUrl(baseUrl) + .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) + .build(); + + AgentRegistrationRequest request = new AgentRegistrationRequest() + .agentDisplayName("Test Agent") + .version("1.0.0") + .agentHost("test-agent.example.com") + .addEndpointsItem(new AgentEndpoint() + .protocol(AgentEndpoint.ProtocolEnum.A2_A) + .agentUrl(URI.create("https://test-agent.example.com/a2a"))) + .identityCsrPEM("test-csr") + .serverCsrPEM("test-csr"); + + assertThatThrownBy(() -> client.registerAgent(request)) + .isInstanceOf(AnsServerException.class) + .hasMessageContaining("missing 'self' link"); + } + + @Test + @DisplayName("Should throw AnsServerException when registration response has null links") + void shouldThrowWhenRegistrationHasNullLinks(WireMockRuntimeInfo wmRuntimeInfo) { + String baseUrl = wmRuntimeInfo.getHttpBaseUrl(); + + stubFor(post(urlEqualTo("/v1/agents/register")) + .willReturn(aResponse() + .withStatus(202) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"PENDING_VALIDATION\"}"))); + + RegistrationClient client = RegistrationClient.builder() + .environment(Environment.OTE) + .baseUrl(baseUrl) + .credentialsProvider(new JwtCredentialsProvider(TEST_JWT_TOKEN)) + .build(); + + AgentRegistrationRequest request = new AgentRegistrationRequest() + .agentDisplayName("Test Agent") + .version("1.0.0") + .agentHost("test-agent.example.com") + .addEndpointsItem(new AgentEndpoint() + .protocol(AgentEndpoint.ProtocolEnum.A2_A) + .agentUrl(URI.create("https://test-agent.example.com/a2a"))) + .identityCsrPEM("test-csr") + .serverCsrPEM("test-csr"); + + assertThatThrownBy(() -> client.registerAgent(request)) + .isInstanceOf(AnsServerException.class) + .hasMessageContaining("missing 'self' link"); + } + // ==================== Helper Methods ==================== private String registrationPendingResponse() { diff --git a/ans-sdk-spring-boot-starter/build.gradle.kts b/ans-sdk-spring-boot-starter/build.gradle.kts new file mode 100644 index 0000000..710365a --- /dev/null +++ b/ans-sdk-spring-boot-starter/build.gradle.kts @@ -0,0 +1,22 @@ +val junitVersion: String by project +val assertjVersion: String by project + +val springBootVersion = "3.4.3" + +dependencies { + // ANS SDK modules + api(project(":ans-sdk-core")) + api(project(":ans-sdk-registration")) + api(project(":ans-sdk-discovery")) + + // Spring Boot auto-configuration + implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") + + // Optional annotation processor for configuration metadata + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") + + // Testing + testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + testImplementation("org.assertj:assertj-core:$assertjVersion") + testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") +} diff --git a/ans-sdk-spring-boot-starter/examples/build.gradle.kts b/ans-sdk-spring-boot-starter/examples/build.gradle.kts new file mode 100644 index 0000000..a59ce97 --- /dev/null +++ b/ans-sdk-spring-boot-starter/examples/build.gradle.kts @@ -0,0 +1,21 @@ +// Parent build file for ANS SDK Spring Boot examples +// Each subdirectory is a standalone example demonstrating Spring Boot integration + +subprojects { + apply(plugin = "java") + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + + repositories { + mavenLocal() + mavenCentral() + } + + tasks.withTypeBoth {@link RegistrationClient} and {@link DiscoveryClient} are + * auto-configured by the starter and injected via constructor.
+ */ +@RestController +@RequestMapping("/api/agents") +public class AgentController { + + private final RegistrationClient registrationClient; + private final DiscoveryClient discoveryClient; + + public AgentController(RegistrationClient registrationClient, + DiscoveryClient discoveryClient) { + this.registrationClient = registrationClient; + this.discoveryClient = discoveryClient; + } + + /** + * Registers a new agent. + * + *
+ * POST /api/agents/register
+ * {"agentHost": "my-agent.example.com", "agentVersion": "1.0.0"}
+ *
+ */
+ @PostMapping("/register")
+ public ResponseEntity+ * GET /api/agents/resolve?agentHost=my-agent.example.com + * GET /api/agents/resolve?agentHost=my-agent.example.com&version=^1.0.0 + *+ */ + @GetMapping("/resolve") + public ResponseEntity