diff --git a/e2e/tests/signup-flow.spec.ts b/e2e/tests/signup-flow.spec.ts new file mode 100644 index 00000000..6396e5a2 --- /dev/null +++ b/e2e/tests/signup-flow.spec.ts @@ -0,0 +1,481 @@ +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(); + } + }); + }); +});