优化
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.iam.client.CustomerDimensionRuleClient;
|
||||
import com.baiyue.iam.model.CustomerDimensionInitializationTask;
|
||||
import com.baiyue.iam.repository.CustomerDimensionInitializationRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomerDimensionInitializationRelayTest {
|
||||
@Mock private CustomerDimensionInitializationRepository repository;
|
||||
@Mock private CustomerDimensionRuleClient client;
|
||||
@InjectMocks private CustomerDimensionInitializationRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void configure() {
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "maxAttempts", 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulBackgroundInitializationMarksTenantReady() {
|
||||
CustomerDimensionInitializationTask task = task("INITIALIZE", 0);
|
||||
when(repository.claim(anyLong(), anyString(), any(LocalDateTime.class))).thenReturn(task);
|
||||
when(client.initialize(anyString(), any())).thenReturn(ApiResponse.success(
|
||||
new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
|
||||
|
||||
relay.deliver(10001L);
|
||||
|
||||
verify(repository).markReady(task, workerId(), 91L);
|
||||
verify(repository, never()).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
anyString(), any(Boolean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientFailureStaysPendingWithDiagnosticAndBackoff() {
|
||||
CustomerDimensionInitializationTask task = task("INITIALIZE", 0);
|
||||
when(repository.claim(anyLong(), anyString(), any(LocalDateTime.class))).thenReturn(task);
|
||||
when(client.initialize(anyString(), any())).thenThrow(new RuntimeException("read timeout"));
|
||||
|
||||
relay.deliver(10001L);
|
||||
|
||||
ArgumentCaptor<Boolean> exhausted = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(repository).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
anyString(), exhausted.capture());
|
||||
assertFalse(exhausted.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void permanentRemoteRejectionBecomesFailedWithoutDefaultSuccess() {
|
||||
CustomerDimensionInitializationTask task = task("INITIALIZE", 0);
|
||||
when(repository.claim(anyLong(), anyString(), any(LocalDateTime.class))).thenReturn(task);
|
||||
when(client.initialize(anyString(), any())).thenReturn(ApiResponse.failure(
|
||||
ResultCode.CONFLICT, "invalid customer", "request-1", "trace-1"));
|
||||
|
||||
relay.deliver(10001L);
|
||||
|
||||
ArgumentCaptor<Boolean> exhausted = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(repository).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
anyString(), exhausted.capture());
|
||||
assertTrue(exhausted.getValue());
|
||||
verify(repository, never()).markReady(any(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationIsAlsoDurableAndUsesTheStableKey() {
|
||||
CustomerDimensionInitializationTask task = task("COMPENSATE", 0);
|
||||
when(repository.claim(anyLong(), anyString(), any(LocalDateTime.class))).thenReturn(task);
|
||||
when(client.compensate("internal-token", 10001L, "iam-customer-create:10001"))
|
||||
.thenReturn(ApiResponse.success(null));
|
||||
|
||||
relay.deliver(10001L);
|
||||
|
||||
verify(repository).markCancelled(task, workerId());
|
||||
}
|
||||
|
||||
private CustomerDimensionInitializationTask task(String action, int attempts) {
|
||||
return new CustomerDimensionInitializationTask(10001L, 9001L, "iam-customer-create:10001",
|
||||
action, "SENDING", attempts, 3);
|
||||
}
|
||||
|
||||
private String workerId() {
|
||||
return (String) ReflectionTestUtils.getField(relay, "workerId");
|
||||
}
|
||||
}
|
||||
+36
-113
@@ -1,138 +1,61 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.client.CustomerDimensionRuleClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import com.baiyue.iam.repository.CustomerDimensionInitializationRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(CustomerDimensionRuleInitializationService.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomerDimensionRuleInitializationServiceTest {
|
||||
@MockBean private CustomerDimensionRuleClient client;
|
||||
@Resource private CustomerDimensionRuleInitializationService service;
|
||||
|
||||
@AfterEach
|
||||
void clearSynchronization() {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
@Mock private CustomerDimensionInitializationRepository repository;
|
||||
@InjectMocks private CustomerDimensionRuleInitializationService service;
|
||||
|
||||
@Test
|
||||
void retriesOneAtomicFourBoxDefaultsCallWithAStableKey() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenThrow(new RuntimeException("timeout"))
|
||||
.thenReturn(ApiResponse.failure(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"temporarily unavailable", "request-1", "trace-1"))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
|
||||
|
||||
void customerCreationOnlyPersistsBackgroundIntentWithStableKey() {
|
||||
var result = service.initialize(10001L, 9001L);
|
||||
|
||||
assertFalse(result.created());
|
||||
ArgumentCaptor<CustomerDimensionRuleClient.InitializationRequest> requests =
|
||||
ArgumentCaptor.forClass(CustomerDimensionRuleClient.InitializationRequest.class);
|
||||
verify(client, times(3)).initialize(anyString(), requests.capture());
|
||||
assertEquals(List.of("iam-customer-create:10001", "iam-customer-create:10001",
|
||||
"iam-customer-create:10001"),
|
||||
requests.getAllValues().stream().map(
|
||||
CustomerDimensionRuleClient.InitializationRequest::initializationKey).toList());
|
||||
assertEquals(List.of(9001L, 9001L, 9001L), requests.getAllValues().stream()
|
||||
.map(CustomerDimensionRuleClient.InitializationRequest::operatorId).toList());
|
||||
assertEquals(List.of(10001L, 10001L, 10001L), requests.getAllValues().stream()
|
||||
.map(CustomerDimensionRuleClient.InitializationRequest::tenantId).toList());
|
||||
assertEquals("PENDING", result.status());
|
||||
assertEquals("iam-customer-create:10001", result.initializationKey());
|
||||
verify(repository).enqueue(10001L, 9001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsServiceUnavailableWhenFourBoxDefaultsFailThreeTimesAndTriesCompensation() {
|
||||
when(client.initialize(anyString(), any())).thenThrow(new RuntimeException("timeout"));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
BizException failure = assertThrows(BizException.class, () -> service.initialize(10001L, 9001L));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
|
||||
verify(client, times(3)).initialize(anyString(), any());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRollbackCompensatesFourBoxDefaultsCreatedByThisRegistration() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
var result = service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
assertTrue(result.created());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRollbackKeepsFourBoxDefaultsThatAlreadyExisted() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
verify(client, never()).compensate(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseTimeoutThenIdempotentFourBoxSuccessStillCompensatesOnLocalRollback() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenThrow(new RuntimeException("response timeout"))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
verify(client, times(2)).initialize(anyString(), any());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void committedClientRegistrationCanBeCompensatedWithItsDeterministicKey() {
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
void committedRegistrationCompensationUsesTheSameStableKey() {
|
||||
service.compensateRegistration(10001L);
|
||||
verify(repository).requestCompensation(10001L, 0L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
@Test
|
||||
void failedTaskCanBeManuallyRequeued() {
|
||||
when(repository.manualRetry(10001L)).thenReturn(true);
|
||||
service.retry(10001L);
|
||||
verify(repository).manualRetry(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonFailedTaskCannotBeSilentlyRequeued() {
|
||||
when(repository.manualRetry(10001L)).thenReturn(false);
|
||||
BizException failure = assertThrows(BizException.class, () -> service.retry(10001L));
|
||||
assertEquals(ResultCode.CONFLICT, failure.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dimensionsDependentFlowFailsClosedUntilReady() {
|
||||
BizException pending = assertThrows(BizException.class, () -> service.requireReady("PENDING"));
|
||||
BizException failed = assertThrows(BizException.class, () -> service.requireReady("FAILED"));
|
||||
assertEquals(ResultCode.CONFLICT, pending.getResultCode());
|
||||
assertEquals(ResultCode.CONFLICT, failed.getResultCode());
|
||||
service.requireReady("READY");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user