diff --git a/src/test/java/com/stripe/net/RequestTelemetryTest.java b/src/test/java/com/stripe/net/RequestTelemetryTest.java new file mode 100644 index 00000000000..5bdc14f9140 --- /dev/null +++ b/src/test/java/com/stripe/net/RequestTelemetryTest.java @@ -0,0 +1,220 @@ +package com.stripe.net; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.stripe.Stripe; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link RequestTelemetry}. + * + *

These test the class directly, verifying enqueue/poll semantics, telemetry toggle behavior, + * queue bounds, and JSON payload structure — without going through the HTTP layer. + */ +public class RequestTelemetryTest { + private boolean originalTelemetry; + private RequestTelemetry telemetry; + + @BeforeEach + public void setUp() { + originalTelemetry = Stripe.enableTelemetry; + Stripe.enableTelemetry = true; + telemetry = new RequestTelemetry(); + // Drain any leftover metrics from prior tests (shared static queue) + while (telemetry.pollPayload().isPresent()) { + // discard + } + } + + @AfterEach + public void tearDown() { + Stripe.enableTelemetry = originalTelemetry; + } + + // ---- basic enqueue / poll ---- + + @Test + public void testPollReturnsEmptyWhenNothingEnqueued() { + Optional payload = telemetry.pollPayload(); + assertFalse(payload.isPresent(), "Expected empty payload when no metrics enqueued"); + } + + @Test + public void testEnqueueAndPollReturnsPayload() { + StripeResponse response = buildResponse("req_test_1"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(42), null); + + Optional payload = telemetry.pollPayload(); + assertTrue(payload.isPresent(), "Expected a telemetry payload after enqueue"); + + JsonObject root = JsonParser.parseString(payload.get()).getAsJsonObject(); + JsonObject metrics = root.getAsJsonObject("last_request_metrics"); + assertEquals("req_test_1", metrics.get("request_id").getAsString()); + assertEquals(42L, metrics.get("request_duration_ms").getAsLong()); + assertFalse(metrics.has("usage"), "usage should be absent when null"); + } + + @Test + public void testPollDrainsQueue() { + StripeResponse response = buildResponse("req_drain"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(10), null); + + assertTrue(telemetry.pollPayload().isPresent()); + assertFalse(telemetry.pollPayload().isPresent(), "Second poll should return empty"); + } + + // ---- usage field ---- + + @Test + public void testUsageIncludedInPayload() { + StripeResponse response = buildResponse("req_usage"); + List usage = Arrays.asList("llm", "streaming"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(100), usage); + + Optional payload = telemetry.pollPayload(); + assertTrue(payload.isPresent()); + + JsonObject metrics = + JsonParser.parseString(payload.get()) + .getAsJsonObject() + .getAsJsonObject("last_request_metrics"); + assertEquals("req_usage", metrics.get("request_id").getAsString()); + assertTrue(metrics.has("usage"), "usage field should be present"); + assertEquals(2, metrics.getAsJsonArray("usage").size()); + } + + @Test + public void testEmptyUsageListTreatedAsNull() { + StripeResponse response = buildResponse("req_empty_usage"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(50), Collections.emptyList()); + + Optional payload = telemetry.pollPayload(); + assertTrue(payload.isPresent()); + + JsonObject metrics = + JsonParser.parseString(payload.get()) + .getAsJsonObject() + .getAsJsonObject("last_request_metrics"); + // The class normalizes empty list to null + assertFalse(metrics.has("usage"), "Empty usage list should be normalized to absent"); + } + + // ---- telemetry toggle ---- + + @Test + public void testEnqueueIgnoredWhenTelemetryDisabled() { + Stripe.enableTelemetry = false; + StripeResponse response = buildResponse("req_disabled"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(10), null); + + // Re-enable so pollPayload doesn't short-circuit + Stripe.enableTelemetry = true; + assertFalse( + telemetry.pollPayload().isPresent(), + "Nothing should be enqueued when telemetry is disabled"); + } + + @Test + public void testPollReturnsEmptyWhenTelemetryDisabledAtPollTime() { + // Enqueue while enabled + StripeResponse response = buildResponse("req_toggle"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(10), null); + + // Disable before polling + Stripe.enableTelemetry = false; + assertFalse( + telemetry.pollPayload().isPresent(), + "pollPayload should return empty when telemetry is disabled at poll time"); + + // Re-enable — the metric was consumed (polled off queue) even though it wasn't returned + Stripe.enableTelemetry = true; + assertFalse( + telemetry.pollPayload().isPresent(), + "Metric should have been consumed even when telemetry was disabled"); + } + + // ---- null request ID ---- + + @Test + public void testEnqueueIgnoredWhenRequestIdIsNull() { + StripeResponse response = buildResponse(null); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(10), null); + + assertFalse( + telemetry.pollPayload().isPresent(), + "Nothing should be enqueued when requestId is null"); + } + + // ---- queue capacity ---- + + @Test + public void testQueueBoundedAtMaxSize() { + // MAX_REQUEST_METRICS_QUEUE_SIZE is 100 + for (int i = 0; i < 110; i++) { + StripeResponse response = buildResponse("req_" + i); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(1), null); + } + + int count = 0; + while (telemetry.pollPayload().isPresent()) { + count++; + } + assertEquals(100, count, "Queue should be bounded at 100 entries"); + } + + // ---- deprecated getHeaderValue ---- + + @Test + public void testGetHeaderValueReturnsEmptyWhenHeaderAlreadyPresent() { + StripeResponse response = buildResponse("req_dup_header"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(10), null); + + HttpHeaders headers = + HttpHeaders.of( + Collections.singletonMap( + RequestTelemetry.HEADER_NAME, Collections.singletonList("existing"))); + + @SuppressWarnings("deprecation") + Optional result = telemetry.getHeaderValue(headers); + assertFalse( + result.isPresent(), + "getHeaderValue should return empty when header is already present"); + } + + @Test + public void testGetHeaderValueReturnsTelemetryWhenHeaderAbsent() { + StripeResponse response = buildResponse("req_no_header"); + telemetry.maybeEnqueueMetrics(response, Duration.ofMillis(55), null); + + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap()); + + @SuppressWarnings("deprecation") + Optional result = telemetry.getHeaderValue(headers); + assertTrue(result.isPresent(), "getHeaderValue should return telemetry when header is absent"); + + JsonObject metrics = + JsonParser.parseString(result.get()) + .getAsJsonObject() + .getAsJsonObject("last_request_metrics"); + assertEquals("req_no_header", metrics.get("request_id").getAsString()); + } + + // ---- helpers ---- + + private static StripeResponse buildResponse(String requestId) { + java.util.Map> headerMap = new java.util.HashMap<>(); + if (requestId != null) { + headerMap.put("Request-Id", Collections.singletonList(requestId)); + } + return new StripeResponse(200, HttpHeaders.of(headerMap), "{}"); + } +}