package auth import ( "strings" "testing" ) func TestTOTPManager_GenerateAndValidate(t *testing.T) { m := NewTOTPManager() // 生成密钥 setup, err := m.GenerateSecret("testuser@example.com") if err != nil { t.Fatalf("GenerateSecret 失败: %v", err) } if setup.Secret == "" { t.Fatal("生成的 Secret 不应为空") } if setup.QRCodeBase64 == "" { t.Fatal("QRCode Base64 不应为空") } if len(setup.RecoveryCodes) != RecoveryCodeCount { t.Fatalf("恢复码数量期望 %d,实际 %d", RecoveryCodeCount, len(setup.RecoveryCodes)) } t.Logf("生成 Secret: %s", setup.Secret) t.Logf("恢复码示例: %s", setup.RecoveryCodes[0]) // 用生成的密钥生成当前 TOTP 码,再验证 code, err := m.GenerateCurrentCode(setup.Secret) if err != nil { t.Fatalf("GenerateCurrentCode 失败: %v", err) } if !m.ValidateCode(setup.Secret, code) { t.Fatalf("有效 TOTP 码应该通过验证,code=%s", code) } t.Logf("TOTP 验证通过,code=%s", code) } func TestTOTPManager_InvalidCode(t *testing.T) { m := NewTOTPManager() setup, err := m.GenerateSecret("user") if err != nil { t.Fatalf("GenerateSecret 失败: %v", err) } // 错误的验证码 if m.ValidateCode(setup.Secret, "000000") { // 偶尔可能恰好正确,跳过而不是 fatal t.Skip("000000 碰巧是有效码,跳过测试") } t.Log("无效验证码正确拒绝") } func TestTOTPManager_RecoveryCodeFormat(t *testing.T) { m := NewTOTPManager() setup, err := m.GenerateSecret("user2") if err != nil { t.Fatalf("GenerateSecret 失败: %v", err) } for i, code := range setup.RecoveryCodes { parts := strings.Split(code, "-") if len(parts) != 2 { t.Errorf("恢复码 [%d] 格式错误(期望 XXXXX-XXXXX): %s", i, code) } if len(parts[0]) != 5 || len(parts[1]) != 5 { t.Errorf("恢复码 [%d] 各部分长度应为 5: %s", i, code) } } } func TestValidateRecoveryCode(t *testing.T) { codes := []string{"ABCDE-FGHIJ", "KLMNO-PQRST", "UVWXY-ZABCD"} // 正确匹配 idx, ok := ValidateRecoveryCode("ABCDE-FGHIJ", codes) if !ok || idx != 0 { t.Fatalf("有效恢复码应该匹配,idx=%d ok=%v", idx, ok) } // 大小写不敏感 idx2, ok2 := ValidateRecoveryCode("klmno-pqrst", codes) if !ok2 || idx2 != 1 { t.Fatalf("大小写不敏感匹配失败,idx=%d ok=%v", idx2, ok2) } // 去除空格 idx3, ok3 := ValidateRecoveryCode(" UVWXY-ZABCD ", codes) if !ok3 || idx3 != 2 { t.Fatalf("去除空格匹配失败,idx=%d ok=%v", idx3, ok3) } // 不匹配 _, ok4 := ValidateRecoveryCode("XXXXX-YYYYY", codes) if ok4 { t.Fatal("无效恢复码不应该匹配") } t.Log("恢复码验证全部通过") } func TestHashRecoveryCode(t *testing.T) { code := "ABCDE-FGHIJ" hashed, err := HashRecoveryCode(code) if err != nil { t.Fatalf("HashRecoveryCode failed: %v", err) } if hashed == "" { t.Fatal("HashRecoveryCode should return non-empty hash") } // Same code should produce same hash hashed2, err := HashRecoveryCode(code) if err != nil { t.Fatalf("HashRecoveryCode second call failed: %v", err) } if hashed != hashed2 { t.Error("Same code should produce same hash") } // Different codes should produce different hashes hashed3, err := HashRecoveryCode("DIFFERENT-CODE") if err != nil { t.Fatalf("HashRecoveryCode for different code failed: %v", err) } if hashed == hashed3 { t.Error("Different codes should produce different hashes") } t.Logf("Hashed code: %s", hashed) } func TestVerifyRecoveryCode(t *testing.T) { // Generate hashed codes codes := []string{"ABCDE-FGHIJ", "KLMNO-PQRST", "UVWXY-ZABCD"} hashedCodes := make([]string, len(codes)) for i, code := range codes { hashed, err := HashRecoveryCode(code) if err != nil { t.Fatalf("HashRecoveryCode failed: %v", err) } hashedCodes[i] = hashed } // Test valid code (exact match) idx, ok := VerifyRecoveryCode("ABCDE-FGHIJ", hashedCodes) if !ok || idx != 0 { t.Fatalf("Valid recovery code should match, idx=%d ok=%v", idx, ok) } // Test second code idx2, ok2 := VerifyRecoveryCode("KLMNO-PQRST", hashedCodes) if !ok2 || idx2 != 1 { t.Fatalf("Second code match failed, idx=%d ok=%v", idx2, ok2) } // Test third code idx3, ok3 := VerifyRecoveryCode("UVWXY-ZABCD", hashedCodes) if !ok3 || idx3 != 2 { t.Fatalf("Third code match failed, idx=%d ok=%v", idx3, ok3) } // Test invalid code _, ok4 := VerifyRecoveryCode("XXXXX-YYYYY", hashedCodes) if ok4 { t.Fatal("Invalid recovery code should not match") } // Test empty hashed codes list _, ok5 := VerifyRecoveryCode("ABCDE-FGHIJ", []string{}) if ok5 { t.Fatal("Should not match against empty list") } t.Log("VerifyRecoveryCode tests passed") } func TestVerifyRecoveryCode_TimingSafety(t *testing.T) { // Test that the function always iterates through all codes // regardless of where the match is found (timing attack prevention) codes := []string{"CODE1-AAAAA", "CODE2-BBBBB", "CODE3-CCCCC"} hashedCodes := make([]string, len(codes)) for i, code := range codes { hashed, _ := HashRecoveryCode(code) hashedCodes[i] = hashed } // Test matching first code idx1, ok1 := VerifyRecoveryCode("CODE1-AAAAA", hashedCodes) if !ok1 || idx1 != 0 { t.Errorf("First code match failed, idx=%d ok=%v", idx1, ok1) } // Test matching last code idx3, ok3 := VerifyRecoveryCode("CODE3-CCCCC", hashedCodes) if !ok3 || idx3 != 2 { t.Errorf("Last code match failed, idx=%d ok=%v", idx3, ok3) } t.Log("Timing safety test passed") }