swiftops-backend / docs /agent /implementation-notes /PASSWORD_RESET_TEST_GUIDE.md
kamau1's picture
feat(project): add complete project setup workflow with service methods and API endpoints for regions, roles, subcontractors, and finalization including validation and authorization
4835b24

Password Reset Testing Guide

Complete guide for testing the password reset functionality with audit logging.

Overview

The password reset system provides secure token-based password recovery with:

  • Secure token generation (32+ characters)
  • Time-limited tokens (1 hour expiry)
  • Email delivery via Resend
  • Comprehensive audit logging
  • Protection against email enumeration attacks
  • Single-use tokens

Prerequisites

  1. Environment Variables (see .env.example):

    RESEND_API_KEY=re_xxxxx
    RESEND_FROM_EMAIL=swiftops@atomio.tech
    APP_DOMAIN=swiftops.atomio.tech
    APP_PROTOCOL=https
    PASSWORD_RESET_TOKEN_EXPIRY_HOURS=1
    
  2. Database Migrations:

    # Ensure user_invitations table exists
    # (Created in migration 11_user_invitations.sql)
    
  3. Test User:

    # Register a test user first
    curl -X POST http://localhost:8000/api/v1/auth/register \
      -H "Content-Type: application/json" \
      -d '{
        "email": "test@example.com",
        "password": "TestPass123!",
        "first_name": "Test",
        "last_name": "User"
      }'
    

Test Scenarios

1. Request Password Reset

Endpoint: POST /api/v1/auth/forgot-password

Request:

curl -X POST http://localhost:8000/api/v1/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com"
  }'

Expected Response (200 OK):

{
  "message": "If an account exists with this email, you will receive a password reset link."
}

Verification:

  1. Check database for reset token:

    SELECT token, expires_at, status 
    FROM user_invitations 
    WHERE email = 'test@example.com' 
      AND invitation_metadata->>'type' = 'password_reset'
    ORDER BY created_at DESC 
    LIMIT 1;
    
  2. Check audit log:

    SELECT action, description, created_at 
    FROM audit_logs 
    WHERE action = 'password_reset_request'
    ORDER BY created_at DESC 
    LIMIT 1;
    
  3. Check email (if Resend configured):

    • Subject: "Reset Your SwiftOps Password"
    • Contains reset link with token
    • Mentions 1 hour expiry

2. Reset Password with Valid Token

Endpoint: POST /api/v1/auth/reset-password

Request:

# Get token from database first
TOKEN="<token_from_database>"

curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "'$TOKEN'",
    "new_password": "NewTestPass456!"
  }'

Expected Response (200 OK):

{
  "message": "Password reset successful. You can now login with your new password."
}

Verification:

  1. Check token status updated:

    SELECT status, accepted_at 
    FROM user_invitations 
    WHERE token = '<token>';
    
    • Status should be 'accepted'
    • accepted_at should be set
  2. Check audit log:

    SELECT action, description 
    FROM audit_logs 
    WHERE action = 'password_reset'
    ORDER BY created_at DESC 
    LIMIT 1;
    
  3. Login with new password:

    curl -X POST http://localhost:8000/api/v1/auth/login \
      -H "Content-Type: application/json" \
      -d '{
        "email": "test@example.com",
        "password": "NewTestPass456!"
      }'
    
  4. Verify old password fails:

    curl -X POST http://localhost:8000/api/v1/auth/login \
      -H "Content-Type: application/json" \
      -d '{
        "email": "test@example.com",
        "password": "TestPass123!"
      }'
    
    • Should return 401 Unauthorized

3. Invalid Token

Request:

curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "invalid-token-12345678901234567890",
    "new_password": "NewTestPass456!"
  }'

Expected Response (400 Bad Request):

{
  "detail": "Invalid or expired reset token"
}

Verification:

  • Check audit log for failed attempt:
    SELECT action, description, additional_metadata 
    FROM audit_logs 
    WHERE action = 'password_reset_failed'
    ORDER BY created_at DESC 
    LIMIT 1;
    

4. Expired Token

Setup:

-- Manually expire a token for testing
UPDATE user_invitations 
SET expires_at = NOW() - INTERVAL '1 hour'
WHERE email = 'test@example.com' 
  AND status = 'pending'
  AND invitation_metadata->>'type' = 'password_reset';

Request:

curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "<expired_token>",
    "new_password": "NewTestPass456!"
  }'

Expected Response (400 Bad Request):

{
  "detail": "Reset token has expired. Please request a new one."
}

Verification:

  • Token status updated to 'expired':
    SELECT status FROM user_invitations WHERE token = '<expired_token>';
    

5. Token Reuse Prevention

Request:

# Use the same token twice
TOKEN="<already_used_token>"

# First use (should succeed)
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "'$TOKEN'",
    "new_password": "NewTestPass456!"
  }'

# Second use (should fail)
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "'$TOKEN'",
    "new_password": "AnotherPass789!"
  }'

Expected:

  • First request: 200 OK
  • Second request: 400 Bad Request

6. Non-Existent Email (No Enumeration)

Request:

curl -X POST http://localhost:8000/api/v1/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "nonexistent@example.com"
  }'

Expected Response (200 OK):

{
  "message": "If an account exists with this email, you will receive a password reset link."
}

Verification:

  • Same response as valid email (prevents enumeration)
  • No token created in database
  • Audit log still created

7. Password Validation

Weak Passwords to Test:

# Too short
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token": "test", "new_password": "short"}'

# No uppercase
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token": "test", "new_password": "nouppercase123"}'

# No digits
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token": "test", "new_password": "NoDigitsHere"}'

Expected: All should return 422 Validation Error

Automated Tests

Python Tests

Run pytest tests:

# All password reset tests
pytest tests/integration/test_password_reset.py -v

# Specific test
pytest tests/integration/test_password_reset.py::TestPasswordReset::test_reset_password_with_valid_token -v

# All auth audit log tests
pytest tests/integration/test_auth_audit_logs.py -v

JavaScript Tests

Run Node.js tests:

cd tests/integration
npm install axios  # If not already installed
node test_password_reset.js

Audit Log Verification

Check all audit logs for password reset flow:

-- All password reset related logs
SELECT 
  action,
  description,
  user_email,
  ip_address,
  created_at
FROM audit_logs
WHERE action IN (
  'password_reset_request',
  'password_reset',
  'password_reset_failed'
)
ORDER BY created_at DESC
LIMIT 20;

-- Detailed view with metadata
SELECT 
  action,
  description,
  additional_metadata,
  created_at
FROM audit_logs
WHERE entity_type = 'auth'
  AND action LIKE '%password%'
ORDER BY created_at DESC;

Email Template Testing

If Resend is configured, verify email appearance:

  1. Subject: "Reset Your SwiftOps Password"
  2. Content:
    • Personalized greeting with user's name
    • Clear call-to-action button
    • Plain text link as fallback
    • Security notice about 1-hour expiry
    • Warning if user didn't request reset
  3. Styling:
    • Responsive design
    • Professional appearance
    • SwiftOps branding

Security Checklist

  • Tokens are cryptographically secure (32+ characters)
  • Tokens expire after 1 hour
  • Tokens are single-use only
  • No email enumeration (same response for valid/invalid emails)
  • Password strength requirements enforced
  • All actions logged in audit trail
  • IP addresses captured in audit logs
  • User agents captured in audit logs
  • Email delivery failures handled gracefully
  • Database errors don't expose sensitive info

Troubleshooting

Email Not Received

  1. Check Resend API key:

    echo $RESEND_API_KEY
    
  2. Check application logs:

    grep "Password reset" logs/app.log
    grep "Email send error" logs/app.log
    
  3. Verify Resend domain configuration

Token Not Working

  1. Check token exists and is valid:

    SELECT * FROM user_invitations 
    WHERE token = '<token>';
    
  2. Check expiry time:

    SELECT 
      token,
      expires_at,
      expires_at > NOW() as is_valid,
      status
    FROM user_invitations 
    WHERE token = '<token>';
    

Audit Logs Not Created

  1. Check database connection
  2. Verify audit_logs table exists
  3. Check application logs for errors
  4. Ensure AuditService is imported correctly

Performance Considerations

  • Token generation: < 10ms
  • Database query: < 50ms
  • Email sending: < 2s (async)
  • Total request time: < 100ms (excluding email)

Next Steps

After testing password reset:

  1. Test all auth endpoints with audit logging
  2. Verify audit log retention policies
  3. Set up monitoring for failed reset attempts
  4. Configure email rate limiting
  5. Review security policies with team