package handler_test import ( "bytes" "encoding/json" "net/http" "testing" ) func TestPasswordResetHandler_ForgotPassword(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "resetuser", "resetuser@test.com", "UserPass123!") resp, body := doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "resetuser@test.com", }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var result map[string]interface{} if err := json.Unmarshal([]byte(body), &result); err != nil { t.Fatalf("failed to parse response: %v", err) } if result["code"] != float64(0) { t.Errorf("expected code 0, got %v", result["code"]) } } func TestPasswordResetHandler_ForgotPassword_MissingEmail(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{}) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestPasswordResetHandler_ForgotPassword_NonExistentEmail(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() // For non-existent email, the service returns success to prevent user enumeration resp, body := doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "nonexistent@test.com", }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected status %d for non-existent email, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } } func TestPasswordResetHandler_ValidateResetToken(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "validatetokenuser", "validatetoken@test.com", "UserPass123!") // First request a password reset to generate a token _, _ = doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "validatetoken@test.com", }) // We can't easily get the token from email, so test with an invalid token resp, body := doPost(server.URL+"/api/v1/auth/password/validate", "", map[string]interface{}{ "token": "invalid-token-12345", }) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status %d, got %d, body: %s", http.StatusOK, resp.StatusCode, body) } var result map[string]interface{} if err := json.Unmarshal([]byte(body), &result); err != nil { t.Fatalf("failed to parse response: %v", err) } if result["code"] != float64(0) { t.Errorf("expected code 0, got %v", result["code"]) } data, ok := result["data"].(map[string]interface{}) if !ok { t.Fatalf("expected data in response, got %s", body) } if data["valid"] != false { t.Errorf("expected valid=false for invalid token, got %v", data["valid"]) } } func TestPasswordResetHandler_ValidateResetToken_MissingToken(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/auth/password/validate", "", map[string]interface{}{}) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPassword(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "resetpwuser", "resetpw@test.com", "UserPass123!") // Request reset to generate token _, _ = doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "resetpw@test.com", }) // Since we can't get the token, test with invalid token resp, body := doPost(server.URL+"/api/v1/auth/reset-password", "", map[string]interface{}{ "token": "invalid-token", "new_password": "NewPass123!", }) defer resp.Body.Close() // Should fail because token is invalid (service returns 404 for "不存在") if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusNotFound { t.Errorf("expected status 401, 400 or 404 for invalid token, got %d, body: %s", resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPassword_MissingToken(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/auth/reset-password", "", map[string]interface{}{ "new_password": "NewPass123!", }) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPassword_MissingPassword(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/auth/reset-password", "", map[string]interface{}{ "token": "some-token", }) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPassword_WeakPassword(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "resetpwweak", "resetpwweak@test.com", "UserPass123!") // We need a valid token to test weak password rejection // Let's manually create one through the cache by using forgot-password _, _ = doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "resetpwweak@test.com", }) // Use invalid token - the validation happens before password strength check resp, body := doPost(server.URL+"/api/v1/auth/reset-password", "", map[string]interface{}{ "token": "invalid-token", "new_password": "123", }) defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusNotFound { t.Errorf("expected status 401, 400 or 404, got %d, body: %s", resp.StatusCode, body) } } func TestPasswordResetHandler_ForgotPasswordByPhone_ServiceUnavailable(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() // The password reset handler in the test setup does not have SMS service configured resp, body := doPost(server.URL+"/api/v1/auth/forgot-password/phone", "", map[string]interface{}{ "phone": "13800138000", }) defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Errorf("expected status %d, got %d, body: %s", http.StatusServiceUnavailable, resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPasswordByPhone_MissingFields(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() resp, body := doPost(server.URL+"/api/v1/auth/reset-password/phone", "", map[string]interface{}{}) defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %d, body: %s", http.StatusBadRequest, resp.StatusCode, body) } } func TestPasswordResetHandler_ResetPasswordByPhone_InvalidCode(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "resetphoneuser", "resetphone@test.com", "UserPass123!") resp, body := doPost(server.URL+"/api/v1/auth/reset-password/phone", "", map[string]interface{}{ "phone": "13800138000", "code": "000000", "new_password": "NewPass123!", }) defer resp.Body.Close() // Should fail because no code was sent if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status 401 or 400 for invalid code, got %d, body: %s", resp.StatusCode, body) } } func TestPasswordResetHandler_ForgotPassword_InvalidJSON(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() req, _ := http.NewRequest("POST", server.URL+"/api/v1/auth/forgot-password", bytes.NewReader([]byte("not json"))) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d for invalid JSON, got %d", http.StatusBadRequest, resp.StatusCode) } } func TestPasswordResetHandler_FullFlow(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "fullflowuser", "fullflow@test.com", "UserPass123!") // Step 1: Request password reset forgotResp, forgotBody := doPost(server.URL+"/api/v1/auth/forgot-password", "", map[string]interface{}{ "email": "fullflow@test.com", }) defer forgotResp.Body.Close() if forgotResp.StatusCode != http.StatusOK { t.Fatalf("forgot-password failed: status=%d body=%s", forgotResp.StatusCode, forgotBody) } // Step 2: Validate token (we don't know the real token, so it will be invalid) validateResp, validateBody := doPost(server.URL+"/api/v1/auth/password/validate", "", map[string]interface{}{ "token": "unknown-token", }) defer validateResp.Body.Close() if validateResp.StatusCode != http.StatusOK { t.Fatalf("validate token failed: status=%d body=%s", validateResp.StatusCode, validateBody) } var validateResult map[string]interface{} if err := json.Unmarshal([]byte(validateBody), &validateResult); err != nil { t.Fatalf("failed to parse validate response: %v", err) } validateData, ok := validateResult["data"].(map[string]interface{}) if !ok { t.Fatalf("expected validate data, got %s", validateBody) } if validateData["valid"] != false { t.Errorf("expected valid=false for unknown token, got %v", validateData["valid"]) } // Step 3: Try reset with invalid token resetResp, resetBody := doPost(server.URL+"/api/v1/auth/reset-password", "", map[string]interface{}{ "token": "unknown-token", "new_password": "NewPass123!", }) defer resetResp.Body.Close() // Should fail because token is invalid (service returns 404 for "不存在") if resetResp.StatusCode != http.StatusUnauthorized && resetResp.StatusCode != http.StatusNotFound { t.Errorf("expected status 401 or 404 for invalid token reset, got %d, body: %s", resetResp.StatusCode, resetBody) } // Step 4: Verify old password still works loginResp, loginBody := doPost(server.URL+"/api/v1/auth/login", "", map[string]interface{}{ "account": "fullflowuser", "password": "UserPass123!", }) defer loginResp.Body.Close() if loginResp.StatusCode != http.StatusOK { t.Fatalf("old password should still work: status=%d body=%s", loginResp.StatusCode, loginBody) } }