Initial commit
This commit is contained in:
353
skills/react-allauth/scripts/test_auth_flows.py
Executable file
353
skills/react-allauth/scripts/test_auth_flows.py
Executable file
@@ -0,0 +1,353 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user