diff --git a/api/register_otp_test.go b/api/register_otp_test.go new file mode 100644 index 00000000..dcd13288 --- /dev/null +++ b/api/register_otp_test.go @@ -0,0 +1,252 @@ +package api + +import ( + "testing" +) + +// MockUser 模擬用戶結構 +type MockUser struct { + ID int + Email string + OTPSecret string + OTPVerified bool +} + +// TestOTPRefetchLogic 測試 OTP 重新獲取邏輯 +func TestOTPRefetchLogic(t *testing.T) { + tests := []struct { + name string + existingUser *MockUser + userExists bool + expectedAction string // "allow_refetch", "reject_duplicate", "create_new" + expectedMessage string + }{ + { + name: "新用戶註冊_郵箱不存在", + existingUser: nil, + userExists: false, + expectedAction: "create_new", + expectedMessage: "創建新用戶", + }, + { + name: "未完成OTP驗證_允許重新獲取", + existingUser: &MockUser{ + ID: 1, + Email: "test@example.com", + OTPSecret: "SECRET123", + OTPVerified: false, + }, + userExists: true, + expectedAction: "allow_refetch", + expectedMessage: "检测到未完成的注册,请继续完成OTP设置", + }, + { + name: "已完成OTP驗證_拒絕重複註冊", + existingUser: &MockUser{ + ID: 2, + Email: "verified@example.com", + OTPSecret: "SECRET456", + OTPVerified: true, + }, + userExists: true, + expectedAction: "reject_duplicate", + expectedMessage: "邮箱已被注册", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 模擬邏輯處理流程 + var actualAction string + var actualMessage string + + if !tt.userExists { + // 用戶不存在,創建新用戶 + actualAction = "create_new" + actualMessage = "創建新用戶" + } else { + // 用戶已存在,檢查 OTP 驗證狀態 + if !tt.existingUser.OTPVerified { + // 未完成 OTP 驗證,允許重新獲取 + actualAction = "allow_refetch" + actualMessage = "检测到未完成的注册,请继续完成OTP设置" + } else { + // 已完成驗證,拒絕重複註冊 + actualAction = "reject_duplicate" + actualMessage = "邮箱已被注册" + } + } + + // 驗證結果 + if actualAction != tt.expectedAction { + t.Errorf("Action 不符: got %s, want %s", actualAction, tt.expectedAction) + } + if actualMessage != tt.expectedMessage { + t.Errorf("Message 不符: got %s, want %s", actualMessage, tt.expectedMessage) + } + }) + } +} + +// TestOTPVerificationStates 測試 OTP 驗證狀態判斷 +func TestOTPVerificationStates(t *testing.T) { + tests := []struct { + name string + otpVerified bool + shouldAllowRefetch bool + }{ + { + name: "OTP已驗證_不允許重新獲取", + otpVerified: true, + shouldAllowRefetch: false, + }, + { + name: "OTP未驗證_允許重新獲取", + otpVerified: false, + shouldAllowRefetch: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 模擬驗證邏輯 + allowRefetch := !tt.otpVerified + + if allowRefetch != tt.shouldAllowRefetch { + t.Errorf("Refetch logic error: OTPVerified=%v, allowRefetch=%v, expected=%v", + tt.otpVerified, allowRefetch, tt.shouldAllowRefetch) + } + }) + } +} + +// TestRegistrationFlow 測試完整註冊流程的邏輯分支 +func TestRegistrationFlow(t *testing.T) { + tests := []struct { + name string + scenario string + userExists bool + otpVerified bool + expectHTTPCode int // 模擬的 HTTP 狀態碼 + expectResponse string + }{ + { + name: "場景1_新用戶首次註冊", + scenario: "新用戶首次訪問註冊接口", + userExists: false, + otpVerified: false, + expectHTTPCode: 200, + expectResponse: "創建用戶並返回 OTP 設置信息", + }, + { + name: "場景2_用戶中斷註冊後重新訪問", + scenario: "用戶之前註冊但未完成 OTP 設置,現在重新訪問", + userExists: true, + otpVerified: false, + expectHTTPCode: 200, + expectResponse: "返回現有用戶的 OTP 信息,允許繼續完成", + }, + { + name: "場景3_已註冊用戶嘗試重複註冊", + scenario: "用戶已完成註冊,嘗試用同一郵箱再次註冊", + userExists: true, + otpVerified: true, + expectHTTPCode: 409, // Conflict + expectResponse: "邮箱已被注册", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 模擬註冊流程邏輯 + var actualHTTPCode int + var actualResponse string + + if !tt.userExists { + // 新用戶,創建並返回 OTP 信息 + actualHTTPCode = 200 + actualResponse = "創建用戶並返回 OTP 設置信息" + } else { + // 用戶已存在 + if !tt.otpVerified { + // 未完成 OTP 驗證,允許重新獲取 + actualHTTPCode = 200 + actualResponse = "返回現有用戶的 OTP 信息,允許繼續完成" + } else { + // 已完成驗證,拒絕重複註冊 + actualHTTPCode = 409 + actualResponse = "邮箱已被注册" + } + } + + // 驗證 + if actualHTTPCode != tt.expectHTTPCode { + t.Errorf("HTTP code 不符: got %d, want %d (scenario: %s)", + actualHTTPCode, tt.expectHTTPCode, tt.scenario) + } + if actualResponse != tt.expectResponse { + t.Errorf("Response 不符: got %s, want %s (scenario: %s)", + actualResponse, tt.expectResponse, tt.scenario) + } + + t.Logf("✓ %s: HTTP %d, %s", tt.scenario, actualHTTPCode, actualResponse) + }) + } +} + +// TestEdgeCases 測試邊界情況 +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + user *MockUser + expectAllow bool + description string + }{ + { + name: "用戶ID為0_視為新用戶", + user: &MockUser{ + ID: 0, + Email: "new@example.com", + OTPVerified: false, + }, + expectAllow: true, + description: "ID為0通常表示用戶還未創建", + }, + { + name: "OTPSecret為空_仍可重新獲取", + user: &MockUser{ + ID: 1, + Email: "test@example.com", + OTPSecret: "", + OTPVerified: false, + }, + expectAllow: true, + description: "即使 OTPSecret 為空,只要未驗證就允許重新獲取", + }, + { + name: "OTPSecret存在但已驗證_不允許", + user: &MockUser{ + ID: 2, + Email: "verified@example.com", + OTPSecret: "SECRET789", + OTPVerified: true, + }, + expectAllow: false, + description: "OTP 已驗證的用戶不能重新獲取", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 核心邏輯:只要 OTPVerified 為 false,就允許重新獲取 + allowRefetch := !tt.user.OTPVerified + + if allowRefetch != tt.expectAllow { + t.Errorf("Edge case failed: %s\nUser: ID=%d, OTPVerified=%v\nExpected allow=%v, got=%v", + tt.description, tt.user.ID, tt.user.OTPVerified, tt.expectAllow, allowRefetch) + } + + t.Logf("✓ %s", tt.description) + }) + } +} diff --git a/api/server.go b/api/server.go index 7a969563..bc6f0986 100644 --- a/api/server.go +++ b/api/server.go @@ -967,7 +967,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) { actualBalance = totalBalance } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"}) - return + return } oldBalance := traderConfig.InitialBalance @@ -1724,8 +1724,21 @@ func (s *Server) handleRegister(c *gin.Context) { } // 检查邮箱是否已存在 - _, err := s.database.GetUserByEmail(req.Email) + existingUser, err := s.database.GetUserByEmail(req.Email) if err == nil { + // 如果用户未完成OTP验证,允许重新获取OTP(支持中断后恢复注册) + if !existingUser.OTPVerified { + qrCodeURL := auth.GetOTPQRCodeURL(existingUser.OTPSecret, req.Email) + c.JSON(http.StatusOK, gin.H{ + "user_id": existingUser.ID, + "email": req.Email, + "otp_secret": existingUser.OTPSecret, + "qr_code_url": qrCodeURL, + "message": "检测到未完成的注册,请继续完成OTP设置", + }) + return + } + // 用户已完成验证,拒绝重复注册 c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) return }