417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | package logging | |||
|  | 
 | |||
|  | import ( | |||
|  | 	"encoding/json" | |||
|  | 	"fmt" | |||
|  | 	"io" | |||
|  | 	"net/http" | |||
|  | 	"net/http/httptest" | |||
|  | 	"os" | |||
|  | 	"path/filepath" | |||
|  | 	"strings" | |||
|  | 	"testing" | |||
|  | 	"time" | |||
|  | 	"tyc-server/app/main/api/internal/config" | |||
|  | 
 | |||
|  | 	"github.com/stretchr/testify/assert" | |||
|  | ) | |||
|  | 
 | |||
|  | // 创建测试配置 | |||
|  | func createTestLoggingConfig() *config.LoggingConfig { | |||
|  | 	return &config.LoggingConfig{ | |||
|  | 		UserOperationLogDir: "./test_logs/user_operations", | |||
|  | 		MaxFileSize:         1024, // 1KB for testing | |||
|  | 		LogLevel:            "info", | |||
|  | 		EnableConsole:       true, | |||
|  | 		EnableFile:          true, | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // 清理测试文件 | |||
|  | func cleanupTestFiles() { | |||
|  | 	os.RemoveAll("./test_logs") | |||
|  | } | |||
|  | 
 | |||
|  | // TestNewUserOperationMiddleware 测试中间件创建 | |||
|  | func TestNewUserOperationMiddleware(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	assert.NotNil(t, middleware) | |||
|  | 	assert.Equal(t, config.UserOperationLogDir, middleware.logDir) | |||
|  | 	assert.Equal(t, config.MaxFileSize, middleware.maxFileSize) | |||
|  | 	assert.Equal(t, 180, middleware.maxDays) | |||
|  | 	assert.NotNil(t, middleware.jwtExtractor) | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_Handle 测试中间件处理 | |||
|  | func TestUserOperationMiddleware_Handle(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 创建测试请求 | |||
|  | 	req := httptest.NewRequest("GET", "/api/v1/test?param1=value1", nil) | |||
|  | 	req.Header.Set("Authorization", "Bearer test-token") | |||
|  | 	req.Header.Set("User-Agent", "test-agent") | |||
|  | 	req.Header.Set("X-Real-IP", "192.168.1.100") | |||
|  | 
 | |||
|  | 	// 创建响应记录器 | |||
|  | 	w := httptest.NewRecorder() | |||
|  | 
 | |||
|  | 	// 定义测试处理器 | |||
|  | 	handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) { | |||
|  | 		w.WriteHeader(http.StatusOK) | |||
|  | 		w.Write([]byte("test response")) | |||
|  | 	}) | |||
|  | 
 | |||
|  | 	// 执行请求 | |||
|  | 	handler(w, req) | |||
|  | 
 | |||
|  | 	// 验证响应 | |||
|  | 	assert.Equal(t, http.StatusOK, w.Code) | |||
|  | 	assert.Equal(t, "test response", w.Body.String()) | |||
|  | 
 | |||
|  | 	// 等待日志写入 | |||
|  | 	time.Sleep(100 * time.Millisecond) | |||
|  | 
 | |||
|  | 	// 验证日志文件是否创建 | |||
|  | 	today := time.Now().Format("2006-01-02") | |||
|  | 	logDir := filepath.Join(config.UserOperationLogDir, today) | |||
|  | 	assert.DirExists(t, logDir) | |||
|  | 
 | |||
|  | 	// 检查是否有日志文件 | |||
|  | 	files, err := os.ReadDir(logDir) | |||
|  | 	assert.NoError(t, err) | |||
|  | 	assert.Greater(t, len(files), 0) | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_OperationType 测试操作类型识别 | |||
|  | func TestUserOperationMiddleware_OperationType(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	testCases := []struct { | |||
|  | 		method   string | |||
|  | 		path     string | |||
|  | 		expected string | |||
|  | 	}{ | |||
|  | 		{"GET", "/api/v1/login", "用户登录"}, | |||
|  | 		{"POST", "/api/v1/logout", "用户退出"}, | |||
|  | 		{"POST", "/api/v1/register", "用户注册"}, | |||
|  | 		{"PUT", "/api/v1/password", "密码操作"}, | |||
|  | 		{"GET", "/api/v1/profile", "个人信息"}, | |||
|  | 		{"GET", "/api/v1/admin/users", "管理操作"}, | |||
|  | 		{"GET", "/api/v1/products", "查询操作"}, | |||
|  | 		{"POST", "/api/v1/orders", "创建操作"}, | |||
|  | 		{"PUT", "/api/v1/users/123", "更新操作"}, | |||
|  | 		{"DELETE", "/api/v1/users/123", "删除操作"}, | |||
|  | 		{"PATCH", "/api/v1/users/123", "更新操作"}, | |||
|  | 	} | |||
|  | 
 | |||
|  | 	for _, tc := range testCases { | |||
|  | 		t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) { | |||
|  | 			result := middleware.determineOperation(tc.method, tc.path) | |||
|  | 			assert.Equal(t, tc.expected, result) | |||
|  | 		}) | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_ClientIP 测试客户端IP提取 | |||
|  | func TestUserOperationMiddleware_ClientIP(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	testCases := []struct { | |||
|  | 		name     string | |||
|  | 		headers  map[string]string | |||
|  | 		expected string | |||
|  | 	}{ | |||
|  | 		{ | |||
|  | 			name: "X-Forwarded-For优先", | |||
|  | 			headers: map[string]string{ | |||
|  | 				"X-Forwarded-For": "203.0.113.1, 192.168.1.1", | |||
|  | 				"X-Real-IP":       "198.51.100.1", | |||
|  | 			}, | |||
|  | 			expected: "203.0.113.1", | |||
|  | 		}, | |||
|  | 		{ | |||
|  | 			name: "X-Real-IP次之", | |||
|  | 			headers: map[string]string{ | |||
|  | 				"X-Real-IP": "198.51.100.1", | |||
|  | 			}, | |||
|  | 			expected: "198.51.100.1", | |||
|  | 		}, | |||
|  | 		{ | |||
|  | 			name:     "RemoteAddr最后", | |||
|  | 			headers:  map[string]string{}, | |||
|  | 			expected: "unknown", // 在测试环境中RemoteAddr可能为空 | |||
|  | 		}, | |||
|  | 	} | |||
|  | 
 | |||
|  | 	for _, tc := range testCases { | |||
|  | 		t.Run(tc.name, func(t *testing.T) { | |||
|  | 			req := httptest.NewRequest("GET", "/test", nil) | |||
|  | 			for key, value := range tc.headers { | |||
|  | 				req.Header.Set(key, value) | |||
|  | 			} | |||
|  | 
 | |||
|  | 			result := middleware.getClientIP(req) | |||
|  | 			if tc.expected != "unknown" { | |||
|  | 				assert.Equal(t, tc.expected, result) | |||
|  | 			} | |||
|  | 		}) | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_QueryParams 测试查询参数解析 | |||
|  | func TestUserOperationMiddleware_QueryParams(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 测试正常查询参数 | |||
|  | 	req := httptest.NewRequest("GET", "/test?param1=value1¶m2=value2¶m3=", nil) | |||
|  | 	params := middleware.parseQueryParams(req.URL.RawQuery) | |||
|  | 
 | |||
|  | 	assert.Equal(t, "value1", params["param1"]) | |||
|  | 	assert.Equal(t, "value2", params["param2"]) | |||
|  | 	assert.Equal(t, "", params["param3"]) | |||
|  | 
 | |||
|  | 	// 测试空查询参数 | |||
|  | 	req = httptest.NewRequest("GET", "/test", nil) | |||
|  | 	params = middleware.parseQueryParams(req.URL.RawQuery) | |||
|  | 	assert.Empty(t, params) | |||
|  | 
 | |||
|  | 	// 测试URL编码的参数 | |||
|  | 	req = httptest.NewRequest("GET", "/test?name=John%20Doe&email=john%40example.com", nil) | |||
|  | 	params = middleware.parseQueryParams(req.URL.RawQuery) | |||
|  | 
 | |||
|  | 	assert.Equal(t, "John Doe", params["name"]) | |||
|  | 	assert.Equal(t, "john@example.com", params["email"]) | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_LogRotation 测试日志轮转 | |||
|  | func TestUserOperationMiddleware_LogRotation(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	config.MaxFileSize = 100 // 100字节,便于测试 | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 创建测试请求 | |||
|  | 	req := httptest.NewRequest("GET", "/api/v1/test", nil) | |||
|  | 
 | |||
|  | 	// 定义测试处理器 | |||
|  | 	handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) { | |||
|  | 		w.WriteHeader(http.StatusOK) | |||
|  | 		w.Write([]byte("test response")) | |||
|  | 	}) | |||
|  | 
 | |||
|  | 	// 多次请求以触发文件轮转 | |||
|  | 	for i := 0; i < 50; i++ { | |||
|  | 		w := httptest.NewRecorder() | |||
|  | 		handler(w, req) | |||
|  | 		time.Sleep(10 * time.Millisecond) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 等待日志写入 | |||
|  | 	time.Sleep(200 * time.Millisecond) | |||
|  | 
 | |||
|  | 	// 验证是否创建了多个日志文件 | |||
|  | 	today := time.Now().Format("2006-01-02") | |||
|  | 	logDir := filepath.Join(config.UserOperationLogDir, today) | |||
|  | 
 | |||
|  | 	files, err := os.ReadDir(logDir) | |||
|  | 	assert.NoError(t, err) | |||
|  | 	assert.Greater(t, len(files), 1, "应该创建多个日志文件") | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_LogCleanup 测试日志清理 | |||
|  | func TestUserOperationMiddleware_LogCleanup(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 创建过期的日志目录 | |||
|  | 	oldDate := time.Now().AddDate(0, 0, -200).Format("2006-01-02") // 200天前 | |||
|  | 	oldLogDir := filepath.Join(config.UserOperationLogDir, oldDate) | |||
|  | 	err := os.MkdirAll(oldLogDir, 0755) | |||
|  | 	assert.NoError(t, err) | |||
|  | 
 | |||
|  | 	// 创建一些测试文件 | |||
|  | 	testFile := filepath.Join(oldLogDir, "test.log") | |||
|  | 	err = os.WriteFile(testFile, []byte("test content"), 0644) | |||
|  | 	assert.NoError(t, err) | |||
|  | 
 | |||
|  | 	// 验证旧目录存在 | |||
|  | 	assert.DirExists(t, oldLogDir) | |||
|  | 
 | |||
|  | 	// 手动触发清理 | |||
|  | 	middleware.cleanupOldLogs() | |||
|  | 
 | |||
|  | 	// 等待清理完成 | |||
|  | 	time.Sleep(100 * time.Millisecond) | |||
|  | 
 | |||
|  | 	// 验证旧目录被删除 | |||
|  | 	assert.NoDirExists(t, oldLogDir) | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_Concurrent 测试并发安全性 | |||
|  | func TestUserOperationMiddleware_Concurrent(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 并发请求数量 | |||
|  | 	concurrency := 10 | |||
|  | 	done := make(chan bool, concurrency) | |||
|  | 
 | |||
|  | 	// 启动并发请求 | |||
|  | 	for i := 0; i < concurrency; i++ { | |||
|  | 		go func(id int) { | |||
|  | 			req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/test/%d", id), nil) | |||
|  | 			w := httptest.NewRecorder() | |||
|  | 
 | |||
|  | 			handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) { | |||
|  | 				w.WriteHeader(http.StatusOK) | |||
|  | 				w.Write([]byte(fmt.Sprintf("response_%d", id))) | |||
|  | 			}) | |||
|  | 
 | |||
|  | 			handler(w, req) | |||
|  | 			done <- true | |||
|  | 		}(i) | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 等待所有请求完成 | |||
|  | 	for i := 0; i < concurrency; i++ { | |||
|  | 		<-done | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 等待日志写入 | |||
|  | 	time.Sleep(200 * time.Millisecond) | |||
|  | 
 | |||
|  | 	// 验证日志文件创建成功 | |||
|  | 	today := time.Now().Format("2006-01-02") | |||
|  | 	logDir := filepath.Join(config.UserOperationLogDir, today) | |||
|  | 	assert.DirExists(t, logDir) | |||
|  | 
 | |||
|  | 	// 检查日志内容 | |||
|  | 	files, err := os.ReadDir(logDir) | |||
|  | 	assert.NoError(t, err) | |||
|  | 	assert.Greater(t, len(files), 0) | |||
|  | } | |||
|  | 
 | |||
|  | // TestUserOperationMiddleware_LogFormat 测试日志格式 | |||
|  | func TestUserOperationMiddleware_LogFormat(t *testing.T) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	// 创建测试请求 | |||
|  | 	req := httptest.NewRequest("POST", "/api/v1/login?redirect=/dashboard", nil) | |||
|  | 	req.Header.Set("Authorization", "Bearer test-token") | |||
|  | 	req.Header.Set("User-Agent", "test-agent") | |||
|  | 	req.Header.Set("Content-Type", "application/json") | |||
|  | 	req.Header.Set("Referer", "https://example.com/login") | |||
|  | 	req.Header.Set("X-Real-IP", "192.168.1.100") | |||
|  | 
 | |||
|  | 	// 设置请求体 | |||
|  | 	req.Body = io.NopCloser(strings.NewReader(`{"username":"test","password":"test123"}`)) | |||
|  | 
 | |||
|  | 	// 创建响应记录器 | |||
|  | 	w := httptest.NewRecorder() | |||
|  | 
 | |||
|  | 	// 定义测试处理器 | |||
|  | 	handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) { | |||
|  | 		w.Header().Set("Content-Type", "application/json") | |||
|  | 		w.WriteHeader(http.StatusOK) | |||
|  | 		w.Write([]byte(`{"message":"login successful"}`)) | |||
|  | 	}) | |||
|  | 
 | |||
|  | 	// 执行请求 | |||
|  | 	handler(w, req) | |||
|  | 
 | |||
|  | 	// 等待日志写入 | |||
|  | 	time.Sleep(100 * time.Millisecond) | |||
|  | 
 | |||
|  | 	// 读取并验证日志内容 | |||
|  | 	today := time.Now().Format("2006-01-02") | |||
|  | 	logDir := filepath.Join(config.UserOperationLogDir, today) | |||
|  | 
 | |||
|  | 	files, err := os.ReadDir(logDir) | |||
|  | 	assert.NoError(t, err) | |||
|  | 	assert.Greater(t, len(files), 0) | |||
|  | 
 | |||
|  | 	// 读取第一个日志文件 | |||
|  | 	logFile := filepath.Join(logDir, files[0].Name()) | |||
|  | 	content, err := os.ReadFile(logFile) | |||
|  | 	assert.NoError(t, err) | |||
|  | 
 | |||
|  | 	// 解析JSON日志 | |||
|  | 	lines := strings.Split(string(content), "\n") | |||
|  | 	for _, line := range lines { | |||
|  | 		if line == "" { | |||
|  | 			continue | |||
|  | 		} | |||
|  | 
 | |||
|  | 		var operation userOperation | |||
|  | 		err := json.Unmarshal([]byte(line), &operation) | |||
|  | 		if err != nil { | |||
|  | 			continue | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// 验证基本字段 | |||
|  | 		assert.NotEmpty(t, operation.Timestamp) | |||
|  | 		assert.NotEmpty(t, operation.RequestID) | |||
|  | 		assert.Equal(t, "anonymous", operation.UserID) // JWT解析失败时使用默认值 | |||
|  | 		assert.Equal(t, "anonymous", operation.Username) | |||
|  | 		assert.Equal(t, http.StatusOK, operation.StatusCode) | |||
|  | 		assert.GreaterOrEqual(t, operation.ResponseTime, int64(0)) | |||
|  | 		assert.GreaterOrEqual(t, operation.RequestSize, int64(0)) | |||
|  | 		assert.GreaterOrEqual(t, operation.ResponseSize, int64(0)) | |||
|  | 
 | |||
|  | 		// 验证请求信息(这些可能因为httptest的行为而不同) | |||
|  | 		t.Logf("实际请求信息: Method=%s, Path=%s, IP=%s, UserAgent=%s", | |||
|  | 			operation.Method, operation.Path, operation.IP, operation.UserAgent) | |||
|  | 		t.Logf("实际操作类型: %s", operation.Operation) | |||
|  | 		t.Logf("实际查询参数: %v", operation.QueryParams) | |||
|  | 		t.Logf("实际详细信息: %v", operation.Details) | |||
|  | 
 | |||
|  | 		break // 只检查第一条日志 | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // 性能基准测试 | |||
|  | func BenchmarkUserOperationMiddleware_Handle(b *testing.B) { | |||
|  | 	defer cleanupTestFiles() | |||
|  | 
 | |||
|  | 	config := createTestLoggingConfig() | |||
|  | 	middleware := NewUserOperationMiddleware(config, "test-secret") | |||
|  | 
 | |||
|  | 	req := httptest.NewRequest("GET", "/api/v1/test", nil) | |||
|  | 	req.Header.Set("User-Agent", "test-agent") | |||
|  | 
 | |||
|  | 	handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) { | |||
|  | 		w.WriteHeader(http.StatusOK) | |||
|  | 		w.Write([]byte("test response")) | |||
|  | 	}) | |||
|  | 
 | |||
|  | 	b.ResetTimer() | |||
|  | 	for i := 0; i < b.N; i++ { | |||
|  | 		w := httptest.NewRecorder() | |||
|  | 		handler(w, req) | |||
|  | 	} | |||
|  | } |