354 lines
14 KiB
Python
Executable File
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)
|