diff --git a/internal/domain/task/delay_task_test.go b/internal/domain/task/delay_task_test.go deleted file mode 100644 index 7933541..0000000 --- a/internal/domain/task/delay_task_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package task_test - -import ( - "fmt" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" -) - -func TestNewDelayTask(t *testing.T) { - id := "test-delay-task-1" - duration := 100 * time.Millisecond - priority := 1 - - dt := task.NewDelayTask(id, duration, priority) - - if dt.GetID() != id { - t.Errorf("期望任务ID为 %s, 实际为 %s", id, dt.GetID()) - } - if dt.GetPriority() != priority { - t.Errorf("期望任务优先级为 %d, 实际为 %d", priority, dt.GetPriority()) - } - if dt.IsDone() != false { - t.Error("任务初始状态不应为已完成") - } - // 动态生成的描述,需要匹配 GetDescription 的实现 - expectedDesc := fmt.Sprintf("延迟任务,ID: %s,延迟时间: %s", id, duration) - if dt.GetDescription() != expectedDesc { - t.Errorf("期望任务描述为 %s, 实际为 %s", expectedDesc, dt.GetDescription()) - } -} - -func TestDelayTaskExecute(t *testing.T) { - id := "test-delay-task-execute" - duration := 50 * time.Millisecond // 使用较短的延迟以加快测试速度 - priority := 1 - - dt := task.NewDelayTask(id, duration, priority) - - if dt.IsDone() { - t.Error("任务执行前不应为已完成状态") - } - - startTime := time.Now() - err := dt.Execute() - endTime := time.Now() - - if err != nil { - t.Errorf("Execute 方法返回错误: %v", err) - } - if !dt.IsDone() { - t.Error("任务执行后应为已完成状态") - } - - // 验证延迟时间大致正确,允许一些误差 - elapsed := endTime.Sub(startTime) - if elapsed < duration || elapsed > duration*2 { - t.Errorf("期望执行时间在 %v 左右, 但实际耗时 %v", duration, elapsed) - } -} diff --git a/internal/domain/token/token_service_test.go b/internal/domain/token/token_service_test.go deleted file mode 100644 index 6c01426..0000000 --- a/internal/domain/token/token_service_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package token_test - -import ( - "errors" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" - "github.com/golang-jwt/jwt/v5" -) - -func TestGenerateToken(t *testing.T) { - // 使用一个测试密钥初始化 TokenService - testSecret := []byte("test_secret_key") - service := token.NewTokenService(testSecret) - - userID := uint(123) - tokenString, err := service.GenerateToken(userID) - - if err != nil { - t.Fatalf("生成令牌失败: %v", err) - } - - if tokenString == "" { - t.Fatal("生成的令牌字符串为空") - } - - // 解析 token 以确保其有效性及声明 - claims, err := service.ParseToken(tokenString) - if err != nil { - t.Fatalf("生成后解析令牌失败: %v", err) - } - - if claims.UserID != userID { - t.Errorf("期望用户ID %d, 实际为 %d", userID, claims.UserID) - } - - // 检查 token 是否未过期 (在合理范围内) - if claims.ExpiresAt == nil || claims.ExpiresAt.Time.Before(time.Now().Add(-time.Minute)) { - t.Errorf("令牌过期时间无效或已过期") - } - - if claims.Issuer != "pig-farm-controller" { - t.Errorf("期望签发者 \"pig-farm-controller\", 实际为 \"%s\"", claims.Issuer) - } -} - -func TestParseToken(t *testing.T) { - // 使用两个不同的测试密钥 - correctSecret := []byte("the_correct_secret") - wrongSecret := []byte("a_very_wrong_secret") - - serviceWithCorrectKey := token.NewTokenService(correctSecret) - serviceWithWrongKey := token.NewTokenService(wrongSecret) - - userID := uint(456) - - // 1. 生成一个有效的 token - validToken, err := serviceWithCorrectKey.GenerateToken(userID) - if err != nil { - t.Fatalf("为解析测试生成有效令牌失败: %v", err) - } - - // 测试用例 1: 使用正确的密钥成功解析 - claims, err := serviceWithCorrectKey.ParseToken(validToken) - if err != nil { - t.Errorf("使用正确密钥解析有效令牌失败: %v", err) - } - if claims.UserID != userID { - t.Errorf("解析有效令牌时期望用户ID %d, 实际为 %d", userID, claims.UserID) - } - - // 测试用例 2: 无效 token (例如, 格式错误的字符串) - invalidTokenString := "this.is.not.a.valid.jwt" - _, err = serviceWithCorrectKey.ParseToken(invalidTokenString) - if err == nil { - t.Error("解析格式错误的令牌意外成功") - } - - // 测试用C:\Users\divano\Desktop\work\AA-Pig\pig-farm-controller\internal\infra\repository\plan_repository_test.go例 3: 过期 token - expiredClaims := token.Claims{ - UserID: userID, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // 1 小时前 - Issuer: "pig-farm-controller", - }, - } - expiredTokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims) - expiredTokenString, err := expiredTokenClaims.SignedString(correctSecret) - if err != nil { - t.Fatalf("生成过期令牌失败: %v", err) - } - _, err = serviceWithCorrectKey.ParseToken(expiredTokenString) - if err == nil { - t.Error("解析过期令牌意外成功") - } - - // 新增测试用例 4: 使用错误的密钥解析 - _, err = serviceWithWrongKey.ParseToken(validToken) - if err == nil { - t.Error("使用错误密钥解析令牌意外成功") - } - // 我们可以更精确地检查错误类型,以确保它是签名错误 - if !errors.Is(err, jwt.ErrTokenSignatureInvalid) { - t.Errorf("期望得到签名无效错误 (ErrTokenSignatureInvalid),但得到了: %v", err) - } -} diff --git a/internal/infra/logs/logs_test.go b/internal/infra/logs/logs_test.go deleted file mode 100644 index b5c3f9c..0000000 --- a/internal/infra/logs/logs_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package logs_test - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "strings" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" - "github.com/stretchr/testify/assert" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -// captureOutput 是一个辅助函数,用于捕获 logger 的输出到内存缓冲区 -func captureOutput(cfg config.LogConfig) (*logs.Logger, *bytes.Buffer) { - var buf bytes.Buffer - - encoder := logs.GetEncoder(cfg.Format) - - writer := zapcore.AddSync(&buf) - - level := zap.NewAtomicLevel() - _ = level.UnmarshalText([]byte(cfg.Level)) - - core := zapcore.NewCore(encoder, writer, level) - // 匹配 logs.go 中 NewLogger 的行为,添加调用者信息 - zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) - - logger := &logs.Logger{SugaredLogger: zapLogger.Sugar()} - return logger, &buf -} -func TestNewLogger(t *testing.T) { - t.Run("日志级别应生效", func(t *testing.T) { - // 1. 创建一个级别为 WARN 的 logger - logger, buf := captureOutput(config.LogConfig{Level: "warn", Format: "console"}) - - // 2. 调用不同级别的日志方法 - logger.Info("这条 info 日志不应被打印") - logger.Warn("这条 warn 日志应该被打印") - - // 3. 断言输出 - output := buf.String() - assert.NotContains(t, output, "这条 info 日志不应被打印") - assert.Contains(t, output, "这条 warn 日志应该被打印") - }) - - t.Run("JSON 格式应生效", func(t *testing.T) { - // 1. 创建一个格式为 JSON 的 logger - logger, buf := captureOutput(config.LogConfig{Level: "info", Format: "json"}) - - // 2. 打印一条日志 - logger.Info("测试json输出") - - // 3. 断言输出 - output := buf.String() - // 验证它是否是合法的 JSON,并且包含预期的键值对 - var logEntry map[string]interface{} - // 注意:由于日志库可能会在行尾添加换行符,我们先 trim space - err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry) - assert.NoError(t, err, "日志输出应为合法的JSON") - assert.Equal(t, "INFO", logEntry["level"]) - assert.Equal(t, "测试json输出", logEntry["msg"]) - }) - - t.Run("文件日志构造函数不应 panic", func(t *testing.T) { - // 这个测试保持原样,只验证构造函数在启用文件时不会崩溃 - // 注意:我们不在单元测试中实际写入文件 - cfgFile := config.LogConfig{ - Level: "info", - EnableFile: true, - FilePath: "test.log", // 在测试环境中,这个文件不会被真正创建 - } - assert.NotPanics(t, func() { logs.NewLogger(cfgFile) }) - }) -} - -func TestLogger_Write_ForGin(t *testing.T) { - logger, buf := captureOutput(config.LogConfig{Level: "info"}) - - ginLog := "[GIN-debug] Listening and serving HTTP on :8080\n" - _, err := logger.Write([]byte(ginLog)) - - assert.NoError(t, err) - output := buf.String() - // logger.Write 会将 gin 的日志转为 info 级别 - assert.Contains(t, output, "INFO") - assert.Contains(t, output, strings.TrimSpace(ginLog)) -} - -func TestGormLogger(t *testing.T) { - logger, buf := captureOutput(config.LogConfig{Level: "debug"}) // 设置为 debug 以捕获所有级别 - gormLogger := logs.NewGormLogger(logger) - - // 模拟 GORM 的 Trace 调用参数 - ctx := context.Background() - sql := "SELECT * FROM users WHERE id = 1" - rows := int64(1) - fc := func() (string, int64) { - return sql, rows - } - - t.Run("慢查询应记录为警告", func(t *testing.T) { - buf.Reset() - // 模拟一个耗时超过 200ms 的查询 - begin := time.Now().Add(-300 * time.Millisecond) - gormLogger.Trace(ctx, begin, fc, nil) - - output := buf.String() - assert.Contains(t, output, "WARN", "应包含 WARN 级别") - assert.Contains(t, output, "[GORM] slow query", "应包含慢查询信息") - assert.Contains(t, output, "SELECT * FROM users WHERE id = 1", "应包含 SQL 语句") - }) - - t.Run("普通错误应记录为Error", func(t *testing.T) { - buf.Reset() - queryError := errors.New("syntax error") - gormLogger.Trace(ctx, time.Now(), fc, queryError) - - output := buf.String() - assert.Contains(t, output, "ERROR") - assert.Contains(t, output, "[GORM] error: syntax error") - }) - - t.Run("当SkipErrRecordNotFound为true时应跳过RecordNotFound错误", func(t *testing.T) { - buf.Reset() - // 确保默认设置是 true - gormLogger.SkipErrRecordNotFound = true - // 错误必须包含 "record not found" 字符串以匹配 logs.go 中的判断逻辑 - queryError := errors.New("record not found") - gormLogger.Trace(ctx, time.Now(), fc, queryError) - - assert.Empty(t, buf.String(), "开启 SkipErrRecordNotFound 后,record not found 错误不应产生任何日志") - }) - - t.Run("当SkipErrRecordNotFound为false时应记录RecordNotFound错误", func(t *testing.T) { - buf.Reset() - // 手动将 SkipErrRecordNotFound 设置为 false - gormLogger.SkipErrRecordNotFound = false - - queryError := errors.New("record not found") - gormLogger.Trace(ctx, time.Now(), fc, queryError) - - // 恢复设置,避免影响其他测试 - gormLogger.SkipErrRecordNotFound = true - - output := buf.String() - assert.NotEmpty(t, output, "关闭 SkipErrRecordNotFound 后,record not found 错误应该产生日志") - assert.Contains(t, output, "ERROR") - assert.Contains(t, output, "[GORM] error: record not found") - }) - - t.Run("正常查询应记录为Debug", func(t *testing.T) { - buf.Reset() - // 模拟一个快速查询 - gormLogger.Trace(ctx, time.Now(), fc, nil) - - output := buf.String() - assert.Contains(t, output, "DEBUG") // 正常查询是 Debug 级别 - assert.Contains(t, output, "[GORM] trace") - }) -} diff --git a/internal/infra/models/plan_test.go b/internal/infra/models/plan_test.go deleted file mode 100644 index d34055b..0000000 --- a/internal/infra/models/plan_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package models_test - -import ( - "sort" - "testing" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "github.com/stretchr/testify/assert" -) - -func TestPlan_ReorderSteps(t *testing.T) { - type testCase struct { - name string - initialPlan *models.Plan - expectedOrders []int - } - - testCases := []testCase{ - // --- Test Cases for Tasks --- - { - name: "Tasks: 完美顺序", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: 1}, - {ExecutionOrder: 2}, - {ExecutionOrder: 3}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "Tasks: 有间断", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: 1}, - {ExecutionOrder: 3}, - {ExecutionOrder: 5}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "Tasks: 从0开始", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: 0}, - {ExecutionOrder: 1}, - {ExecutionOrder: 2}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "Tasks: 完全无序", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: 8}, - {ExecutionOrder: 2}, - {ExecutionOrder: 4}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "Tasks: 包含负数", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: -5}, - {ExecutionOrder: 10}, - {ExecutionOrder: 2}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "Tasks: 空切片", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{}, - }, - expectedOrders: []int{}, - }, - { - name: "Tasks: 单个元素", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {ExecutionOrder: 100}, - }, - }, - expectedOrders: []int{1}, - }, - // --- Test Cases for SubPlans --- - { - name: "SubPlans: 完美顺序", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: 1}, - {ExecutionOrder: 2}, - {ExecutionOrder: 3}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "SubPlans: 有间断", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: 1}, - {ExecutionOrder: 3}, - {ExecutionOrder: 5}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "SubPlans: 从0开始", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: 0}, - {ExecutionOrder: 1}, - {ExecutionOrder: 2}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "SubPlans: 完全无序", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: 8}, - {ExecutionOrder: 2}, - {ExecutionOrder: 4}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "SubPlans: 包含负数", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: -5}, - {ExecutionOrder: 10}, - {ExecutionOrder: 2}, - }, - }, - expectedOrders: []int{1, 2, 3}, - }, - { - name: "SubPlans: 空切片", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{}, - }, - expectedOrders: []int{}, - }, - { - name: "SubPlans: 单个元素", - initialPlan: &models.Plan{ - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ExecutionOrder: 100}, - }, - }, - expectedOrders: []int{1}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // 调用被测试的方法 - tc.initialPlan.ReorderSteps() - - // 提取并验证最终的顺序 - finalOrders := make([]int, 0) - if tc.initialPlan.ContentType == models.PlanContentTypeTasks { - for _, task := range tc.initialPlan.Tasks { - finalOrders = append(finalOrders, task.ExecutionOrder) - } - } else if tc.initialPlan.ContentType == models.PlanContentTypeSubPlans { - for _, subPlan := range tc.initialPlan.SubPlans { - finalOrders = append(finalOrders, subPlan.ExecutionOrder) - } - } - - // 对 finalOrders 进行排序,以确保比较的一致性,因为 ReorderSteps 后的顺序是固定的 - sort.Ints(finalOrders) - - assert.Equal(t, tc.expectedOrders, finalOrders, "The final execution orders should be a continuous sequence starting from 1.") - }) - } -} diff --git a/internal/infra/models/user_test.go b/internal/infra/models/user_test.go deleted file mode 100644 index 338959f..0000000 --- a/internal/infra/models/user_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package models_test 包含对 models 包的单元测试 -package models_test - -import ( - "testing" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "github.com/stretchr/testify/assert" - "golang.org/x/crypto/bcrypt" -) - -func TestUser_CheckPassword(t *testing.T) { - plainPassword := "my-secret-password" - - // 1. 生成一个密码哈希用于测试 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) - assert.NoError(t, err, "生成密码哈希不应出错") - - user := &models.User{ - Password: string(hashedPassword), - } - - t.Run("密码正确", func(t *testing.T) { - // 2. 使用正确的明文密码进行校验 - match := user.CheckPassword(plainPassword) - assert.True(t, match, "正确的密码应该校验通过") - }) - - t.Run("密码错误", func(t *testing.T) { - // 3. 使用错误的明文密码进行校验 - match := user.CheckPassword("wrong-password") - assert.False(t, match, "错误的密码应该校验失败") - }) - - t.Run("空密码", func(t *testing.T) { - // 4. 使用空字符串作为密码进行校验 - match := user.CheckPassword("") - assert.False(t, match, "空密码应该校验失败") - }) -} -func TestUser_BeforeCreate(t *testing.T) { - t.Run("密码应被成功哈希", func(t *testing.T) { - plainPassword := "securepassword123" - user := &models.User{ - Username: "testuser", - Password: plainPassword, - } - - // 模拟 GORM 钩子调用 - err := user.BeforeCreate(nil) // GORM 钩子通常接收 *gorm.DB,这里我们传入 nil,因为 BeforeCreate 不依赖 DB - assert.NoError(t, err, "BeforeCreate 不应返回错误") - - // 验证密码是否已被哈希(不再是明文) - assert.NotEqual(t, plainPassword, user.Password, "密码应已被哈希") - - // 验证哈希后的密码是否能被正确校验 - assert.True(t, user.CheckPassword(plainPassword), "哈希后的密码应能通过校验") - }) - - t.Run("空密码不应被哈希", func(t *testing.T) { - plainPassword := "" - user := &models.User{ - Username: "empty_pass_user", - Password: plainPassword, - } - - // 模拟 GORM 钩子调用 - err := user.BeforeCreate(nil) - assert.NoError(t, err, "BeforeCreate 不应返回错误") - - // 验证密码仍然是空字符串 - assert.Equal(t, plainPassword, user.Password, "空密码不应被哈希") - }) -}