package com.baiyue.integration.provider; import com.baiyue.integration.config.IntegrationProperties; import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest; import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ZimTrackingClientTest { private HttpServer server; @AfterEach void stopServer() { if (server != null) server.stop(0); } @Test void usesOAuthTokenCacheAndContainerOnlyTracingPath() throws Exception { Credentials credentials = Credentials.random(); String accessToken = UUID.randomUUID().toString(); AtomicInteger authCalls = new AtomicInteger(); AtomicReference authForm = new AtomicReference<>(); AtomicReference tracingPath = new AtomicReference<>(); server = server(); server.createContext("/authorize/v1", exchange -> { authCalls.incrementAndGet(); authForm.set(body(exchange)); respond(exchange, 200, tokenBody(accessToken)); }); server.createContext("/tracing/v2", exchange -> { tracingPath.set(exchange.getRequestURI().getPath()); assertThat(exchange.getRequestHeaders().getFirst("Authorization")).isEqualTo("bearer " + accessToken); assertThat(exchange.getRequestHeaders().getFirst("Ocp-Apim-Subscription-Key")) .isEqualTo(credentials.subscriptionKey()); respond(exchange, 200, successBody()); }); server.start(); ZimTrackingClient client = client(credentials); client.query(query()); client.query(query()); assertThat(authCalls).hasValue(1); assertThat(authForm.get()).contains("grant_type=client_credentials", "scope=tracing") .contains("client_id=", "client_secret="); assertThat(tracingPath.get()).isEqualTo("/tracing/v2/ZCSU7881970"); } @Test void unauthorizedRefreshesTokenAndRetriesTracingExactlyOnce() throws Exception { Credentials credentials = Credentials.random(); String firstToken = UUID.randomUUID().toString(); String secondToken = UUID.randomUUID().toString(); AtomicInteger authCalls = new AtomicInteger(); AtomicInteger tracingCalls = new AtomicInteger(); server = server(); server.createContext("/authorize/v1", exchange -> { int call = authCalls.incrementAndGet(); respond(exchange, 200, tokenBody(call == 1 ? firstToken : secondToken)); }); server.createContext("/tracing/v2", exchange -> { int call = tracingCalls.incrementAndGet(); respond(exchange, call == 1 ? 401 : 200, call == 1 ? "{}" : successBody()); }); server.start(); client(credentials).query(query()); assertThat(authCalls).hasValue(2); assertThat(tracingCalls).hasValue(2); } @Test void forbiddenIsManualAuthorizationFailureAndDoesNotEchoSecrets() throws Exception { Credentials credentials = Credentials.random(); server = server(); server.createContext("/authorize/v1", exchange -> respond(exchange, 200, tokenBody(UUID.randomUUID().toString()))); server.createContext("/tracing/v2", exchange -> respond(exchange, 403, "forbidden")); server.start(); assertThatThrownBy(() -> client(credentials).query(query())) .isInstanceOfSatisfying(ZimTrackingClient.ZimTrackingException.class, value -> { assertThat(value.outcome()).isEqualTo("MANUAL_REQUIRED"); assertThat(value.code()).isEqualTo("ZIM_HTTP_403"); assertThat(value.getMessage()).doesNotContain(credentials.clientSecret()) .doesNotContain(credentials.subscriptionKey()).doesNotContain("ZIM_NOT_FOUND"); }); } @Test void emptyOrMissingContainerListRequiresManualHandling() throws Exception { Credentials credentials = Credentials.random(); AtomicReference businessBody = new AtomicReference<>( "{\"bkBlDetails\":{\"consContainerList\":[]}}"); server = server(); server.createContext("/authorize/v1", exchange -> respond(exchange, 200, tokenBody(UUID.randomUUID().toString()))); server.createContext("/tracing/v2", exchange -> respond(exchange, 200, businessBody.get())); server.start(); ZimTrackingClient client = client(credentials); assertThatThrownBy(() -> client.query(query())).isInstanceOfSatisfying( ZimTrackingClient.ZimTrackingException.class, value -> assertThat(value.code()).isEqualTo("ZIM_NOT_FOUND")); businessBody.set("{}"); assertThatThrownBy(() -> client.query(query())).isInstanceOfSatisfying( ZimTrackingClient.ZimTrackingException.class, value -> assertThat(value.code()).isEqualTo("ZIM_EMPTY_RESULT")); } private HttpServer server() throws IOException { return HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); } private QueryRequest query() { return new QueryRequest("ZIM", "CONTAINER_NO", "ZCSU7881970"); } private ZimTrackingClient client(Credentials credentials) { String root = "http://127.0.0.1:" + server.getAddress().getPort(); var zim = new IntegrationProperties.CarrierSchedule.Zim(true, URI.create(root + "/authorize/v1"), URI.create(root + "/tracing/v2"), credentials.clientId(), credentials.clientSecret(), credentials.subscriptionKey(), Duration.ofSeconds(2), Duration.ofSeconds(2), 10); ZimTrackingClient client = new ZimTrackingClient(); ReflectionTestUtils.setField(client, "properties", new IntegrationProperties(null, null, new IntegrationProperties.CarrierSchedule(null, null, null, zim, null))); ReflectionTestUtils.setField(client, "objectMapper", new ObjectMapper()); client.initialize(); return client; } private static String body(HttpExchange exchange) throws IOException { return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); } private static String tokenBody(String token) { return "{\"access_token\":\"" + token + "\",\"expires_in\":3600}"; } private static String successBody() { return "{\"bkBlDetails\":{\"consContainerList\":[{\"containerNumber\":\" ZCSU7881970 \"}]}}"; } private static void respond(HttpExchange exchange, int status, String body) throws IOException { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8"); exchange.sendResponseHeaders(status, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); } private record Credentials(String clientId, String clientSecret, String subscriptionKey) { static Credentials random() { return new Credentials(UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString()); } } }