package user import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/gin-gonic/gin" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/constant" "github.com/redis/go-redis/v9" ) type handlerResponse struct { Code uint32 `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } func newDeleteAccountTestRouter(serverCtx *svc.ServiceContext) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/v1/public/user/delete_account", DeleteAccountHandler(serverCtx)) return router } func TestDeleteAccountHandlerInvalidParamsUsesUnifiedResponse(t *testing.T) { router := newDeleteAccountTestRouter(&svc.ServiceContext{}) reqBody := bytes.NewBufferString(`{"email":"invalid-email"}`) req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody) req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, req) if recorder.Code != http.StatusOK { t.Fatalf("expected HTTP 200, got %d", recorder.Code) } var resp handlerResponse if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } if resp.Code != 400 { t.Fatalf("expected business code 400, got %d, body=%s", resp.Code, recorder.Body.String()) } var raw map[string]interface{} if err := json.Unmarshal(recorder.Body.Bytes(), &raw); err != nil { t.Fatalf("failed to decode raw response: %v", err) } if _, exists := raw["error"]; exists { t.Fatalf("unexpected raw error field in response: %s", recorder.Body.String()) } } func TestDeleteAccountHandlerVerifyCodeErrorUsesUnifiedResponse(t *testing.T) { redisClient := redis.NewClient(&redis.Options{ Addr: "invalid:6379", Dialer: func(_ context.Context, _, _ string) (net.Conn, error) { return nil, errors.New("dial disabled in test") }, }) defer redisClient.Close() serverCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{ VerifyCodeExpireTime: 900, }, }, } router := newDeleteAccountTestRouter(serverCtx) reqBody := bytes.NewBufferString(`{"email":"user@example.com","code":"123456"}`) req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody) req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, req) if recorder.Code != http.StatusOK { t.Fatalf("expected HTTP 200, got %d", recorder.Code) } var resp handlerResponse if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } if resp.Code != 70001 { t.Fatalf("expected business code 70001, got %d, body=%s", resp.Code, recorder.Body.String()) } } func TestVerifyEmailCode_DeleteAccountSceneConsume(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) serverCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900}, }, } email := "delete-account@example.com" code := "112233" cacheKey := seedDeleteSceneCode(t, redisClient, constant.DeleteAccount.String(), email, code) err := verifyEmailCode(context.Background(), serverCtx, email, code) if err != nil { t.Fatalf("verifyEmailCode returned unexpected error: %v", err) } exists, err := redisClient.Exists(context.Background(), cacheKey).Result() if err != nil { t.Fatalf("failed to check redis key: %v", err) } if exists != 0 { t.Fatalf("expected verification code to be consumed, key still exists") } } func TestVerifyEmailCode_SecurityFallbackConsume(t *testing.T) { miniRedis := miniredis.RunT(t) redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()}) t.Cleanup(func() { redisClient.Close() miniRedis.Close() }) serverCtx := &svc.ServiceContext{ Redis: redisClient, Config: config.Config{ VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900}, }, } email := "security-fallback@example.com" code := "445566" cacheKey := seedDeleteSceneCode(t, redisClient, constant.Security.String(), email, code) err := verifyEmailCode(context.Background(), serverCtx, email, code) if err != nil { t.Fatalf("verifyEmailCode fallback returned unexpected error: %v", err) } exists, err := redisClient.Exists(context.Background(), cacheKey).Result() if err != nil { t.Fatalf("failed to check redis key: %v", err) } if exists != 0 { t.Fatalf("expected fallback verification code to be consumed, key still exists") } } func seedDeleteSceneCode(t *testing.T, redisClient *redis.Client, scene string, email string, code string) string { t.Helper() cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email) payload := map[string]interface{}{ "code": code, "lastAt": time.Now().Unix(), } payloadRaw, err := json.Marshal(payload) if err != nil { t.Fatalf("failed to marshal payload: %v", err) } err = redisClient.Set(context.Background(), cacheKey, payloadRaw, time.Minute*15).Err() if err != nil { t.Fatalf("failed to seed redis payload: %v", err) } return cacheKey }