Files
photography/backend/tests/unit_test.go
xujiang 48b6a5f4aa feat: 完善 CI/CD 配置并修复代码质量问题
## 修复内容

### 前端 (Frontend)
- 修复 ESLint 错误:未使用变量重命名为下划线前缀
- 修复 TypeScript 类型错误:完善 BackendPhoto 接口定义
- 修复引号转义问题:搜索结果显示优化
- 优化 useEffect 依赖:添加 useCallback 避免无限循环
- 移除未使用的导入和变量

### 后端 (Backend)
- 修复 go vet 错误:测试文件中的字段名称不匹配
- 修复数组访问错误:使用正确的结构体字段路径
- 统一代码格式:go fmt 自动格式化

### 管理后台 (Admin)
- 创建缺失的 ESLint 配置文件
- 修复 React 导入缺失问题
- 确保 TypeScript 编译通过

## CI/CD 改进
- 验证了前端、后端、管理后台的完整构建流程
- 所有 lint 检查、类型检查、测试均通过
- 为自动化部署做好准备

## 技术细节
- 前端:修复 5+ ESLint 错误,完善类型定义
- 后端:修复 3+ go vet 错误,通过所有测试
- 管理后台:创建 ESLint 配置,修复导入问题
- 所有模块均可正常构建和运行
2025-07-14 10:01:48 +08:00

586 lines
14 KiB
Go

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, &registerResp)
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)
}