日志记录提交
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.audit.controller;
|
||||
|
||||
import com.baiyue.audit.service.AuditIngestService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringJUnitConfig(AuditIngestController.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=0123456789abcdef0123456789abcdef")
|
||||
class AuditIngestControllerTest {
|
||||
private static final String TOKEN = "0123456789abcdef0123456789abcdef";
|
||||
|
||||
@MockBean
|
||||
private AuditIngestService service;
|
||||
|
||||
@Resource
|
||||
private AuditIngestController controller;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidInternalToken() throws Exception {
|
||||
mockMvc.perform(post("/internal/audit/events")
|
||||
.header("X-Internal-Service-Token", "wrong-token")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.code").value("A0403"));
|
||||
|
||||
verify(service, never()).ingest(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsEventWithInternalToken() throws Exception {
|
||||
mockMvc.perform(post("/internal/audit/events")
|
||||
.header("X-Internal-Service-Token", TOKEN)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isAccepted())
|
||||
.andExpect(jsonPath("$.code").value("200"));
|
||||
|
||||
verify(service).ingest(any());
|
||||
}
|
||||
|
||||
private String validRequest() {
|
||||
return """
|
||||
{
|
||||
"eventType":"ROLE_UPDATE",
|
||||
"resourceType":"ROLE",
|
||||
"durationMs":0,
|
||||
"success":true,
|
||||
"occurredAt":"2026-08-05T00:00:00Z"
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.audit.controller;
|
||||
|
||||
import com.baiyue.audit.dto.AuditLogQuery;
|
||||
import com.baiyue.audit.dto.AuditLogResponses;
|
||||
import com.baiyue.audit.service.AuditLogQueryService;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(PlatformAuditLogController.class)
|
||||
class PlatformAuditLogControllerContractTest {
|
||||
@MockBean
|
||||
private AuditLogQueryService service;
|
||||
|
||||
@Resource
|
||||
private PlatformAuditLogController controller;
|
||||
|
||||
@Test
|
||||
void platformCanQueryLogList() {
|
||||
when(service.page(any(AuditLogQuery.class), org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(20L)))
|
||||
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
var response = controller.page(user("PLATFORM"), 1, 20, 10001L, 7L, "ROLE_UPDATE", "ROLE", "9",
|
||||
true, null, null, "管理员");
|
||||
|
||||
assertThat(response.successful()).isTrue();
|
||||
verify(service).page(new AuditLogQuery(10001L, 7L, "ROLE_UPDATE", "ROLE", "9", true,
|
||||
null, null, "管理员"), 1, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotReadPlatformLogs() {
|
||||
assertThatThrownBy(() -> controller.page(user("MERCHANT"), 1, 20, null, null, null, null,
|
||||
null, null, null, null, null)).isInstanceOf(BizException.class)
|
||||
.hasMessage("仅平台端可查看操作日志");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformCanReadLogDetail() {
|
||||
AuditLogResponses.Detail detail = new AuditLogResponses.Detail(8L, "audit-1", "ROLE_UPDATE", "ROLE", "9", null,
|
||||
null, null, null, null, 7L, "管理员", "PLATFORM", "WEB", null, null, null, 1L,
|
||||
true, null, null, "{}", "{}", "iam-service", null, null);
|
||||
when(service.detail(8L)).thenReturn(detail);
|
||||
|
||||
assertThat(controller.detail(user("PLATFORM"), 8L).data()).isEqualTo(detail);
|
||||
verify(service).detail(8L);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(String appCode) {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 1L, "管理员", appCode, "WEB", Set.of("platform:audit:list"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.baiyue.audit.mq;
|
||||
|
||||
import com.baiyue.audit.repository.AuditDeadLetterRepository;
|
||||
import com.baiyue.audit.service.AuditDeadLetterCompensator;
|
||||
import com.baiyue.audit.service.AuditIngestService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
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.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(classes = {
|
||||
AuditMqReliabilityTest.Config.class,
|
||||
AuditRocketMqConsumer.class,
|
||||
AuditDeadLetterConsumer.class,
|
||||
AuditDeadLetterCompensator.class
|
||||
})
|
||||
@TestPropertySource(properties = {
|
||||
"baiyue.audit.rocketmq.consumer-enabled=true",
|
||||
"baiyue.audit.rocketmq.dead-letter-consumer-enabled=true",
|
||||
"baiyue.audit.rocketmq.dead-letter-compensation-enabled=true",
|
||||
"baiyue.audit.rocketmq.topic=test-audit",
|
||||
"baiyue.audit.rocketmq.consumer-group=test-audit-consumer",
|
||||
"baiyue.audit.rocketmq.dead-letter-topic=%DLQ%test-audit-consumer",
|
||||
"baiyue.audit.rocketmq.dead-letter-group=test-audit-dlq",
|
||||
"spring.application.name=audit-service-test"
|
||||
})
|
||||
class AuditMqReliabilityTest {
|
||||
private static final String PAYLOAD = """
|
||||
{"eventId":"event-1","eventType":"ROLE_UPDATE","resourceType":"ROLE",
|
||||
"durationMs":10,"success":true,"occurredAt":"2026-08-05T00:00:00Z"}
|
||||
""";
|
||||
|
||||
@MockBean
|
||||
private AuditIngestService service;
|
||||
@MockBean
|
||||
private AuditDeadLetterRepository repository;
|
||||
@Resource
|
||||
private AuditRocketMqConsumer consumer;
|
||||
@Resource
|
||||
private AuditDeadLetterConsumer deadLetterConsumer;
|
||||
@Resource
|
||||
private AuditDeadLetterCompensator compensator;
|
||||
|
||||
@Test
|
||||
void consumesAuditEvent() {
|
||||
consumer.onMessage(PAYLOAD);
|
||||
|
||||
verify(service).ingest(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidPayload() {
|
||||
assertThatThrownBy(() -> consumer.onMessage("not-json"))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void storesDeadLetter() {
|
||||
deadLetterConsumer.onMessage(PAYLOAD);
|
||||
|
||||
verify(repository).save("event-1", PAYLOAD);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensatesDeadLetter() {
|
||||
when(repository.pendingIds(50)).thenReturn(java.util.List.of(7L));
|
||||
when(repository.claim(eq(7L), anyString(), eq(60))).thenReturn(true);
|
||||
when(repository.find(7L)).thenReturn(
|
||||
new AuditDeadLetterRepository.DeadRow(7L, "event-1", PAYLOAD, 1));
|
||||
|
||||
compensator.compensate();
|
||||
|
||||
verify(service).ingest(any());
|
||||
verify(repository).processed(eq(7L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void retriesFailedCompensation() {
|
||||
when(repository.pendingIds(50)).thenReturn(java.util.List.of(8L));
|
||||
when(repository.claim(eq(8L), anyString(), eq(60))).thenReturn(true);
|
||||
when(repository.find(8L)).thenReturn(
|
||||
new AuditDeadLetterRepository.DeadRow(8L, "event-2", "not-json", 1));
|
||||
|
||||
compensator.compensate();
|
||||
|
||||
verify(repository).failed(eq(8L), anyString(), eq(5), anyString());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.audit.service;
|
||||
|
||||
import com.baiyue.audit.dto.AuditIngestRequest;
|
||||
import com.baiyue.audit.repository.AuditEventRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(AuditIngestService.class)
|
||||
class AuditIngestIdempotencyTest {
|
||||
@MockBean
|
||||
private AuditEventRepository repository;
|
||||
@Resource
|
||||
private AuditIngestService service;
|
||||
|
||||
@Test
|
||||
void keepsStableEventIdForMqRedelivery() {
|
||||
AuditIngestRequest request = request("audit-1");
|
||||
when(repository.save(request, "audit-1")).thenReturn(false);
|
||||
|
||||
assertThat(service.ingest(request)).isFalse();
|
||||
|
||||
verify(repository).save(request, "audit-1");
|
||||
}
|
||||
|
||||
private static AuditIngestRequest request(String eventId) {
|
||||
return new AuditIngestRequest(eventId, "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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.audit.service;
|
||||
|
||||
import com.baiyue.audit.dto.AuditLogQuery;
|
||||
import com.baiyue.audit.dto.AuditLogResponses;
|
||||
import com.baiyue.audit.entity.AuditLogEntity;
|
||||
import com.baiyue.audit.repository.AuditEventRepository;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(AuditLogQueryService.class)
|
||||
class AuditLogQueryServiceTest {
|
||||
@MockBean
|
||||
private AuditEventRepository repository;
|
||||
|
||||
@Resource
|
||||
private AuditLogQueryService service;
|
||||
|
||||
@Test
|
||||
void pageDoesNotExposeSnapshots() {
|
||||
AuditLogQuery query = new AuditLogQuery(10001L, 7L, "ROLE_UPDATE", "ROLE", "9", true,
|
||||
null, null, "管理员");
|
||||
when(repository.page(query, 1, 20)).thenReturn(new PageResult<>(List.of(log(8L)), 1, 20, 1, 1));
|
||||
|
||||
PageResult<AuditLogResponses.ListItem> result = service.page(query, 1, 20);
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(item -> {
|
||||
assertThat(item.id()).isEqualTo(8L);
|
||||
assertThat(item.accountId()).isEqualTo(10001L);
|
||||
assertThat(item.eventType()).isEqualTo("ROLE_UPDATE");
|
||||
});
|
||||
assertThat(AuditLogResponses.ListItem.class.getRecordComponents())
|
||||
.extracting(component -> component.getName())
|
||||
.doesNotContain("beforeData", "afterData");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsSanitizedSnapshots() {
|
||||
when(repository.findById(8L)).thenReturn(log(8L));
|
||||
|
||||
AuditLogResponses.Detail detail = service.detail(8L);
|
||||
|
||||
assertThat(detail.beforeData()).isEqualTo("{\"name\":\"旧角色\"}");
|
||||
assertThat(detail.afterData()).isEqualTo("{\"name\":\"新角色\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingLogIsReportedAsNotFound() {
|
||||
when(repository.findById(8L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.detail(8L)).isInstanceOf(BizException.class)
|
||||
.hasMessage("操作日志不存在");
|
||||
}
|
||||
|
||||
private static AuditLogEntity log(Long id) {
|
||||
AuditLogEntity entity = new AuditLogEntity();
|
||||
entity.setId(id);
|
||||
entity.setEventId("audit-8");
|
||||
entity.setEventType("ROLE_UPDATE");
|
||||
entity.setResourceType("ROLE");
|
||||
entity.setResourceId("9");
|
||||
entity.setDescription("管理员修改角色权限");
|
||||
entity.setTenantId(10001L);
|
||||
entity.setOperatorTenantId(0L);
|
||||
entity.setUserId(7L);
|
||||
entity.setUsername("管理员");
|
||||
entity.setAppCode("PLATFORM");
|
||||
entity.setClientType("WEB");
|
||||
entity.setRequestId("request-8");
|
||||
entity.setTraceId("trace-8");
|
||||
entity.setSuccess(true);
|
||||
entity.setBeforeData("{\"name\":\"旧角色\"}");
|
||||
entity.setAfterData("{\"name\":\"新角色\"}");
|
||||
entity.setServiceName("iam-service");
|
||||
entity.setOccurredAt(Instant.parse("2026-08-05T10:00:00Z"));
|
||||
entity.setCreatedAt(LocalDateTime.of(2026, 8, 5, 18, 0));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user