package com.baiyue.common.audit; import com.baiyue.common.security.AuthenticatedUser; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.util.ReflectionTestUtils; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class BusinessAuditAspectTest { @AfterEach void clearSecurityContext() { SecurityContextHolder.clearContext(); } @Test void platformOperationUsesTargetCustomerAndKeepsOperatorScope() throws Throwable { AuditEvent event = invoke(new AuthenticatedUser(7L, 0L, 0L, 1L, "platform.admin", "PLATFORM", "WEB", Set.of("platform:customer:edit")), 10001L); assertThat(event.tenantId()).isEqualTo(10001L); assertThat(event.operatorTenantId()).isZero(); assertThat(event.userId()).isEqualTo(7L); assertThat(event.appCode()).isEqualTo("PLATFORM"); assertThat(event.beforeData()).isEqualTo("[10001]"); assertThat(event.beforeData()).doesNotContain("platform.admin", "platform:customer:edit", "permissions"); } @Test void merchantOperationIgnoresTargetCustomerArgument() throws Throwable { AuditEvent event = invoke(new AuthenticatedUser(8L, 10001L, 10001L, 2L, "merchant.user", "MERCHANT", "WEB", Set.of()), 20002L); assertThat(event.tenantId()).isEqualTo(10001L); assertThat(event.operatorTenantId()).isEqualTo(10001L); } @Test void failedOperationStillPublishesSanitizedBeforeSnapshot() throws Throwable { AuditEvent event = invokeFailure(new AuthenticatedUser(7L, 0L, 0L, 1L, "platform.admin", "PLATFORM", "WEB", Set.of()), 10001L); assertThat(event.success()).isFalse(); assertThat(event.errorCode()).isEqualTo("IllegalStateException"); assertThat(event.beforeData()).isEqualTo("[10001]"); assertThat(event.afterData()).isNull(); } private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId) throws Throwable { return invoke(user, requestedTenantId, null); } private static AuditEvent invokeFailure(AuthenticatedUser user, long requestedTenantId) throws Throwable { return invoke(user, requestedTenantId, new IllegalStateException("validation failed")); } private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId, Throwable failure) throws Throwable { AuditSink sink = mock(AuditSink.class); BusinessAuditAspect aspect = new BusinessAuditAspect(); ReflectionTestUtils.setField(aspect, "sink", sink); ReflectionTestUtils.setField(aspect, "sanitizer", new SensitiveDataSanitizer(new ObjectMapper(), 1024)); ReflectionTestUtils.setField(aspect, "serviceName", "test-service"); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(user, null, List.of())); TestOperations target = new TestOperations(); Method method = TestOperations.class.getDeclaredMethod("update", AuthenticatedUser.class, long.class); MethodSignature signature = mock(MethodSignature.class); when(signature.getMethod()).thenReturn(method); when(signature.getParameterNames()).thenReturn(null); ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); when(joinPoint.getSignature()).thenReturn(signature); when(joinPoint.getTarget()).thenReturn(target); when(joinPoint.getArgs()).thenReturn(new Object[]{user, requestedTenantId}); if (failure == null) when(joinPoint.proceed()).thenReturn("ok"); else when(joinPoint.proceed()).thenThrow(failure); try { aspect.around(joinPoint, method.getAnnotation(BusinessAudit.class)); } catch (Throwable error) { if (failure == null) throw error; } ArgumentCaptor event = ArgumentCaptor.forClass(AuditEvent.class); verify(sink).publish(event.capture()); return event.getValue(); } private static final class TestOperations { @BusinessAudit(event = "TEST_UPDATE", resourceType = "TEST", captureAfter = false) public String update(AuthenticatedUser user, long tenantId) { return "ok"; } } }