package tests import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "photography-backend/internal/config" "photography-backend/internal/handler" "photography-backend/internal/svc" "photography-backend/internal/types" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/rest" ) // TestContext 测试上下文 type TestContext struct { server *rest.Server svcCtx *svc.ServiceContext cfg config.Config authToken string } // SetupTestEnvironment 设置测试环境 func SetupTestEnvironment(t *testing.T) *TestContext { // 加载测试配置 var cfg config.Config conf.MustLoad("../etc/photography-api.yaml", &cfg) // 使用内存数据库进行测试 cfg.Database.Driver = "sqlite" cfg.Database.FilePath = ":memory:" // 创建服务上下文 svcCtx := svc.NewServiceContext(cfg) // 创建 REST 服务器 server := rest.MustNewServer(rest.RestConf{ ServiceConf: cfg.ServiceConf, Port: 0, // 使用随机端口 }) // 注册路由 handler.RegisterHandlers(server, svcCtx) return &TestContext{ server: server, svcCtx: svcCtx, cfg: cfg, } } // StartServer 启动测试服务器 func (tc *TestContext) StartServer() { go tc.server.Start() time.Sleep(100 * time.Millisecond) // 等待服务器启动 } // StopServer 停止测试服务器 func (tc *TestContext) StopServer() { tc.server.Stop() } // Login 登录并获取token func (tc *TestContext) Login(t *testing.T) { loginReq := types.LoginRequest{ Username: "admin", Password: "admin123", } respBody, err := tc.PostJSON("/api/v1/auth/login", loginReq) require.NoError(t, err) var resp types.LoginResponse err = json.Unmarshal(respBody, &resp) require.NoError(t, err) tc.authToken = resp.Data.Token } // PostJSON 发送 POST JSON 请求 func (tc *TestContext) PostJSON(path string, data interface{}) ([]byte, error) { body, err := json.Marshal(data) if err != nil { return nil, err } req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") if tc.authToken != "" { req.Header.Set("Authorization", "Bearer "+tc.authToken) } w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) return w.Body.Bytes(), nil } // GetJSON 发送 GET JSON 请求 func (tc *TestContext) GetJSON(path string) ([]byte, error) { req := httptest.NewRequest(http.MethodGet, path, nil) if tc.authToken != "" { req.Header.Set("Authorization", "Bearer "+tc.authToken) } w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) return w.Body.Bytes(), nil } // PutJSON 发送 PUT JSON 请求 func (tc *TestContext) PutJSON(path string, data interface{}) ([]byte, error) { body, err := json.Marshal(data) if err != nil { return nil, err } req := httptest.NewRequest(http.MethodPut, path, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") if tc.authToken != "" { req.Header.Set("Authorization", "Bearer "+tc.authToken) } w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) return w.Body.Bytes(), nil } // DeleteJSON 发送 DELETE 请求 func (tc *TestContext) DeleteJSON(path string) ([]byte, error) { req := httptest.NewRequest(http.MethodDelete, path, nil) if tc.authToken != "" { req.Header.Set("Authorization", "Bearer "+tc.authToken) } w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) return w.Body.Bytes(), nil } // TestHealthCheck 健康检查接口测试 func TestHealthCheck(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() respBody, err := tc.GetJSON("/api/v1/health") require.NoError(t, err) var resp types.BaseResponse err = json.Unmarshal(respBody, &resp) require.NoError(t, err) assert.Equal(t, 200, resp.Code) assert.Equal(t, "success", resp.Message) } // TestAuthFlow 认证流程测试 func TestAuthFlow(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试注册 registerReq := types.RegisterRequest{ Username: "testuser", Password: "testpass123", Email: "test@example.com", } respBody, err := tc.PostJSON("/api/v1/auth/register", registerReq) require.NoError(t, err) var registerResp types.RegisterResponse err = json.Unmarshal(respBody, ®isterResp) require.NoError(t, err) assert.Equal(t, 200, registerResp.Code) assert.NotEmpty(t, registerResp.Data.Token) // 测试登录 loginReq := types.LoginRequest{ Username: "testuser", Password: "testpass123", } respBody, err = tc.PostJSON("/api/v1/auth/login", loginReq) require.NoError(t, err) var loginResp types.LoginResponse err = json.Unmarshal(respBody, &loginResp) require.NoError(t, err) assert.Equal(t, 200, loginResp.Code) assert.NotEmpty(t, loginResp.Data.Token) // 测试无效凭证 invalidLoginReq := types.LoginRequest{ Username: "testuser", Password: "wrongpassword", } respBody, err = tc.PostJSON("/api/v1/auth/login", invalidLoginReq) require.NoError(t, err) var invalidResp types.LoginResponse err = json.Unmarshal(respBody, &invalidResp) require.NoError(t, err) assert.NotEqual(t, 200, invalidResp.Code) } // TestUserCRUD 用户 CRUD 测试 func TestUserCRUD(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 先登录获取token tc.Login(t) // 测试创建用户 createReq := types.CreateUserRequest{ Username: "newuser", Password: "newpass123", Email: "newuser@example.com", } respBody, err := tc.PostJSON("/api/v1/users", createReq) require.NoError(t, err) var createResp types.CreateUserResponse err = json.Unmarshal(respBody, &createResp) require.NoError(t, err) assert.Equal(t, 200, createResp.Code) userID := createResp.Data.ID // 测试获取用户 respBody, err = tc.GetJSON(fmt.Sprintf("/api/v1/users/%d", userID)) require.NoError(t, err) var getResp types.GetUserResponse err = json.Unmarshal(respBody, &getResp) require.NoError(t, err) assert.Equal(t, 200, getResp.Code) assert.Equal(t, "newuser", getResp.Data.Username) // 测试更新用户 updateReq := types.UpdateUserRequest{ Username: "updateduser", Email: "updated@example.com", } respBody, err = tc.PutJSON(fmt.Sprintf("/api/v1/users/%d", userID), updateReq) require.NoError(t, err) var updateResp types.UpdateUserResponse err = json.Unmarshal(respBody, &updateResp) require.NoError(t, err) assert.Equal(t, 200, updateResp.Code) // 测试删除用户 respBody, err = tc.DeleteJSON(fmt.Sprintf("/api/v1/users/%d", userID)) require.NoError(t, err) var deleteResp types.DeleteUserResponse err = json.Unmarshal(respBody, &deleteResp) require.NoError(t, err) assert.Equal(t, 200, deleteResp.Code) } // TestCategoryCRUD 分类 CRUD 测试 func TestCategoryCRUD(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 先登录获取token tc.Login(t) // 测试创建分类 createReq := types.CreateCategoryRequest{ Name: "测试分类", Description: "这是一个测试分类", } respBody, err := tc.PostJSON("/api/v1/categories", createReq) require.NoError(t, err) var createResp types.CreateCategoryResponse err = json.Unmarshal(respBody, &createResp) require.NoError(t, err) assert.Equal(t, 200, createResp.Code) categoryID := createResp.Data.ID // 测试获取分类列表 respBody, err = tc.GetJSON("/api/v1/categories") require.NoError(t, err) var listResp types.GetCategoryListResponse err = json.Unmarshal(respBody, &listResp) require.NoError(t, err) assert.Equal(t, 200, listResp.Code) assert.GreaterOrEqual(t, len(listResp.Data), 1) // 测试更新分类 updateReq := types.UpdateCategoryRequest{ Name: "更新的分类", Description: "这是一个更新的分类", } respBody, err = tc.PutJSON(fmt.Sprintf("/api/v1/categories/%d", categoryID), updateReq) require.NoError(t, err) var updateResp types.UpdateCategoryResponse err = json.Unmarshal(respBody, &updateResp) require.NoError(t, err) assert.Equal(t, 200, updateResp.Code) } // TestPhotoUpload 照片上传测试 func TestPhotoUpload(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 先登录获取token tc.Login(t) // 创建测试图片文件 testImageContent := []byte("fake image content") // 创建 multipart form var buf bytes.Buffer writer := multipart.NewWriter(&buf) // 添加文件字段 part, err := writer.CreateFormFile("file", "test.jpg") require.NoError(t, err) _, err = part.Write(testImageContent) require.NoError(t, err) // 添加其他字段 _ = writer.WriteField("title", "测试照片") _ = writer.WriteField("description", "这是一个测试照片") _ = writer.WriteField("category_id", "1") err = writer.Close() require.NoError(t, err) // 发送请求 req := httptest.NewRequest(http.MethodPost, "/api/v1/photos", &buf) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tc.authToken) w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) respBody := w.Body.Bytes() var resp types.UploadPhotoResponse err = json.Unmarshal(respBody, &resp) require.NoError(t, err) assert.Equal(t, 200, resp.Code) assert.NotEmpty(t, resp.Data.ID) } // TestPhotoList 照片列表测试 func TestPhotoList(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 先登录获取token tc.Login(t) // 测试获取照片列表 respBody, err := tc.GetJSON("/api/v1/photos?page=1&limit=10") require.NoError(t, err) var resp types.GetPhotoListResponse err = json.Unmarshal(respBody, &resp) require.NoError(t, err) assert.Equal(t, 200, resp.Code) assert.IsType(t, []types.Photo{}, resp.Data) } // TestMiddleware 中间件测试 func TestMiddleware(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试 CORS 中间件 req := httptest.NewRequest(http.MethodOptions, "/api/v1/health", nil) req.Header.Set("Origin", "http://localhost:3000") req.Header.Set("Access-Control-Request-Method", "GET") w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) assert.Equal(t, "http://localhost:3000", w.Header().Get("Access-Control-Allow-Origin")) assert.Contains(t, w.Header().Get("Access-Control-Allow-Methods"), "GET") // 测试认证中间件 req = httptest.NewRequest(http.MethodGet, "/api/v1/users", nil) w = httptest.NewRecorder() tc.server.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // TestErrorHandling 错误处理测试 func TestErrorHandling(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试不存在的接口 respBody, err := tc.GetJSON("/api/v1/nonexistent") require.NoError(t, err) var resp types.BaseResponse err = json.Unmarshal(respBody, &resp) require.NoError(t, err) assert.NotEqual(t, 200, resp.Code) // 测试无效的 JSON req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader("invalid json")) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() tc.server.ServeHTTP(w, req) assert.NotEqual(t, http.StatusOK, w.Code) } // TestPerformance 性能测试 func TestPerformance(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试健康检查接口的性能 start := time.Now() for i := 0; i < 100; i++ { _, err := tc.GetJSON("/api/v1/health") require.NoError(t, err) } duration := time.Since(start) avgDuration := duration / 100 // 平均响应时间应该小于 10ms assert.Less(t, avgDuration, 10*time.Millisecond) t.Logf("Average response time: %v", avgDuration) } // TestConcurrency 并发测试 func TestConcurrency(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 并发测试健康检查接口 concurrency := 50 done := make(chan bool, concurrency) for i := 0; i < concurrency; i++ { go func() { _, err := tc.GetJSON("/api/v1/health") assert.NoError(t, err) done <- true }() } // 等待所有请求完成 for i := 0; i < concurrency; i++ { <-done } t.Log("Concurrency test completed successfully") } // TestFileOperations 文件操作测试 func TestFileOperations(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试文件上传目录创建 uploadDir := "test_uploads" err := os.MkdirAll(uploadDir, 0755) require.NoError(t, err) defer os.RemoveAll(uploadDir) // 测试文件写入 testFile := filepath.Join(uploadDir, "test.txt") content := []byte("test content") err = os.WriteFile(testFile, content, 0644) require.NoError(t, err) // 测试文件读取 readContent, err := os.ReadFile(testFile) require.NoError(t, err) assert.Equal(t, content, readContent) // 测试文件删除 err = os.Remove(testFile) require.NoError(t, err) _, err = os.Stat(testFile) assert.True(t, os.IsNotExist(err)) } // TestDatabaseOperations 数据库操作测试 func TestDatabaseOperations(t *testing.T) { tc := SetupTestEnvironment(t) defer tc.StopServer() tc.StartServer() // 测试数据库连接 assert.NotNil(t, tc.svcCtx.DB) // 测试数据库查询 var count int64 err := tc.svcCtx.DB.Table("users").Count(&count).Error require.NoError(t, err) assert.GreaterOrEqual(t, count, int64(0)) } // TestConfigValidation 配置验证测试 func TestConfigValidation(t *testing.T) { tc := SetupTestEnvironment(t) // 测试配置加载 assert.NotEmpty(t, tc.cfg.Name) assert.NotEmpty(t, tc.cfg.Host) assert.Greater(t, tc.cfg.Port, 0) // 测试数据库配置 assert.NotEmpty(t, tc.cfg.Database.Driver) // 测试认证配置 assert.NotEmpty(t, tc.cfg.Auth.AccessSecret) assert.Greater(t, tc.cfg.Auth.AccessExpire, int64(0)) // 测试文件上传配置 assert.Greater(t, tc.cfg.FileUpload.MaxSize, int64(0)) assert.NotEmpty(t, tc.cfg.FileUpload.UploadDir) }