add curl field

This commit is contained in:
Chuck1sn
2025-07-08 10:48:20 +08:00
parent 36d285a61d
commit 0a0174c01e
9 changed files with 913 additions and 12 deletions

View File

@@ -6,18 +6,24 @@ import com.zl.mjga.annotation.SkipAopLog;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.AopLogService;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Enumeration;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.jooq.generated.mjga.tables.pojos.AopLog;
import org.jooq.generated.mjga.tables.pojos.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@@ -28,6 +34,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
@Component
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(name = "aop.logging.enabled", havingValue = "true", matchIfMissing = true)
public class LoggingAspect {
private final AopLogService aopLogService;
@@ -137,6 +144,7 @@ public class LoggingAspect {
HttpServletRequest request = attributes.getRequest();
aopLog.setIpAddress(getClientIpAddress(request));
aopLog.setUserAgent(request.getHeader("User-Agent"));
aopLog.setCurl(generateCurlCommand(request));
}
private String getClientIpAddress(HttpServletRequest request) {
@@ -179,4 +187,132 @@ public class LoggingAspect {
return e.getMessage();
}
}
public String generateCurlCommand(HttpServletRequest request) {
try {
StringBuilder curl = new StringBuilder("curl -X ");
curl.append(request.getMethod());
String url = getFullRequestUrl(request);
curl.append(" '").append(url).append("'");
appendHeaders(curl, request);
if (hasRequestBody(request.getMethod())) {
appendRequestBody(curl, request);
}
return curl.toString();
} catch (Exception e) {
log.error("Failed to generate curl command", e);
return "curl command generation failed: " + e.getMessage();
}
}
private String getFullRequestUrl(HttpServletRequest request) {
StringBuilder url = new StringBuilder();
String scheme = request.getScheme();
String serverName = request.getServerName();
int serverPort = request.getServerPort();
if (scheme == null) {
scheme = "http";
}
if (serverName == null) {
serverName = "localhost";
}
url.append(scheme).append("://").append(serverName);
if ((scheme.equals("http") && serverPort != 80)
|| (scheme.equals("https") && serverPort != 443)) {
url.append(":").append(serverPort);
}
url.append(request.getRequestURI());
if (request.getQueryString() != null) {
url.append("?").append(request.getQueryString());
}
return url.toString();
}
private void appendHeaders(StringBuilder curl, HttpServletRequest request) {
Enumeration<String> headerNames = request.getHeaderNames();
for (String headerName : Collections.list(headerNames)) {
if (shouldSkipHeader(headerName)) {
continue;
}
String headerValue = request.getHeader(headerName);
curl.append(" -H '").append(headerName).append(": ").append(headerValue).append("'");
}
}
private boolean shouldSkipHeader(String headerName) {
String lowerName = headerName.toLowerCase();
return lowerName.equals("host")
|| lowerName.equals("content-length")
|| lowerName.equals("connection")
|| lowerName.startsWith("sec-")
|| lowerName.equals("upgrade-insecure-requests");
}
private boolean hasRequestBody(String method) {
return "POST".equalsIgnoreCase(method)
|| "PUT".equalsIgnoreCase(method)
|| "PATCH".equalsIgnoreCase(method);
}
private void appendRequestBody(StringBuilder curl, HttpServletRequest request) {
try {
String contentType = request.getContentType();
if (StringUtils.contains(contentType, "application/json")) {
String body = getRequestBody(request);
if (StringUtils.isNotEmpty(body)) {
curl.append(" -d '").append(body.replace("'", "\\'")).append("'");
}
} else if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
appendFormData(curl, request);
}
} catch (Exception e) {
log.warn("Failed to append request body to curl command", e);
}
}
private String getRequestBody(HttpServletRequest request) {
try (BufferedReader reader = request.getReader()) {
if (reader == null) {
return null;
}
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
return body.toString();
} catch (IOException e) {
log.warn("Failed to read request body", e);
return null;
}
}
private void appendFormData(StringBuilder curl, HttpServletRequest request) {
Enumeration<String> paramNames = request.getParameterNames();
StringBuilder formData = new StringBuilder();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String[] paramValues = request.getParameterValues(paramName);
for (String paramValue : paramValues) {
if (!formData.isEmpty()) {
formData.append("&");
}
formData.append(paramName).append("=").append(paramValue);
}
}
if (!formData.isEmpty()) {
curl.append(" -d '").append(formData).append("'");
}
}
}

