issue_49 #51
@@ -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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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.")
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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, "空密码不应被哈希")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user