Files
user-system/internal/api/handler/password_reset_handler_test.go

309 lines
10 KiB
Go

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)
}
}