Adds detailed e2e tests for user registration: Complete signup flows: - Account creation with email/password - Organization creation for new users - Post-registration login and dashboard access Validation testing: - Invalid email format - Weak passwords - Duplicate email addresses - Password mismatch errors Post-registration: - Login after signup - Dashboard access - Email confirmation prompts and resend Security: - CSRF token protection - Password field masking - Double-submit prevention These tests catch issues in the critical signup funnel that would prevent new users from creating accounts.
481 lines
19 KiB
TypeScript
481 lines
19 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|