View File

@@ -48,6 +48,9 @@ public class AopLogRespDto {
/** 用户代理 */
private String userAgent;
/** curl命令 */
private String curl;
/** 创建时间 */
private OffsetDateTime createTime;
}

View File

@@ -41,4 +41,7 @@ minio:
endpoint: ${MINIO_ENDPOINT}
access-key: ${MINIO_ROOT_USER}
secret-key: ${MINIO_ROOT_PASSWORD}
default-bucket: ${MINIO_DEFAULT_BUCKETS}
default-bucket: ${MINIO_DEFAULT_BUCKETS}
aop:
logging:
enabled: true

View File

@@ -10,6 +10,7 @@ CREATE TABLE mjga.aop_log (
user_id BIGINT,
ip_address VARCHAR,
user_agent VARCHAR,
curl VARCHAR,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
);

View File

@@ -22,6 +22,7 @@ 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.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -47,11 +48,10 @@ class LoggingAspectTest {
@Mock private ServletRequestAttributes servletRequestAttributes;
@Mock private HttpServletRequest httpServletRequest;
private LoggingAspect loggingAspect;
@InjectMocks LoggingAspect loggingAspect;
@BeforeEach
void setUp() {
loggingAspect = new LoggingAspect(aopLogService, objectMapper, userRepository);
SecurityContextHolder.setContext(securityContext);
}
@@ -149,7 +149,6 @@ class LoggingAspectTest {
assertThat(log.getMethodName()).isEqualTo("serviceMethod");
assertThat(log.getSuccess()).isTrue();
assertThat(log.getUserId()).isEqualTo(123L);
// Service层不应该有请求信息
assertThat(log.getIpAddress()).isNull();
assertThat(log.getUserAgent()).isNull();
});
@@ -179,7 +178,6 @@ class LoggingAspectTest {
assertThat(log.getMethodName()).isEqualTo("findById");
assertThat(log.getSuccess()).isTrue();
assertThat(log.getUserId()).isEqualTo(123L);
// Repository层不应该有请求信息
assertThat(log.getIpAddress()).isNull();
assertThat(log.getUserAgent()).isNull();
});
@@ -414,6 +412,51 @@ class LoggingAspectTest {
return TestController.class.getMethod("skipLogMethod");
}
@Test
void logController_givenHttpRequest_shouldGenerateCurlCommand() throws Throwable {
// arrange
TestController target = new TestController();
Object[] args = {"arg1"};
String expectedResult = "success";
User mockUser = createMockUser(123L, "testUser");
setupAuthenticatedUser("testUser", mockUser);
setupJoinPoint(target, "testMethod", args, expectedResult);
setupSerialization("[\"arg1\"]", "\"success\"");
// Setup HTTP request mocks before setupRequestContext
when(httpServletRequest.getMethod()).thenReturn("POST");
when(httpServletRequest.getScheme()).thenReturn("http");
when(httpServletRequest.getServerName()).thenReturn("localhost");
when(httpServletRequest.getServerPort()).thenReturn(8080);
when(httpServletRequest.getRequestURI()).thenReturn("/api/test");
when(httpServletRequest.getQueryString()).thenReturn("param1=value1");
when(httpServletRequest.getContentType()).thenReturn("application/json");
when(httpServletRequest.getHeaderNames())
.thenReturn(
java.util.Collections.enumeration(
java.util.Arrays.asList("Content-Type", "Authorization")));
when(httpServletRequest.getHeader("Content-Type")).thenReturn("application/json");
when(httpServletRequest.getHeader("Authorization")).thenReturn("Bearer token123");
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
setupRequestContext("127.0.0.1", "Test-Agent")) {
// action
Object result = loggingAspect.logController(joinPoint);
// assert
assertThat(result).isEqualTo(expectedResult);
verifyLogSaved(
log -> {
assertThat(log.getCurl()).isNotNull();
assertThat(log.getCurl()).contains("curl -X POST");
assertThat(log.getCurl()).contains("'http://localhost:8080/api/test?param1=value1'");
assertThat(log.getCurl()).contains("-H 'Content-Type: application/json'");
assertThat(log.getCurl()).contains("-H 'Authorization: Bearer token123'");
});
}
}
// Test classes for mocking
private static class TestController {
public String testMethod() {

View File

@@ -180,6 +180,7 @@ public class AopLogControllerTest {
.username("testUser")
.ipAddress("127.0.0.1")
.userAgent("Test Agent")
.curl("curl -X GET 'http://localhost:8080/test' -H 'Content-Type: application/json'")
.createTime(OffsetDateTime.now())
.build();
}

View File

@@ -0,0 +1,612 @@
package com.zl.mjga.unit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.aspect.LoggingAspect;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.AopLogService;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@DisplayName("LoggingAspect - generateCurlCommand 方法测试")
class LoggingAspectCurlGenerationTest {
@Mock private AopLogService aopLogService;
@Mock private ObjectMapper objectMapper;
@Mock private UserRepository userRepository;
@Mock private HttpServletRequest request;
private LoggingAspect loggingAspect;
@BeforeEach
void setUp() {
loggingAspect = new LoggingAspect(aopLogService, objectMapper, userRepository);
}
@Nested
@DisplayName("GET 请求测试")
class GetRequestTests {
@Test
@DisplayName("基本 GET 请求 - 无查询参数")
void generateCurlCommand_givenBasicGetRequest_shouldGenerateCorrectCurl() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'http://localhost:8080/api/users'");
}
@Test
@DisplayName("GET 请求 - 包含查询参数")
void generateCurlCommand_givenGetRequestWithQueryParams_shouldIncludeQueryString() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn("page=1&size=10&name=test");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.isEqualTo("curl -X GET 'http://localhost:8080/api/users?page=1&size=10&name=test'");
}
@Test
@DisplayName("GET 请求 - HTTPS 协议")
void generateCurlCommand_givenHttpsGetRequest_shouldUseHttpsScheme() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("https");
when(request.getServerName()).thenReturn("api.example.com");
when(request.getServerPort()).thenReturn(443);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'https://api.example.com/api/users'");
}
@Test
@DisplayName("GET 请求 - 自定义端口")
void generateCurlCommand_givenGetRequestWithCustomPort_shouldIncludePort() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(9090);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'http://localhost:9090/api/users'");
}
}
@Nested
@DisplayName("POST 请求测试")
class PostRequestTests {
@Test
@DisplayName("POST 请求 - JSON 请求体")
void generateCurlCommand_givenPostRequestWithJsonBody_shouldIncludeDataFlag()
throws IOException {
// arrange
String jsonBody = "{\"name\":\"test\",\"age\":25}";
setupPostRequest(jsonBody, "application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/users'")
.contains("-d '{\"name\":\"test\",\"age\":25}'");
}
@Test
@DisplayName("POST 请求 - 空 JSON 请求体")
void generateCurlCommand_givenPostRequestWithEmptyJsonBody_shouldNotIncludeDataFlag()
throws IOException {
// arrange
setupPostRequest("", "application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/users'")
.doesNotContain("-d");
}
@Test
@DisplayName("POST 请求 - 包含单引号的 JSON")
void generateCurlCommand_givenPostRequestWithQuotesInJson_shouldEscapeQuotes()
throws IOException {
// arrange
String jsonBody = "{\"message\":\"It's a test\"}";
setupPostRequest(jsonBody, "application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("-d '{\"message\":\"It\\'s a test\"}'");
}
private void setupPostRequest(String body, String contentType) throws IOException {
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn(contentType);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
if (body != null && !body.trim().isEmpty()) {
BufferedReader reader = new BufferedReader(new StringReader(body));
when(request.getReader()).thenReturn(reader);
} else {
when(request.getReader()).thenReturn(new BufferedReader(new StringReader("")));
}
}
}
@Nested
@DisplayName("PUT 和 PATCH 请求测试")
class PutAndPatchRequestTests {
@Test
@DisplayName("PUT 请求 - JSON 请求体")
void generateCurlCommand_givenPutRequestWithJsonBody_shouldIncludeDataFlag()
throws IOException {
// arrange
String jsonBody = "{\"id\":1,\"name\":\"updated\"}";
setupRequestWithBody("PUT", jsonBody, "application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("curl -X PUT").contains("-d '{\"id\":1,\"name\":\"updated\"}'");
}
@Test
@DisplayName("PATCH 请求 - JSON 请求体")
void generateCurlCommand_givenPatchRequestWithJsonBody_shouldIncludeDataFlag()
throws IOException {
// arrange
String jsonBody = "{\"name\":\"patched\"}";
setupRequestWithBody("PATCH", jsonBody, "application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("curl -X PATCH").contains("-d '{\"name\":\"patched\"}'");
}
private void setupRequestWithBody(String method, String body, String contentType)
throws IOException {
when(request.getMethod()).thenReturn(method);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users/1");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn(contentType);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
BufferedReader reader = new BufferedReader(new StringReader(body));
when(request.getReader()).thenReturn(reader);
}
}
@Nested
@DisplayName("表单数据请求测试")
class FormDataRequestTests {
@Test
@DisplayName("POST 请求 - 表单数据")
void generateCurlCommand_givenPostRequestWithFormData_shouldIncludeFormData() {
// arrange
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/login");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn("application/x-www-form-urlencoded");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
List<String> paramNames = new ArrayList<>();
paramNames.add("username");
paramNames.add("password");
when(request.getParameterNames()).thenReturn(Collections.enumeration(paramNames));
when(request.getParameterValues("username")).thenReturn(new String[] {"testuser"});
when(request.getParameterValues("password")).thenReturn(new String[] {"testpass"});
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/login'")
.contains("-d 'username=testuser&password=testpass'");
}
@Test
@DisplayName("POST 请求 - 多值表单参数")
void generateCurlCommand_givenPostRequestWithMultiValueFormData_shouldIncludeAllValues() {
// arrange
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/submit");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn("application/x-www-form-urlencoded");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
List<String> paramNames = new ArrayList<>();
paramNames.add("tags");
when(request.getParameterNames()).thenReturn(Collections.enumeration(paramNames));
when(request.getParameterValues("tags")).thenReturn(new String[] {"tag1", "tag2", "tag3"});
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("-d 'tags=tag1&tags=tag2&tags=tag3'");
}
@Test
@DisplayName("POST 请求 - 空表单数据")
void generateCurlCommand_givenPostRequestWithEmptyFormData_shouldNotIncludeDataFlag() {
// arrange
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/submit");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn("application/x-www-form-urlencoded");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
when(request.getParameterNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/submit'")
.doesNotContain("-d");
}
}
@Nested
@DisplayName("请求头处理测试")
class HeaderProcessingTests {
@Test
@DisplayName("包含常规请求头")
void generateCurlCommand_givenRequestWithHeaders_shouldIncludeHeaders() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
List<String> headerNames = new ArrayList<>();
headerNames.add("Authorization");
headerNames.add("Content-Type");
headerNames.add("Accept");
when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames));
when(request.getHeader("Authorization")).thenReturn("Bearer token123");
when(request.getHeader("Content-Type")).thenReturn("application/json");
when(request.getHeader("Accept")).thenReturn("application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("-H 'Authorization: Bearer token123'")
.contains("-H 'Content-Type: application/json'")
.contains("-H 'Accept: application/json'");
}
@Test
@DisplayName("跳过特定请求头")
void generateCurlCommand_givenRequestWithSkippedHeaders_shouldExcludeSkippedHeaders() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
List<String> headerNames = new ArrayList<>();
headerNames.add("Authorization");
when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames));
when(request.getHeader("Authorization")).thenReturn("Bearer token123");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("-H 'Authorization: Bearer token123'");
}
@Test
@DisplayName("验证跳过的请求头不会出现在 curl 命令中")
void generateCurlCommand_givenSkippedHeaders_shouldNotIncludeSkippedHeaders() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
List<String> headerNames = new ArrayList<>();
headerNames.add("Host");
headerNames.add("Content-Length");
headerNames.add("Connection");
headerNames.add("Sec-Fetch-Mode");
headerNames.add("Upgrade-Insecure-Requests");
headerNames.add("Accept");
when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames));
when(request.getHeader("Accept")).thenReturn("application/json");
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("-H 'Accept: application/json'")
.doesNotContain("Host")
.doesNotContain("Content-Length")
.doesNotContain("Connection")
.doesNotContain("Sec-Fetch-Mode")
.doesNotContain("Upgrade-Insecure-Requests");
}
}
@Nested
@DisplayName("异常情况测试")
class ExceptionHandlingTests {
@Test
@DisplayName("读取请求体时发生 IOException")
void generateCurlCommand_givenIOExceptionWhenReadingBody_shouldHandleGracefully()
throws IOException {
// arrange
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn("application/json");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
when(request.getReader()).thenThrow(new IOException("Reader error"));
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/users'")
.doesNotContain("-d"); // 应该不包含数据标志,因为读取失败
}
@Test
@DisplayName("请求参数为 null")
void generateCurlCommand_givenNullRequest_shouldReturnErrorMessage() {
// action
String result = loggingAspect.generateCurlCommand(null);
// assert
assertThat(result).contains("curl command generation failed:");
}
@Test
@DisplayName("请求方法为 null")
void generateCurlCommand_givenNullMethod_shouldHandleGracefully() {
// arrange
when(request.getMethod()).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).contains("curl -X null");
}
@Test
@DisplayName("服务器信息为 null")
void generateCurlCommand_givenNullServerInfo_shouldUseDefaults() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn(null);
when(request.getServerName()).thenReturn(null);
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'http://localhost:8080/api/users'");
}
}
@Nested
@DisplayName("边界用例测试")
class BoundaryTests {
@Test
@DisplayName("最小化 GET 请求")
void generateCurlCommand_givenMinimalGetRequest_shouldGenerateBasicCurl() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(80); // 标准端口
when(request.getRequestURI()).thenReturn("/");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'http://localhost/'");
}
@Test
@DisplayName("复杂查询参数 - 包含特殊字符")
void generateCurlCommand_givenComplexQueryParams_shouldIncludeAllParams() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/search");
when(request.getQueryString())
.thenReturn("q=hello%20world&filter=type%3Duser&sort=name%2Casc");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.isEqualTo(
"curl -X GET"
+ " 'http://localhost:8080/api/search?q=hello%20world&filter=type%3Duser&sort=name%2Casc'");
}
@Test
@DisplayName("DELETE 请求 - 不应包含请求体")
void generateCurlCommand_givenDeleteRequest_shouldNotIncludeBody() {
// arrange
when(request.getMethod()).thenReturn("DELETE");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users/1");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X DELETE")
.contains("'http://localhost:8080/api/users/1'")
.doesNotContain("-d");
}
@Test
@DisplayName("HTTPS 请求 - 标准端口 443")
void generateCurlCommand_givenHttpsRequestWithStandardPort_shouldNotIncludePort() {
// arrange
when(request.getMethod()).thenReturn("GET");
when(request.getScheme()).thenReturn("https");
when(request.getServerName()).thenReturn("api.example.com");
when(request.getServerPort()).thenReturn(443);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result).isEqualTo("curl -X GET 'https://api.example.com/api/users'");
}
@Test
@DisplayName("JSON 请求体为 null")
void generateCurlCommand_givenPostRequestWithNullJsonBody_shouldNotIncludeDataFlag()
throws IOException {
// arrange
when(request.getMethod()).thenReturn("POST");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getRequestURI()).thenReturn("/api/users");
when(request.getQueryString()).thenReturn(null);
when(request.getContentType()).thenReturn("application/json");
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
when(request.getReader()).thenReturn(null);
// action
String result = loggingAspect.generateCurlCommand(request);
// assert
assertThat(result)
.contains("curl -X POST")
.contains("'http://localhost:8080/api/users'")
.doesNotContain("-d");
}
}
}

