package common import ( "context" "encoding/json" "fmt" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/apiversion" "github.com/perfect-panel/server/pkg/authmethod" "github.com/perfect-panel/server/pkg/constant" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCheckVerificationCodeCanonicalConsume(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) svcCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{ VerifyCodeExpireTime: 900, }, }, } email := "user@example.com" code := "123456" scene := constant.Register.String() cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email) setEmailCodePayload(t, redisClient, cacheKey, code, time.Now().Unix()) logic := NewCheckVerificationCodeLogic(context.Background(), svcCtx) req := &types.CheckVerificationCodeRequest{ Method: authmethod.Email, Account: email, Code: code, Type: uint8(constant.Register), } resp, err := logic.CheckVerificationCode(req) require.NoError(t, err) require.NotNil(t, resp) assert.True(t, resp.Status) assert.True(t, resp.Exist) exists, err := redisClient.Exists(context.Background(), cacheKey).Result() require.NoError(t, err) assert.Equal(t, int64(0), exists) resp, err = logic.CheckVerificationCode(req) require.NoError(t, err) require.NotNil(t, resp) assert.False(t, resp.Status) assert.False(t, resp.Exist) } func TestCheckVerificationCodeLegacyNoConsumeAndType3Mapping(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) svcCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{ VerifyCodeExpireTime: 900, }, }, } email := "legacy@example.com" code := "654321" scene := constant.Security.String() cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email) setEmailCodePayload(t, redisClient, cacheKey, code, time.Now().Unix()) legacyReq := &types.LegacyCheckVerificationCodeRequest{ Email: email, Code: code, Type: 3, } normalizedReq, type3Mapped, err := NormalizeLegacyCheckVerificationCodeRequest(legacyReq) require.NoError(t, err) assert.True(t, type3Mapped) assert.Equal(t, uint8(constant.Security), normalizedReq.Type) assert.Equal(t, authmethod.Email, normalizedReq.Method) assert.Equal(t, email, normalizedReq.Account) logic := NewCheckVerificationCodeLogic(context.Background(), svcCtx) legacyBehavior := VerifyCodeCheckBehavior{ Source: "legacy", Consume: false, LegacyType3Mapped: true, AllowSceneFallback: true, } resp, err := logic.CheckVerificationCodeWithBehavior(normalizedReq, legacyBehavior) require.NoError(t, err) require.NotNil(t, resp) assert.True(t, resp.Status) assert.True(t, resp.Exist) exists, err := redisClient.Exists(context.Background(), cacheKey).Result() require.NoError(t, err) assert.Equal(t, int64(1), exists) resp, err = logic.CheckVerificationCodeWithBehavior(normalizedReq, legacyBehavior) require.NoError(t, err) assert.True(t, resp.Status) resp, err = logic.CheckVerificationCode(normalizedReq) require.NoError(t, err) assert.True(t, resp.Status) exists, err = redisClient.Exists(context.Background(), cacheKey).Result() require.NoError(t, err) assert.Equal(t, int64(0), exists) } func TestCheckVerificationCodeLegacySceneFallback(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) svcCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{ VerifyCodeExpireTime: 900, }, }, } email := "fallback@example.com" code := "778899" cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register.String(), email) setEmailCodePayload(t, redisClient, cacheKey, code, time.Now().Unix()) logic := NewCheckVerificationCodeLogic(context.Background(), svcCtx) req := &types.CheckVerificationCodeRequest{ Method: authmethod.Email, Account: email, Code: code, Type: uint8(constant.Security), } resp, err := logic.CheckVerificationCodeWithBehavior(req, VerifyCodeCheckBehavior{ Source: "legacy", Consume: false, AllowSceneFallback: true, }) require.NoError(t, err) require.NotNil(t, resp) assert.True(t, resp.Status) resp, err = logic.CheckVerificationCodeWithBehavior(req, VerifyCodeCheckBehavior{ Source: "legacy", Consume: false, AllowSceneFallback: false, }) require.NoError(t, err) require.NotNil(t, resp) assert.False(t, resp.Status) } func setEmailCodePayload(t *testing.T, redisClient *redis.Client, cacheKey string, code string, lastAt int64) { t.Helper() payload := CacheKeyPayload{ Code: code, LastAt: lastAt, } value, err := json.Marshal(payload) require.NoError(t, err) err = redisClient.Set(context.Background(), cacheKey, value, time.Minute*15).Err() require.NoError(t, err) } func TestCheckVerificationCodeWithApiHeaderGate(t *testing.T) { tests := []struct { name string header string expectConsume bool }{ {name: "missing header", header: "", expectConsume: false}, {name: "invalid header", header: "invalid", expectConsume: false}, {name: "equal threshold", header: "1.0.0", expectConsume: false}, {name: "greater threshold", header: "1.0.1", expectConsume: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) svcCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{ VerifyCodeExpireTime: 900, }, }, } email := "gate@example.com" code := "101010" cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register.String(), email) setEmailCodePayload(t, redisClient, cacheKey, code, time.Now().Unix()) logic := NewCheckVerificationCodeLogic(context.Background(), svcCtx) req := &types.CheckVerificationCodeRequest{ Method: authmethod.Email, Account: email, Code: code, Type: uint8(constant.Register), } resp, err := logic.CheckVerificationCodeWithBehavior(req, VerifyCodeCheckBehavior{ Source: "canonical", Consume: apiversion.UseLatest(tt.header, apiversion.DefaultThreshold), }) require.NoError(t, err) require.NotNil(t, resp) assert.True(t, resp.Status) exists, err := redisClient.Exists(context.Background(), cacheKey).Result() require.NoError(t, err) if tt.expectConsume { assert.Equal(t, int64(0), exists) } else { assert.Equal(t, int64(1), exists) } }) } }