#!/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)