Files
gh-otoshek-claude-code-toolkit/skills/react-allauth/scripts/test_auth_flows.py
2025-11-30 08:46:40 +08:00

354 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Professional test suite for Django-Allauth authentication flows.
Tests: Signup, Email Verification, Password Login, Logout, Code Login, Password Reset
"""
import os
import re
import time
import unittest
from pathlib import Path
from playwright.sync_api import sync_playwright
# Find project root by looking for Django project markers
# Works regardless of whether skill is installed at project or user level
def find_project_root():
"""Find Django project root by looking for manage.py or venv."""
cwd = Path.cwd()
# Search upwards for Django project markers
current = cwd
for _ in range(10): # Prevent infinite loop
# Look for Django project markers
if (current / 'manage.py').exists() and (current / 'venv').exists():
return current
if current.parent == current: # Reached filesystem root
break
current = current.parent
# Fallback: use current working directory
return cwd
PROJECT_ROOT = find_project_root()
EMAIL_DIR = PROJECT_ROOT / 'sent_emails'
class AuthenticationFlowTests(unittest.TestCase):
"""Test suite for complete authentication flows."""
@classmethod
def setUpClass(cls):
"""Set up test fixtures before running any tests."""
cls.password = "SecureTestPassword123!"
cls.new_password = "NewSecurePassword456!"
cls.playwright = None
cls.browser = None
cls.context = None
cls.page = None
def setUp(self):
"""Set up before each test."""
# Each test gets its own email to avoid conflicts
self.email = f"testuser_{int(time.time() * 1000)}@example.com"
time.sleep(0.1) # Ensure unique timestamps
self.clear_email_folder()
@staticmethod
def clear_email_folder():
"""Clear all emails from the email folder."""
if EMAIL_DIR.exists():
for f in EMAIL_DIR.iterdir():
f.unlink()
@staticmethod
def get_latest_email_content(wait_time=3):
"""Read the most recently created email file."""
# Wait for email file to be written
for _ in range(wait_time * 10): # Check every 100ms
if EMAIL_DIR.exists():
files = list(EMAIL_DIR.iterdir())
if files:
latest_file = max(files, key=lambda f: f.stat().st_ctime)
return latest_file.read_text()
time.sleep(0.1)
return None
@staticmethod
def extract_verification_code(email_content):
"""Extract the verification code from email."""
# Match 6-character alphanumeric code
pattern = r'^\s*([A-Z0-9]{6})\s*$'
match = re.search(pattern, email_content, re.MULTILINE)
return match.group(1) if match else None
@staticmethod
def extract_login_code(email_content):
"""Extract the login code from email."""
pattern = r'^\s*([A-Z0-9]{6})\s*$'
match = re.search(pattern, email_content, re.MULTILINE)
if match:
return match.group(1)
pattern = r'code:\s*([A-Z0-9]{6})'
match = re.search(pattern, email_content, re.IGNORECASE)
return match.group(1) if match else None
@staticmethod
def extract_password_reset_url(email_content):
"""Extract the password reset URL from email."""
pattern = r'https://localhost:5173/account/password/reset/key/([^\s\n]+)'
match = re.search(pattern, email_content)
return match.group(0) if match else None
def start_browser(self):
"""Start browser session."""
if not self.playwright:
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.launch(headless=False)
self.context = self.browser.new_context(ignore_https_errors=True)
self.page = self.context.new_page()
def stop_browser(self):
"""Stop browser session."""
if self.page:
self.page.close()
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
self.page = None
self.context = None
self.browser = None
self.playwright = None
def test_01_signup(self):
"""Test user signup flow."""
self.start_browser()
try:
self.page.goto('https://localhost:5173/account/signup', wait_until='networkidle')
self.page.fill('[data-testid="signup-email"]', self.email)
self.page.fill('[data-testid="signup-password1"]', self.password)
self.page.fill('[data-testid="signup-password2"]', self.password)
self.page.click('[data-testid="signup-submit"]')
self.page.wait_for_url('**/account/verify-email', timeout=5000)
self.assertIn('verify-email', self.page.url, "Should redirect to verify-email page")
finally:
self.stop_browser()
def test_02_email_verification(self):
"""Test email verification flow."""
self.start_browser()
try:
# First signup
self.page.goto('https://localhost:5173/account/signup', wait_until='networkidle')
self.page.fill('[data-testid="signup-email"]', self.email)
self.page.fill('[data-testid="signup-password1"]', self.password)
self.page.fill('[data-testid="signup-password2"]', self.password)
self.page.click('[data-testid="signup-submit"]')
self.page.wait_for_url('**/account/verify-email', timeout=5000)
# Get verification email
email_content = self.get_latest_email_content()
self.assertIsNotNone(email_content, "Verification email should exist")
verification_code = self.extract_verification_code(email_content)
self.assertIsNotNone(verification_code, "Should extract verification code")
self.assertEqual(len(verification_code), 6, "Verification code should be 6 characters")
# Verify email with code
self.page.fill('[data-testid="verify-email-code-input"]', verification_code)
self.page.click('[data-testid="verify-email-code-submit"]')
self.page.wait_for_url('**/account/email', timeout=5000)
self.assertIn('email', self.page.url, "Should redirect to email management after verification")
finally:
self.stop_browser()
def test_03_password_login(self):
"""Test password-based login flow."""
self.start_browser()
try:
# Setup: signup and verify (user will be logged in after verification)
self._signup_and_verify()
# Logout first
self.page.goto('https://localhost:5173/account/logout', wait_until='networkidle')
time.sleep(1)
self.page.click('[data-testid="logout-submit"]')
time.sleep(2)
# Now login with password
self.page.goto('https://localhost:5173/account/login', wait_until='networkidle')
self.page.fill('[data-testid="login-email"]', self.email)
self.page.fill('[data-testid="login-password"]', self.password)
self.page.click('[data-testid="login-submit"]')
self.page.wait_for_url('https://localhost:5173/', timeout=5000)
# Verify logged in
logout_link = self.page.locator('a:has-text("Logout")')
self.assertGreater(logout_link.count(), 0, "Should see Logout link when logged in")
finally:
self.stop_browser()
def test_04_logout(self):
"""Test logout flow."""
self.start_browser()
try:
# Setup: login
self._login_user()
# Logout
self.page.goto('https://localhost:5173/account/logout', wait_until='networkidle')
time.sleep(1)
self.page.click('[data-testid="logout-submit"]')
self.page.wait_for_load_state('networkidle')
time.sleep(2)
# Verify logged out
self.page.goto('https://localhost:5173/', wait_until='networkidle')
time.sleep(1)
login_link = self.page.locator('a:has-text("Login")')
self.assertGreater(login_link.count(), 0, "Should see Login link when logged out")
finally:
self.stop_browser()
def test_05_code_login(self):
"""Test passwordless login with code flow."""
self.start_browser()
try:
# Setup: signup and verify (user will be logged in after verification)
self._signup_and_verify()
# Logout
self.page.goto('https://localhost:5173/account/logout', wait_until='networkidle')
time.sleep(1)
self.page.click('[data-testid="logout-submit"]')
time.sleep(2)
self.clear_email_folder()
# Request login code
self.page.goto('https://localhost:5173/account/login', wait_until='networkidle')
time.sleep(2)
self.page.click('a:has-text("Send me a sign-in code")')
self.page.wait_for_load_state('networkidle')
time.sleep(1)
self.page.fill('[data-testid="request-code-email"]', self.email)
self.page.click('[data-testid="request-code-submit"]')
self.page.wait_for_load_state('networkidle')
time.sleep(2)
# Get code from email
code_email = self.get_latest_email_content()
self.assertIsNotNone(code_email, "Login code email should exist")
login_code = self.extract_login_code(code_email)
self.assertIsNotNone(login_code, "Should extract login code")
self.assertEqual(len(login_code), 6, "Login code should be 6 characters")
# Enter code and login
self.page.fill('[data-testid="confirm-code-input"]', login_code)
self.page.click('[data-testid="confirm-code-submit"]')
self.page.wait_for_url('https://localhost:5173/', timeout=5000)
# Verify logged in
logout_link = self.page.locator('a:has-text("Logout")')
self.assertGreater(logout_link.count(), 0, "Should be logged in after code login")
finally:
self.stop_browser()
def test_06_password_reset(self):
"""Test password reset flow."""
self.start_browser()
try:
# Setup: signup and verify
self._signup_and_verify()
# Logout (should be at login page)
self.page.goto('https://localhost:5173/account/logout', wait_until='networkidle')
time.sleep(1)
if self.page.locator('[data-testid="logout-submit"]').count() > 0:
self.page.click('[data-testid="logout-submit"]')
time.sleep(2)
self.clear_email_folder()
# Request password reset
self.page.goto('https://localhost:5173/account/login', wait_until='networkidle')
time.sleep(1)
self.page.click('a:has-text("Forgot your password?")')
self.page.wait_for_load_state('networkidle')
time.sleep(1)
self.page.fill('[data-testid="password-reset-email"]', self.email)
self.page.click('[data-testid="password-reset-submit"]')
self.page.wait_for_load_state('networkidle')
time.sleep(2)
# Get reset email
reset_email = self.get_latest_email_content()
self.assertIsNotNone(reset_email, "Password reset email should exist")
reset_url = self.extract_password_reset_url(reset_email)
self.assertIsNotNone(reset_url, "Should extract password reset URL")
# Reset password
self.page.goto(reset_url)
self.page.wait_for_load_state('networkidle')
time.sleep(1)
self.page.fill('[data-testid="reset-password1"]', self.new_password)
self.page.fill('[data-testid="reset-password2"]', self.new_password)
self.page.click('[data-testid="reset-password-confirm"]')
self.page.wait_for_load_state('networkidle')
time.sleep(2)
# Password reset complete - now login with new password to verify it worked
# Navigate to login page
self.page.goto('https://localhost:5173/account/login', wait_until='networkidle')
time.sleep(1)
# Login with new password
self.page.fill('[data-testid="login-email"]', self.email)
self.page.fill('[data-testid="login-password"]', self.new_password)
self.page.click('[data-testid="login-submit"]')
self.page.wait_for_url('https://localhost:5173/', timeout=5000)
# Verify logged in with new password
logout_link = self.page.locator('a:has-text("Logout")')
self.assertGreater(logout_link.count(), 0, "Should be logged in with new password")
finally:
self.stop_browser()
# Helper methods
def _signup_and_verify(self):
"""Helper: signup and verify email."""
self.clear_email_folder()
self.page.goto('https://localhost:5173/account/signup', wait_until='networkidle')
self.page.fill('[data-testid="signup-email"]', self.email)
self.page.fill('[data-testid="signup-password1"]', self.password)
self.page.fill('[data-testid="signup-password2"]', self.password)
self.page.click('[data-testid="signup-submit"]')
self.page.wait_for_url('**/account/verify-email', timeout=5000)
email_content = self.get_latest_email_content()
verification_code = self.extract_verification_code(email_content)
self.page.fill('[data-testid="verify-email-code-input"]', verification_code)
self.page.click('[data-testid="verify-email-code-submit"]')
self.page.wait_for_url('**/account/email', timeout=5000)
# User is now logged in and on the email management page
def _login_user(self):
"""Helper: complete signup, verify, and login."""
self._signup_and_verify()
# User is already logged in after email verification
if __name__ == '__main__':
# Run tests with verbose output
unittest.main(verbosity=2)