import { test, expect } from '@playwright/test'; /** * Comprehensive Signup Flow Tests * * Tests the complete user registration journey: * 1. Account creation * 2. Organization creation (first-time user) * 3. Email confirmation * 4. First login * 5. Validation and error handling */ test.describe('Complete Signup Flow', () => { // Use a unique email for each test run to avoid conflicts const timestamp = Date.now(); const testEmail = `signup-test-${timestamp}@example.com`; const testPassword = 'SecureTestPassword123!'; const testOrgName = `Test Org ${timestamp}`; test.describe('New User Registration', () => { test('can complete full signup flow', async ({ page }) => { // Step 1: Navigate to registration page await page.goto('/users/register'); await expect(page).toHaveURL(/\/users\/register/); // Step 2: Fill out registration form const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(testEmail); await passwordField.fill(testPassword); // Look for password confirmation field if it exists const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } // Step 3: Submit registration await submitButton.click(); await page.waitForTimeout(2000); // Step 4: Should redirect to org creation or dashboard const currentUrl = page.url(); // Could redirect to various places depending on app flow: // - Organization creation page // - Dashboard (if org auto-created) // - Email confirmation prompt // - TOTP enrollment if (currentUrl.includes('/organizations/new') || currentUrl.includes('/org/new')) { // Organization creation flow const orgNameField = page.locator('input[name*="name"]').first(); if (await orgNameField.isVisible({ timeout: 2000 }).catch(() => false)) { await orgNameField.fill(testOrgName); const orgSubmitButton = page.locator('button[type="submit"]').first(); await orgSubmitButton.click(); await page.waitForTimeout(2000); } } // Step 5: Verify successful signup (should be on dashboard or similar) // Check we're NOT on the login or register page anymore const finalUrl = page.url(); expect(finalUrl).not.toContain('/users/log-in'); expect(finalUrl).not.toContain('/users/register'); // Should see some authenticated user content await expect(page.locator('body')).toBeVisible(); }); test('shows validation error for invalid email', async ({ page }) => { await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); // Try to register with invalid email await emailField.fill('not-an-email'); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(1000); // Should show validation error const error = page.locator('.error, [role="alert"], :has-text("invalid"), :has-text("valid email")').first(); if (await error.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(error).toBeVisible(); } // Should still be on registration page expect(page.url()).toContain('/users/register'); }); test('shows error for weak password', async ({ page }) => { await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(`weak-pwd-${Date.now()}@example.com`); await passwordField.fill('123'); // Too short/weak const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill('123'); } await submitButton.click(); await page.waitForTimeout(1000); // Should show password validation error const error = page.locator(':has-text("password"), :has-text("characters"), :has-text("least")').first(); if (await error.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(error).toBeVisible(); } expect(page.url()).toContain('/users/register'); }); test('shows error for duplicate email', async ({ page }) => { // First, try to register with the e2e test account email (known to exist) await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill('e2e-test@towerops.local'); // Known existing account await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(1000); // Should show duplicate error const error = page.locator(':has-text("already"), :has-text("taken"), :has-text("exists")').first(); if (await error.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(error).toBeVisible(); } }); test('shows error when passwords do not match', async ({ page }) => { await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordFields = page.locator('input[type="password"]'); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(`mismatch-${Date.now()}@example.com`); // Fill first password field await passwordFields.first().fill(testPassword); // Fill second password field with different value const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill('DifferentPassword123!'); await submitButton.click(); await page.waitForTimeout(1000); // Should show mismatch error const error = page.locator(':has-text("match"), :has-text("same")').first(); if (await error.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(error).toBeVisible(); } } }); }); test.describe('Organization Creation for New Users', () => { test('prompts new user to create organization', async ({ page }) => { const uniqueEmail = `org-test-${Date.now()}@example.com`; // Register new account await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(uniqueEmail); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(2000); // Check if redirected to org creation const currentUrl = page.url(); if (currentUrl.includes('/organizations/new') || currentUrl.includes('/org/new')) { // Verify org creation form is shown const orgNameField = page.locator('input[name*="name"]').first(); await expect(orgNameField).toBeVisible(); } else { // Org might be auto-created - just verify we're authenticated expect(currentUrl).not.toContain('/users/log-in'); expect(currentUrl).not.toContain('/users/register'); } }); test('can create organization after registration', async ({ page }) => { const uniqueEmail = `org-create-${Date.now()}@example.com`; const orgName = `E2E Test Org ${Date.now()}`; // Register await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(uniqueEmail); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(2000); // If on org creation page, create org if (page.url().includes('/organizations/new') || page.url().includes('/org/new')) { const orgNameField = page.locator('input[name*="name"]').first(); const orgSubmitButton = page.locator('button[type="submit"]').first(); await orgNameField.fill(orgName); await orgSubmitButton.click(); await page.waitForTimeout(2000); // Should redirect to dashboard or welcome page const finalUrl = page.url(); expect(finalUrl).not.toContain('/organizations/new'); expect(finalUrl).not.toContain('/org/new'); } // Verify we're authenticated and can see content await expect(page.locator('body')).toBeVisible(); }); }); test.describe('Post-Registration Flow', () => { test('can log in after registration', async ({ page }) => { const uniqueEmail = `login-after-reg-${Date.now()}@example.com`; // Step 1: Register await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(uniqueEmail); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(2000); // Complete org creation if prompted if (page.url().includes('/organizations/new') || page.url().includes('/org/new')) { const orgNameField = page.locator('input[name*="name"]').first(); if (await orgNameField.isVisible({ timeout: 2000 }).catch(() => false)) { await orgNameField.fill(`Test Org ${Date.now()}`); const orgSubmitButton = page.locator('button[type="submit"]').first(); await orgSubmitButton.click(); await page.waitForTimeout(2000); } } // Step 2: Log out const userMenu = page.locator('[data-user-menu], button:has-text("Settings"), a:has-text("Settings")').first(); if (await userMenu.isVisible({ timeout: 2000 }).catch(() => false)) { await userMenu.click(); await page.waitForTimeout(500); const logoutButton = page.locator('a:has-text("Log out"), button:has-text("Log out")').first(); if (await logoutButton.isVisible({ timeout: 2000 }).catch(() => false)) { await logoutButton.click(); await page.waitForTimeout(1000); } } // Step 3: Log back in await page.goto('/users/log-in'); const loginEmailField = page.locator('input[type="email"]').first(); const loginPasswordField = page.locator('input[type="password"]').first(); const loginSubmitButton = page.locator('button[type="submit"]').first(); await loginEmailField.fill(uniqueEmail); await loginPasswordField.fill(testPassword); await loginSubmitButton.click(); await page.waitForTimeout(2000); // Should be logged in - not on login page const finalUrl = page.url(); expect(finalUrl).not.toContain('/users/log-in'); }); test('new user can access dashboard after signup', async ({ page }) => { const uniqueEmail = `dashboard-access-${Date.now()}@example.com`; // Register await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(uniqueEmail); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(2000); // Complete org creation if needed if (page.url().includes('/organizations/new') || page.url().includes('/org/new')) { const orgNameField = page.locator('input[name*="name"]').first(); if (await orgNameField.isVisible({ timeout: 2000 }).catch(() => false)) { await orgNameField.fill(`Test Org ${Date.now()}`); const orgSubmitButton = page.locator('button[type="submit"]').first(); await orgSubmitButton.click(); await page.waitForTimeout(2000); } } // Navigate to dashboard await page.goto('/dashboard'); await page.waitForTimeout(1000); // Should be able to access dashboard await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator('body')).toBeVisible(); }); }); test.describe('Email Confirmation', () => { test('shows confirmation prompt after registration', async ({ page }) => { const uniqueEmail = `confirm-${Date.now()}@example.com`; await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(uniqueEmail); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } await submitButton.click(); await page.waitForTimeout(2000); // Look for confirmation message (might be flash message or separate page) const confirmationMessage = page.locator(':has-text("confirm"), :has-text("email"), :has-text("check")').first(); if (await confirmationMessage.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(confirmationMessage).toBeVisible(); } }); test('can resend confirmation email', async ({ page }) => { await page.goto('/users/confirm'); const resendButton = page.locator('button:has-text("Resend"), a:has-text("Resend")').first(); if (await resendButton.isVisible({ timeout: 2000 }).catch(() => false)) { await resendButton.click(); await page.waitForTimeout(1000); // Should show success message const successMessage = page.locator(':has-text("sent"), :has-text("check")').first(); if (await successMessage.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(successMessage).toBeVisible(); } } }); }); test.describe('Security Features', () => { test('registration form has CSRF protection', async ({ page }) => { await page.goto('/users/register'); // Look for CSRF token in form const csrfInput = page.locator('input[name="_csrf_token"], input[name="csrf_token"]').first(); if (await csrfInput.isVisible({ timeout: 1000 }).catch(() => false)) { const csrfValue = await csrfInput.getAttribute('value'); expect(csrfValue).toBeTruthy(); expect(csrfValue!.length).toBeGreaterThan(10); } }); test('password field is properly masked', async ({ page }) => { await page.goto('/users/register'); const passwordField = page.locator('input[type="password"]').first(); if (await passwordField.isVisible({ timeout: 2000 }).catch(() => false)) { const inputType = await passwordField.getAttribute('type'); expect(inputType).toBe('password'); } }); test('form prevents double submission', async ({ page }) => { await page.goto('/users/register'); const emailField = page.locator('input[type="email"]').first(); const passwordField = page.locator('input[type="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await emailField.fill(`double-submit-${Date.now()}@example.com`); await passwordField.fill(testPassword); const passwordConfirmField = page.locator('input[name*="password_confirmation"], input[name*="confirm"]').first(); if (await passwordConfirmField.isVisible({ timeout: 1000 }).catch(() => false)) { await passwordConfirmField.fill(testPassword); } // Try to click submit multiple times rapidly await Promise.all([ submitButton.click(), submitButton.click({ force: true }).catch(() => {}), submitButton.click({ force: true }).catch(() => {}) ]); await page.waitForTimeout(2000); // Button should be disabled after first click const isDisabled = await submitButton.isDisabled().catch(() => false); if (!isDisabled) { // At minimum, form should have been submitted (not crashed) await expect(page.locator('body')).toBeVisible(); } }); }); });