package com.baiyue.lastleg.client; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.event.*; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import java.time.Instant; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; class DirectEventClientTest { private final FirstlegEventClient firstleg = mock(FirstlegEventClient.class); private final FinanceBusinessEventClient finance = mock(FinanceBusinessEventClient.class); private final DirectEventClient client = new DirectEventClient(firstleg, finance); @Test void walletIsSentOnlyToFinance() { var event = event(EventTypes.WALLET_COMMAND_REQUESTED); var ack = ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.APPLIED)); when(finance.wallet("token", event)).thenReturn(ack); assertThat(client.receive("token", event)).isSameAs(ack); verify(finance).wallet("token", event); verifyNoMoreInteractions(finance); verifyNoInteractions(firstleg); } @Test void feeIsSentOnlyToFinance() { var event = event(EventTypes.BUSINESS_FEE_SOURCE_CHANGED); when(finance.fee("token", event)).thenReturn(ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.APPLIED))); client.receive("token", event); verify(finance).fee("token", event); verifyNoMoreInteractions(finance); verifyNoInteractions(firstleg); } @Test void companyVehicleCostsUseTheirOwnFinanceEndpoint() { var event = event(EventTypes.COMPANY_VEHICLE_COST_SUBMITTED); var ack = ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.APPLIED)); when(finance.vehicleCost("token", event)).thenReturn(ack); assertThat(client.receive("token", event)).isSameAs(ack); verify(finance).vehicleCost("token", event); verifyNoMoreInteractions(finance); verifyNoInteractions(firstleg); } @Test void companyCostValidationConflictsArePermanentButDependencyFailuresRetry() { var event = event(EventTypes.COMPANY_VEHICLE_COST_SUBMITTED); for (String code : new String[]{"FIN_400", "FIN_404", "FIN_409", "FIN_503"}) { when(finance.vehicleCost("token", event)).thenReturn(new ApiResponse<>(code, "rejected", null, null, null, Instant.now())); assertThatThrownBy(() -> client.receive("token", event)) .isInstanceOf(com.baiyue.common.web.EventDeliveryFailure.class) .satisfies(error -> assertThat(com.baiyue.common.web.EventDeliveryFailure.isPermanent((Exception) error)).isEqualTo(!"FIN_503".equals(code))); } verifyNoInteractions(firstleg); } @Test void fulfillmentEventsAreSentOnlyToFirstleg() { for (String type : new String[]{EventTypes.OVERSEAS_CONTAINER_UNLOADED, EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED, EventTypes.OVERSEAS_DELIVERY_COMPLETED}) { var event = event(type); client.receive("token", event); verify(firstleg).receive("token", event); } verifyNoInteractions(finance); } @Test void unsupportedEventsHaveNoDefaultDestination() { assertThatThrownBy(() -> client.receive("token", event(EventTypes.WALLET_COMMAND_COMPLETED))) .isInstanceOf(com.baiyue.common.web.EventDeliveryFailure.class).hasMessageContaining("Unsupported outbound"); verifyNoInteractions(firstleg, finance); } @Test void declaredEndpointsMatchTheSharedRouteTable() throws Exception { assertEndpoint(FirstlegEventClient.class, "receive", "FIRSTLEG_SERVICE_URL", "firstleg", EventTypes.OVERSEAS_CONTAINER_UNLOADED); assertEndpoint(FinanceBusinessEventClient.class, "wallet", "FINANCE_SERVICE_URL", "finance", EventTypes.WALLET_COMMAND_REQUESTED); assertEndpoint(FinanceBusinessEventClient.class, "fee", "FINANCE_SERVICE_URL", "finance", EventTypes.BUSINESS_FEE_SOURCE_CHANGED); assertEndpoint(FinanceBusinessEventClient.class, "vehicleCost", "FINANCE_SERVICE_URL", "finance", EventTypes.COMPANY_VEHICLE_COST_SUBMITTED); } private void assertEndpoint(Class type, String method, String env, String property, String eventType) throws Exception { var route = EventDeliveryRoute.forEventType(eventType); var annotation = type.getAnnotation(FeignClient.class); assertThat(annotation.name()).isEqualTo(route.serviceName()); assertThat(annotation.url()).isEqualTo("${" + env + ":${baiyue.clients." + property + ".url:}}"); assertThat(type.getMethod(method, String.class, EventEnvelope.class).getAnnotation(PostMapping.class).value()) .containsExactly(route.path()); } private EventEnvelope event(String type) { var payload = new ObjectMapper().createObjectNode().put("businessNo", "test"); return new EventEnvelope("00000000-0000-4000-8000-000000000009", type, 1, Instant.now(), "1", "BUSINESS", "1", 1L, null, "trace", "correlation", null, payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))); } }