package com.baiyue.common.web; import feign.*; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.*; class EventDeliveryFeignConfigurationTest { interface Endpoint { @RequestLine("POST /events") String send(); } interface TypedEndpoint { @RequestLine("POST /events") com.baiyue.common.core.api.ApiResponse> send(); } @Test void wireCodeMustBeAStringAndAmbiguousJsonCannotBeAcknowledged() { for (String body : List.of("{\"code\":200}", "{\"code\":true}", "{\"code\":\"200\",\"code\":\"500\"}", "{\"code\":\"200\"} {}", "{}")) { var decoded = new AtomicInteger(); TypedEndpoint endpoint = Feign.builder() .decoder((response, type) -> { decoded.incrementAndGet(); return com.baiyue.common.core.api.ApiResponse.success(Map.of()); }) .addCapability(new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60)) .client((request, options) -> Response.builder().request(request).status(200).reason("OK").headers(Map.of()).body(body, StandardCharsets.UTF_8).build()) .target(TypedEndpoint.class, "http://event-consumer.invalid"); assertThatThrownBy(endpoint::send).isInstanceOf(feign.codec.DecodeException.class); assertThat(decoded.get()).isZero(); } } @Test void validStringCodePreservesBodyForTheConfiguredDecoder() { String body = "{\"code\":\"200\",\"data\":{\"outcome\":\"APPLIED\"}}"; TypedEndpoint endpoint = Feign.builder().decoder((response, type) -> { assertThat(new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8)).isEqualTo(body); return com.baiyue.common.core.api.ApiResponse.success(Map.of("outcome", "APPLIED")); }).addCapability(new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60)) .client((request, options) -> Response.builder().request(request).status(200).reason("OK").headers(Map.of()).body(body, StandardCharsets.UTF_8).build()) .target(TypedEndpoint.class, "http://event-consumer.invalid"); assertThat(endpoint.send().data()).containsEntry("outcome", "APPLIED"); } @Test void builtClientOverridesLegacyTimeoutAndRedirectSettings() { var capability = new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60); var observed = new AtomicReference(); Endpoint endpoint = Feign.builder().options(new Request.Options(1, 5000)) .addCapability(capability).client((request, options) -> { observed.set(options); return Response.builder().request(request).status(200).reason("OK") .headers(Map.of()).body("ok", StandardCharsets.UTF_8).build(); }).target(Endpoint.class, "http://event-consumer.invalid"); assertThat(endpoint.send()).isEqualTo("ok"); assertThat(observed.get().connectTimeoutMillis()).isEqualTo(3000); assertThat(observed.get().readTimeoutMillis()).isEqualTo(15000); assertThat(observed.get().isFollowRedirects()).isFalse(); } @Test void retryAfterResponseCannotTriggerHiddenHttpRetries() { var calls = new AtomicInteger(); Endpoint endpoint = Feign.builder().retryer(new Retryer.Default(1, 1, 3)) .addCapability(new EventDeliveryFeignConfiguration().eventDeliveryCapability(3000, 15000, 60)) .client((request, options) -> { calls.incrementAndGet(); return Response.builder().request(request).status(503).reason("Unavailable") .headers(Map.of("Retry-After", List.of("0"))).build(); }).target(Endpoint.class, "http://event-consumer.invalid"); assertThatThrownBy(endpoint::send).isInstanceOf(RetryableException.class); assertThat(calls.get()).isEqualTo(1); } @Test void invalidTimeoutsAndTooShortLeaseAreRejected() { var configuration = new EventDeliveryFeignConfiguration(); assertThatThrownBy(() -> configuration.eventDeliveryCapability(0, 15000, 60)).isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> configuration.eventDeliveryCapability(3000, 15000, 18)).isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> configuration.eventDeliveryCapability(3000, -1, 60)).isInstanceOf(IllegalArgumentException.class); } }