package com.baiyue.common.audit; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.Message; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.support.TransactionOperations; import java.time.Instant; import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SpringJUnitConfig(ReliableAuditDeliveryTest.Config.class) class ReliableAuditDeliveryTest { @MockBean private AuditOutboxRepository repository; @MockBean private RocketMQTemplate rocketMQ; @MockBean private TransactionOperations transactions; @Resource private OutboxAuditSink sink; @Resource private AuditOutboxPublisher publisher; @Test void storesEventBeforeDelivery() { AuditEvent event = event("audit-1"); org.mockito.Mockito.doAnswer(invocation -> { java.util.function.Consumer callback = invocation.getArgument(0); ((java.util.function.Consumer) callback).accept(null); return null; }).when(transactions).executeWithoutResult(any()); sink.publish(event); verify(repository).append(eq(event), anyString()); } @Test void marksDeliveredMessageSent() { when(repository.publishableIds(50)).thenReturn(List.of("audit-1")); when(repository.claim("audit-1", "test-owner", 60)).thenReturn(true); when(repository.find("audit-1")) .thenReturn(new AuditOutboxRepository.OutboxRow("audit-1", "{}", 1)); publisher.publish(); verify(rocketMQ).syncSend(anyString(), any(Message.class), anyLong()); verify(repository).sent("audit-1", "test-owner"); } @Test void schedulesRetryAfterMqFailure() { when(repository.publishableIds(50)).thenReturn(List.of("audit-2")); when(repository.claim("audit-2", "test-owner", 60)).thenReturn(true); when(repository.find("audit-2")) .thenReturn(new AuditOutboxRepository.OutboxRow("audit-2", "{}", 1)); doThrow(new IllegalStateException("broker unavailable")) .when(rocketMQ).syncSend(anyString(), any(Message.class), anyLong()); publisher.publish(); verify(repository).failed("audit-2", "test-owner", 5, 5, "broker unavailable"); } private static AuditEvent event(String id) { return new AuditEvent(id, "ROLE_UPDATE", "ROLE", "1", "修改角色", "request-1", "trace-1", 1L, 1L, 1L, "admin", "PLATFORM", "WEB", "PUT", "/roles/1", "127.0.0.1", 10, true, null, null, null, null, Instant.parse("2026-08-05T00:00:00Z"), "iam-service"); } @Configuration static class Config { @Bean ObjectMapper objectMapper() { return new ObjectMapper().findAndRegisterModules(); } @Bean AuditProperties properties() { AuditProperties value = new AuditProperties(); value.getOutbox().setBatchSize(50); value.getOutbox().setLeaseSeconds(60); value.getOutbox().setMaxAttempts(5); value.getRocketmq().setTopic("test-baiyue-audit-event-v1"); return value; } @Bean OutboxAuditSink sink(AuditOutboxRepository repository, ObjectMapper json, TransactionOperations transactions) { return new OutboxAuditSink(repository, json, transactions); } @Bean AuditOutboxPublisher publisher(AuditOutboxRepository repository, RocketMQTemplate rocketMQ, AuditProperties properties) { return new AuditOutboxPublisher(repository, rocketMQ, properties, "test-owner"); } } }