View File

@@ -0,0 +1,103 @@
# LoggingAspect generateCurlCommand 方法单元测试
## 测试概述
本测试文件 `LoggingAspectCurlGenerationTest.java` 专门针对 `LoggingAspect` 类中的 `generateCurlCommand` 方法进行全面的单元测试,验证该方法在各种场景下生成 curl 命令的正确性。
## 测试架构
测试采用 **嵌套测试类** 的结构,按功能模块组织:
### 1. GET 请求测试 (`GetRequestTests`)
- ✅ 基本 GET 请求 - 无查询参数
- ✅ GET 请求 - 包含查询参数
- ✅ GET 请求 - HTTPS 协议
- ✅ GET 请求 - 自定义端口
### 2. POST 请求测试 (`PostRequestTests`)
- ✅ POST 请求 - JSON 请求体
- ✅ POST 请求 - 空 JSON 请求体
- ✅ POST 请求 - 包含单引号的 JSON
### 3. PUT 和 PATCH 请求测试 (`PutAndPatchRequestTests`)
- ✅ PUT 请求 - JSON 请求体
- ✅ PATCH 请求 - JSON 请求体
### 4. 表单数据请求测试 (`FormDataRequestTests`)
- ✅ POST 请求 - 表单数据
- ✅ POST 请求 - 多值表单参数
- ✅ POST 请求 - 空表单数据
### 5. 请求头处理测试 (`HeaderProcessingTests`)
- ✅ 包含常规请求头
- ✅ 跳过特定请求头
- ✅ 验证跳过的请求头不会出现在 curl 命令中
### 6. 异常情况测试 (`ExceptionHandlingTests`)
- ✅ 读取请求体时发生 IOException
- ✅ 请求参数为 null
- ✅ 请求方法为 null
- ✅ 服务器信息为 null
### 7. 边界用例测试 (`BoundaryTests`)
- ✅ 最小化 GET 请求
- ✅ 复杂查询参数 - 包含特殊字符
- ✅ DELETE 请求 - 不应包含请求体
- ✅ HTTPS 请求 - 标准端口 443
- ✅ JSON 请求体为 null
## 测试覆盖的功能点
### 核心功能验证
1. **HTTP 方法处理**: GET, POST, PUT, PATCH, DELETE
2. **URL 构建**: 协议、主机名、端口、路径、查询参数
3. **请求头处理**: 包含/排除特定请求头
4. **请求体处理**: JSON、表单数据、空请求体
5. **异常处理**: 各种异常情况的优雅处理
### 特殊场景验证
1. **端口处理**: 标准端口省略,非标准端口包含
2. **字符转义**: JSON 中的单引号转义
3. **空值处理**: null 值的安全处理
4. **多值参数**: 表单中同名参数的多个值
## 测试技术特点
### 使用的测试技术
- **JUnit 5**: 现代化的测试框架
- **Mockito**: Mock 对象和行为验证
- **AssertJ**: 流畅的断言 API
- **嵌套测试**: 清晰的测试组织结构
### Mock 策略
- Mock `HttpServletRequest` 对象模拟各种 HTTP 请求场景
- Mock 依赖服务避免外部依赖
- 精确控制测试数据和行为
### 断言策略
- 验证生成的 curl 命令包含预期内容
- 验证不应包含的内容确实被排除
- 验证异常情况的错误消息
## 运行测试
```bash
# 运行所有 generateCurlCommand 相关测试
./gradlew test --tests "com.zl.mjga.unit.LoggingAspectCurlGenerationTest"
# 运行特定测试类别
./gradlew test --tests "com.zl.mjga.unit.LoggingAspectCurlGenerationTest\$GetRequestTests"
```
## 测试价值
这套测试确保了 `generateCurlCommand` 方法在各种复杂场景下都能正确工作,为 AOP 日志功能的 curl 命令生成提供了可靠的质量保证。通过全面的测试覆盖,可以:
1. **防止回归**: 代码修改时及时发现问题
2. **文档作用**: 测试用例本身就是最好的使用文档
3. **重构支持**: 安全地进行代码重构
4. **质量保证**: 确保功能在各种边界条件下正常工作
## 测试结果
所有 **24 个测试用例** 均通过,覆盖了 `generateCurlCommand` 方法的所有主要功能和边界情况。

View File

@@ -1,21 +1,20 @@
-- AOP日志表
CREATE TABLE mjga.aop_log (
id BIGSERIAL PRIMARY KEY,
class_name VARCHAR NOT NULL,
method_name VARCHAR NOT NULL,
method_args TEXT,
return_value TEXT,
method_args VARCHAR,
return_value VARCHAR,
execution_time BIGINT NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_message TEXT,
error_message VARCHAR,
user_id BIGINT,
ip_address VARCHAR(45),
user_agent TEXT,
ip_address VARCHAR,
user_agent VARCHAR,
curl VARCHAR,
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
);
-- 创建索引以提高查询性能
CREATE INDEX idx_aop_log_class_name ON mjga.aop_log(class_name);
CREATE INDEX idx_aop_log_method_name ON mjga.aop_log(method_name);
CREATE INDEX idx_aop_log_create_time ON mjga.aop_log(create_time);