feat: Phase 2 Workstream 4 — Java SDK (ai.sentryagent:idp-sdk)
Java 17 SDK in sdk-java/: - AgentIdPClient composing AgentRegistryClient, CredentialClient, TokenClient, AuditClient — all 14 endpoints covered - Both sync methods and CompletableFuture<T> async counterparts on each client - Thread-safe TokenManager (synchronized) with 60s refresh buffer - AgentIdPException (extends RuntimeException) with Code/HTTPStatus/Details - Builder pattern for all request types; Jackson 2.17 for JSON - Zero external HTTP dependencies — java.net.http.HttpClient (Java 11+) - No-dep JDK HttpServer used for unit tests (no WireMock needed) - mvn verify: 49/49 tests passed | JaCoCo coverage gate: >80% ✓ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package ai.sentryagent.idp;
|
||||
|
||||
import ai.sentryagent.idp.models.Agent;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AgentIdPClientTest {
|
||||
|
||||
private MockServer srv;
|
||||
|
||||
private static final String TOKEN_BODY =
|
||||
"{\"access_token\":\"integration-token\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"scope\":\"agents:read agents:write tokens:read audit:read\"}";
|
||||
|
||||
private static final String AGENT_JSON =
|
||||
"{\"agentId\":\"uuid-1\",\"email\":\"a@b.ai\",\"agentType\":\"screener\",\"version\":\"1.0.0\"," +
|
||||
"\"capabilities\":[\"read\"],\"owner\":\"team\",\"deploymentEnv\":\"production\"," +
|
||||
"\"status\":\"active\",\"createdAt\":\"2026-01-01T00:00:00Z\",\"updatedAt\":\"2026-01-01T00:00:00Z\"}";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
// Register token endpoint for every test (each test gets a fresh MockServer)
|
||||
srv.addHandler("/api/v1/token", 200, TOKEN_BODY);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
private AgentIdPClient makeClient() {
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
return new AgentIdPClient(srv.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAgent_endToEnd() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
AgentIdPClient client = makeClient();
|
||||
Agent agent = client.agents().getAgent("uuid-1");
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
assertEquals("screener", agent.getAgentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void serviceClients_areAccessible() {
|
||||
AgentIdPClient client = makeClient();
|
||||
assertNotNull(client.agents());
|
||||
assertNotNull(client.credentials());
|
||||
assertNotNull(client.tokens());
|
||||
assertNotNull(client.audit());
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearTokenCache_forcesRefetch() throws IOException {
|
||||
// Dedicated MockServer so we control the token counter from scratch
|
||||
MockServer dedicated = new MockServer();
|
||||
AtomicInteger tokenCalls = new AtomicInteger(0);
|
||||
dedicated.addHandler("/api/v1/token", exchange -> {
|
||||
tokenCalls.incrementAndGet();
|
||||
try {
|
||||
byte[] body = TOKEN_BODY.getBytes();
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.getResponseBody().close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
dedicated.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
|
||||
try {
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
AgentIdPClient client = new AgentIdPClient(dedicated.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
client.agents().getAgent("uuid-1");
|
||||
client.clearTokenCache();
|
||||
client.agents().getAgent("uuid-1");
|
||||
assertEquals(2, tokenCalls.get(), "Token should be refetched after clearTokenCache");
|
||||
} finally {
|
||||
dedicated.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultScope_containsAllFourScopes() throws IOException {
|
||||
MockServer dedicated = new MockServer();
|
||||
StringBuilder capturedBody = new StringBuilder();
|
||||
dedicated.addHandler("/api/v1/token", exchange -> {
|
||||
try {
|
||||
String body = new String(exchange.getRequestBody().readAllBytes());
|
||||
capturedBody.append(body);
|
||||
byte[] resp = TOKEN_BODY.getBytes();
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, resp.length);
|
||||
exchange.getResponseBody().write(resp);
|
||||
exchange.getResponseBody().close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
dedicated.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
|
||||
try {
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
// Two-arg constructor → default scope applied
|
||||
AgentIdPClient client = new AgentIdPClient(dedicated.baseUrl(), "cid", "secret",
|
||||
"agents:read agents:write tokens:read audit:read", httpClient);
|
||||
client.agents().getAgent("uuid-1");
|
||||
String captured = capturedBody.toString();
|
||||
assertTrue(captured.contains("agents"), "Scope should be present in token request body: " + captured);
|
||||
} finally {
|
||||
dedicated.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ai.sentryagent.idp;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AgentIdPExceptionTest {
|
||||
|
||||
@Test
|
||||
void constructor_setsFields() {
|
||||
AgentIdPException ex = new AgentIdPException("AgentNotFoundError", "Not found.", 404);
|
||||
assertEquals("AgentNotFoundError", ex.getCode());
|
||||
assertEquals("Not found.", ex.getMessage());
|
||||
assertEquals(404, ex.getHttpStatus());
|
||||
assertNull(ex.getDetails());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromApiError_validBody() {
|
||||
String body = "{\"code\":\"AgentNotFoundError\",\"message\":\"Not found.\"}";
|
||||
AgentIdPException ex = AgentIdPException.fromApiError(body, 404);
|
||||
assertEquals("AgentNotFoundError", ex.getCode());
|
||||
assertEquals("Not found.", ex.getMessage());
|
||||
assertEquals(404, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromApiError_emptyCode_fallsBackToUnknown() {
|
||||
String body = "{\"message\":\"oops\"}";
|
||||
AgentIdPException ex = AgentIdPException.fromApiError(body, 503);
|
||||
assertEquals("UNKNOWN_ERROR", ex.getCode());
|
||||
assertEquals(503, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromApiError_unparseable_fallsBackToUnknown() {
|
||||
AgentIdPException ex = AgentIdPException.fromApiError("not json", 500);
|
||||
assertEquals("UNKNOWN_ERROR", ex.getCode());
|
||||
assertEquals(500, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromOAuth2Error_validBody() {
|
||||
String body = "{\"error\":\"invalid_client\",\"error_description\":\"Bad credentials.\"}";
|
||||
AgentIdPException ex = AgentIdPException.fromOAuth2Error(body, 401);
|
||||
assertEquals("invalid_client", ex.getCode());
|
||||
assertEquals("Bad credentials.", ex.getMessage());
|
||||
assertEquals(401, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromOAuth2Error_unparseable_fallsBackToUnknown() {
|
||||
AgentIdPException ex = AgentIdPException.fromOAuth2Error("garbage", 400);
|
||||
assertEquals("unknown_error", ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void networkError_setsCodeAndCause() {
|
||||
RuntimeException cause = new RuntimeException("connection refused");
|
||||
AgentIdPException ex = AgentIdPException.networkError(cause);
|
||||
assertEquals("NETWORK_ERROR", ex.getCode());
|
||||
assertEquals(0, ex.getHttpStatus());
|
||||
assertSame(cause, ex.getCause());
|
||||
assertTrue(ex.getMessage().contains("connection refused"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toString_containsCodeAndStatus() {
|
||||
AgentIdPException ex = new AgentIdPException("CODE", "msg", 400);
|
||||
assertTrue(ex.toString().contains("CODE"));
|
||||
assertTrue(ex.toString().contains("400"));
|
||||
}
|
||||
}
|
||||
73
sdk-java/src/test/java/ai/sentryagent/idp/MockServer.java
Normal file
73
sdk-java/src/test/java/ai/sentryagent/idp/MockServer.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package ai.sentryagent.idp;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Lightweight in-process HTTP server for unit tests.
|
||||
* Uses the JDK's built-in {@code com.sun.net.httpserver.HttpServer}.
|
||||
*/
|
||||
public final class MockServer {
|
||||
|
||||
private final HttpServer server;
|
||||
private final int port;
|
||||
|
||||
public MockServer() throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
server.start();
|
||||
port = server.getAddress().getPort();
|
||||
}
|
||||
|
||||
/** Base URL of the mock server (e.g. {@code "http://localhost:PORT"}). */
|
||||
public String baseUrl() {
|
||||
return "http://localhost:" + port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler for an exact path.
|
||||
*
|
||||
* @param path URL path (e.g. {@code "/api/v1/agents"})
|
||||
* @param statusCode HTTP status code to return
|
||||
* @param responseBody JSON body to return (may be null for empty body)
|
||||
*/
|
||||
public void addHandler(String path, int statusCode, String responseBody) {
|
||||
server.createContext(path, new StaticHandler(statusCode, responseBody));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom handler for an exact path.
|
||||
*/
|
||||
public void addHandler(String path, HttpHandler handler) {
|
||||
server.createContext(path, handler);
|
||||
}
|
||||
|
||||
/** Stops the server. */
|
||||
public void stop() {
|
||||
server.stop(0);
|
||||
}
|
||||
|
||||
private static final class StaticHandler implements HttpHandler {
|
||||
private final int statusCode;
|
||||
private final byte[] body;
|
||||
|
||||
StaticHandler(int statusCode, String body) {
|
||||
this.statusCode = statusCode;
|
||||
this.body = body != null ? body.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(statusCode, body.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
sdk-java/src/test/java/ai/sentryagent/idp/TokenManagerTest.java
Normal file
102
sdk-java/src/test/java/ai/sentryagent/idp/TokenManagerTest.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package ai.sentryagent.idp;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TokenManagerTest {
|
||||
|
||||
private MockServer srv;
|
||||
private HttpClient httpClient;
|
||||
|
||||
private static final String TOKEN_BODY = """
|
||||
{"access_token":"eyJ.abc.def","token_type":"Bearer","expires_in":3600,"scope":"agents:read"}
|
||||
""";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
@Test
|
||||
void getToken_issuesToken() {
|
||||
srv.addHandler("/api/v1/token", 200, TOKEN_BODY);
|
||||
TokenManager tm = new TokenManager(srv.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
assertEquals("eyJ.abc.def", tm.getToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToken_cachesToken() {
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
srv.addHandler("/api/v1/token", exchange -> {
|
||||
calls.incrementAndGet();
|
||||
byte[] body = TOKEN_BODY.getBytes();
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.getResponseBody().close();
|
||||
});
|
||||
TokenManager tm = new TokenManager(srv.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
tm.getToken();
|
||||
tm.getToken();
|
||||
assertEquals(1, calls.get(), "Should only call the token endpoint once");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToken_authFailure_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/token", 401,
|
||||
"{\"error\":\"invalid_client\",\"error_description\":\"Bad credentials.\"}");
|
||||
TokenManager tm = new TokenManager(srv.baseUrl(), "cid", "bad-secret", "agents:read", httpClient);
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class, tm::getToken);
|
||||
assertEquals("invalid_client", ex.getCode());
|
||||
assertEquals(401, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearCache_forcesRefetch() {
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
srv.addHandler("/api/v1/token", exchange -> {
|
||||
calls.incrementAndGet();
|
||||
byte[] body = TOKEN_BODY.getBytes();
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.getResponseBody().close();
|
||||
});
|
||||
TokenManager tm = new TokenManager(srv.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
tm.getToken();
|
||||
tm.clearCache();
|
||||
tm.getToken();
|
||||
assertEquals(2, calls.get(), "Should call token endpoint again after clearCache");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getToken_threadSafe() throws InterruptedException {
|
||||
srv.addHandler("/api/v1/token", 200, TOKEN_BODY);
|
||||
TokenManager tm = new TokenManager(srv.baseUrl(), "cid", "secret", "agents:read", httpClient);
|
||||
|
||||
Thread[] threads = new Thread[10];
|
||||
String[] results = new String[10];
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
int idx = i;
|
||||
threads[idx] = new Thread(() -> results[idx] = tm.getToken());
|
||||
}
|
||||
for (Thread t : threads) t.start();
|
||||
for (Thread t : threads) t.join();
|
||||
|
||||
for (String result : results) {
|
||||
assertEquals("eyJ.abc.def", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ai.sentryagent.idp.services;
|
||||
|
||||
import ai.sentryagent.idp.AgentIdPException;
|
||||
import ai.sentryagent.idp.MockServer;
|
||||
import ai.sentryagent.idp.internal.HttpHelper;
|
||||
import ai.sentryagent.idp.models.*;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AgentRegistryClientTest {
|
||||
|
||||
private MockServer srv;
|
||||
private AgentRegistryClient client;
|
||||
|
||||
private static final String AGENT_JSON = """
|
||||
{"agentId":"uuid-1","email":"a@b.ai","agentType":"screener","version":"1.0.0",
|
||||
"capabilities":["read"],"owner":"team","deploymentEnv":"production",
|
||||
"status":"active","createdAt":"2026-01-01T00:00:00Z","updatedAt":"2026-01-01T00:00:00Z"}
|
||||
""";
|
||||
|
||||
private static final String PAGINATED_AGENTS = """
|
||||
{"data":[%s],"total":1,"page":1,"limit":20}
|
||||
""".formatted(AGENT_JSON.strip());
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
HttpHelper httpHelper = new HttpHelper(httpClient);
|
||||
client = new AgentRegistryClient(srv.baseUrl(), () -> "test-token", httpHelper);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
@Test
|
||||
void registerAgent_returns201() {
|
||||
srv.addHandler("/api/v1/agents", 201, AGENT_JSON);
|
||||
Agent agent = client.registerAgent(RegisterAgentRequest.builder()
|
||||
.email("a@b.ai").agentType("screener").version("1.0.0")
|
||||
.capabilities(List.of("read")).owner("team").deploymentEnv("production")
|
||||
.build());
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
assertEquals("screener", agent.getAgentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAgents_returnsPaginated() {
|
||||
srv.addHandler("/api/v1/agents", 200, PAGINATED_AGENTS);
|
||||
PaginatedAgents result = client.listAgents(null);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals("uuid-1", result.getData().get(0).getAgentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAgent_returnsAgent() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
Agent agent = client.getAgent("uuid-1");
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAgent_notFound_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/agents/bad-id", 404,
|
||||
"{\"code\":\"AgentNotFoundError\",\"message\":\"Not found.\"}");
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class, () -> client.getAgent("bad-id"));
|
||||
assertEquals("AgentNotFoundError", ex.getCode());
|
||||
assertEquals(404, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAgent_returnsUpdated() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
Agent agent = client.updateAgent("uuid-1",
|
||||
UpdateAgentRequest.builder().version("2.0.0").build());
|
||||
assertNotNull(agent);
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void decommissionAgent_returns204() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 204, null);
|
||||
assertDoesNotThrow(() -> client.decommissionAgent("uuid-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerAgentAsync_returnsCompletableFuture() throws Exception {
|
||||
srv.addHandler("/api/v1/agents", 201, AGENT_JSON);
|
||||
Agent agent = client.registerAgentAsync(RegisterAgentRequest.builder()
|
||||
.email("a@b.ai").agentType("screener").version("1.0.0")
|
||||
.capabilities(List.of("read")).owner("team").deploymentEnv("production")
|
||||
.build()).get();
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAgentAsync_returnsCompletableFuture() throws Exception {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
Agent agent = client.getAgentAsync("uuid-1").get();
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAgentsAsync_withParams() throws Exception {
|
||||
srv.addHandler("/api/v1/agents", 200, PAGINATED_AGENTS);
|
||||
PaginatedAgents result = client.listAgentsAsync(
|
||||
ListAgentsParams.builder().status("active").page(1).limit(20).build()
|
||||
).get();
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void decommissionAgentAsync_completesSuccessfully() throws Exception {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 204, null);
|
||||
assertDoesNotThrow(() -> client.decommissionAgentAsync("uuid-1").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAgentAsync_returnsCompletableFuture() throws Exception {
|
||||
srv.addHandler("/api/v1/agents/uuid-1", 200, AGENT_JSON);
|
||||
Agent agent = client.updateAgentAsync("uuid-1",
|
||||
UpdateAgentRequest.builder().version("2.0.0").build()).get();
|
||||
assertEquals("uuid-1", agent.getAgentId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package ai.sentryagent.idp.services;
|
||||
|
||||
import ai.sentryagent.idp.AgentIdPException;
|
||||
import ai.sentryagent.idp.MockServer;
|
||||
import ai.sentryagent.idp.internal.HttpHelper;
|
||||
import ai.sentryagent.idp.models.*;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AuditClientTest {
|
||||
|
||||
private MockServer srv;
|
||||
private AuditClient client;
|
||||
|
||||
private static final String AUDIT_EVENT = """
|
||||
{"eventId":"ev-1","agentId":"uuid-1","action":"token.issued","outcome":"success",
|
||||
"ipAddress":"1.2.3.4","userAgent":"curl","metadata":{},"timestamp":"2026-01-01T00:00:00Z"}
|
||||
""";
|
||||
|
||||
private static final String PAGINATED_AUDIT = """
|
||||
{"data":[%s],"total":1,"page":1,"limit":20}
|
||||
""".formatted(AUDIT_EVENT.strip());
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
client = new AuditClient(srv.baseUrl(), () -> "test-token", new HttpHelper(httpClient));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
@Test
|
||||
void queryAuditLog_returnsPaginated() {
|
||||
srv.addHandler("/api/v1/audit", 200, PAGINATED_AUDIT);
|
||||
PaginatedAuditEvents result = client.queryAuditLog(null);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals("ev-1", result.getData().get(0).getEventId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAuditLog_withParams() {
|
||||
srv.addHandler("/api/v1/audit", 200, PAGINATED_AUDIT);
|
||||
PaginatedAuditEvents result = client.queryAuditLog(
|
||||
QueryAuditParams.builder()
|
||||
.agentId("uuid-1")
|
||||
.action("token.issued")
|
||||
.fromDate("2026-01-01")
|
||||
.build());
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAuditEvent_returnsEvent() {
|
||||
srv.addHandler("/api/v1/audit/ev-1", 200, AUDIT_EVENT);
|
||||
AuditEvent event = client.getAuditEvent("ev-1");
|
||||
assertEquals("ev-1", event.getEventId());
|
||||
assertEquals("token.issued", event.getAction());
|
||||
assertEquals("success", event.getOutcome());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAuditEvent_notFound_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/audit/bad-id", 404,
|
||||
"{\"code\":\"AuditEventNotFoundError\",\"message\":\"Event not found.\"}");
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class,
|
||||
() -> client.getAuditEvent("bad-id"));
|
||||
assertEquals("AuditEventNotFoundError", ex.getCode());
|
||||
assertEquals(404, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAuditLogAsync_returnsPaginated() throws Exception {
|
||||
srv.addHandler("/api/v1/audit", 200, PAGINATED_AUDIT);
|
||||
PaginatedAuditEvents result = client.queryAuditLogAsync(null).get();
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAuditEventAsync_returnsEvent() throws Exception {
|
||||
srv.addHandler("/api/v1/audit/ev-1", 200, AUDIT_EVENT);
|
||||
AuditEvent event = client.getAuditEventAsync("ev-1").get();
|
||||
assertEquals("ev-1", event.getEventId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package ai.sentryagent.idp.services;
|
||||
|
||||
import ai.sentryagent.idp.AgentIdPException;
|
||||
import ai.sentryagent.idp.MockServer;
|
||||
import ai.sentryagent.idp.internal.HttpHelper;
|
||||
import ai.sentryagent.idp.models.*;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class CredentialClientTest {
|
||||
|
||||
private MockServer srv;
|
||||
private CredentialClient client;
|
||||
|
||||
private static final String CRED_JSON = """
|
||||
{"credentialId":"cred-1","clientId":"uuid-1","status":"active",
|
||||
"createdAt":"2026-01-01T00:00:00Z","expiresAt":null,"revokedAt":null}
|
||||
""";
|
||||
|
||||
private static final String CRED_WITH_SECRET = """
|
||||
{"credentialId":"cred-1","clientId":"uuid-1","status":"active",
|
||||
"createdAt":"2026-01-01T00:00:00Z","expiresAt":null,"revokedAt":null,
|
||||
"clientSecret":"sk_live_abc"}
|
||||
""";
|
||||
|
||||
private static final String PAGINATED_CREDS = """
|
||||
{"data":[%s],"total":1,"page":1,"limit":20}
|
||||
""".formatted(CRED_JSON.strip());
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
client = new CredentialClient(srv.baseUrl(), () -> "test-token", new HttpHelper(httpClient));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
@Test
|
||||
void generateCredential_returnsSecret() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials", 201, CRED_WITH_SECRET);
|
||||
CredentialWithSecret cred = client.generateCredential("uuid-1");
|
||||
assertEquals("sk_live_abc", cred.getClientSecret());
|
||||
assertEquals("cred-1", cred.getCredentialId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listCredentials_returnsPaginated() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials", 200, PAGINATED_CREDS);
|
||||
PaginatedCredentials result = client.listCredentials("uuid-1", null, null);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals("cred-1", result.getData().get(0).getCredentialId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rotateCredential_returnsNewSecret() {
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials/cred-1/rotate", 200, CRED_WITH_SECRET);
|
||||
CredentialWithSecret cred = client.rotateCredential("uuid-1", "cred-1");
|
||||
assertEquals("sk_live_abc", cred.getClientSecret());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCredential_returnsRevoked() {
|
||||
String revoked = """
|
||||
{"credentialId":"cred-1","clientId":"uuid-1","status":"revoked",
|
||||
"createdAt":"2026-01-01T00:00:00Z","expiresAt":null,
|
||||
"revokedAt":"2026-01-02T00:00:00Z"}
|
||||
""";
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials/cred-1", 200, revoked);
|
||||
Credential cred = client.revokeCredential("uuid-1", "cred-1");
|
||||
assertEquals("revoked", cred.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateCredential_error_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/agents/bad/credentials", 404,
|
||||
"{\"code\":\"AgentNotFoundError\",\"message\":\"Not found.\"}");
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class,
|
||||
() -> client.generateCredential("bad"));
|
||||
assertEquals(404, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateCredentialAsync_returnsCompletableFuture() throws Exception {
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials", 201, CRED_WITH_SECRET);
|
||||
CredentialWithSecret cred = client.generateCredentialAsync("uuid-1").get();
|
||||
assertEquals("sk_live_abc", cred.getClientSecret());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rotateCredentialAsync_returnsCompletableFuture() throws Exception {
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials/cred-1/rotate", 200, CRED_WITH_SECRET);
|
||||
CredentialWithSecret cred = client.rotateCredentialAsync("uuid-1", "cred-1").get();
|
||||
assertEquals("sk_live_abc", cred.getClientSecret());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCredentialAsync_returnsCompletableFuture() throws Exception {
|
||||
String revoked = """
|
||||
{"credentialId":"cred-1","clientId":"uuid-1","status":"revoked",
|
||||
"createdAt":"2026-01-01T00:00:00Z","expiresAt":null,
|
||||
"revokedAt":"2026-01-02T00:00:00Z"}
|
||||
""";
|
||||
srv.addHandler("/api/v1/agents/uuid-1/credentials/cred-1", 200, revoked);
|
||||
Credential cred = client.revokeCredentialAsync("uuid-1", "cred-1").get();
|
||||
assertEquals("revoked", cred.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package ai.sentryagent.idp.services;
|
||||
|
||||
import ai.sentryagent.idp.AgentIdPException;
|
||||
import ai.sentryagent.idp.MockServer;
|
||||
import ai.sentryagent.idp.models.IntrospectResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TokenClientTest {
|
||||
|
||||
private MockServer srv;
|
||||
private TokenClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
srv = new MockServer();
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
client = new TokenClient(srv.baseUrl(), () -> "test-token", httpClient);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() { srv.stop(); }
|
||||
|
||||
@Test
|
||||
void introspectToken_active() {
|
||||
srv.addHandler("/api/v1/token/introspect", 200,
|
||||
"{\"active\":true,\"sub\":\"uuid-1\",\"exp\":9999999999}");
|
||||
IntrospectResponse result = client.introspectToken("some-token");
|
||||
assertTrue(result.isActive());
|
||||
assertEquals("uuid-1", result.getSub());
|
||||
}
|
||||
|
||||
@Test
|
||||
void introspectToken_inactive() {
|
||||
srv.addHandler("/api/v1/token/introspect", 200, "{\"active\":false}");
|
||||
IntrospectResponse result = client.introspectToken("expired-token");
|
||||
assertFalse(result.isActive());
|
||||
assertNull(result.getSub());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeToken_succeeds() {
|
||||
srv.addHandler("/api/v1/token/revoke", 200, "{}");
|
||||
assertDoesNotThrow(() -> client.revokeToken("some-token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void introspectToken_error_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/token/introspect", 401,
|
||||
"{\"code\":\"UnauthorizedError\",\"message\":\"Invalid token.\"}");
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class,
|
||||
() -> client.introspectToken("bad-token"));
|
||||
assertEquals(401, ex.getHttpStatus());
|
||||
assertEquals("UnauthorizedError", ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeToken_error_throwsAgentIdPException() {
|
||||
srv.addHandler("/api/v1/token/revoke", 401,
|
||||
"{\"code\":\"UnauthorizedError\",\"message\":\"Invalid token.\"}");
|
||||
AgentIdPException ex = assertThrows(AgentIdPException.class,
|
||||
() -> client.revokeToken("bad-token"));
|
||||
assertEquals(401, ex.getHttpStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void introspectTokenAsync_active() throws Exception {
|
||||
srv.addHandler("/api/v1/token/introspect", 200,
|
||||
"{\"active\":true,\"sub\":\"uuid-1\",\"exp\":9999999999}");
|
||||
IntrospectResponse result = client.introspectTokenAsync("some-token").get();
|
||||
assertTrue(result.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeTokenAsync_succeeds() throws Exception {
|
||||
srv.addHandler("/api/v1/token/revoke", 200, "{}");
|
||||
assertDoesNotThrow(() -> client.revokeTokenAsync("some-token").get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user