diff --git a/e2e/.gitignore b/e2e/.gitignore index 2e2dd085..b24895d9 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -9,6 +9,18 @@ playwright-report/ playwright/.cache/ tests/.auth/ +# Playwright browser binaries (if downloaded locally) +.playwright/ +ms-playwright/ + +# Playwright artifacts +*.webm +*.mp4 +trace.zip +screenshots/ +videos/ +traces/ + # Environment .env .env.local diff --git a/e2e/README.md b/e2e/README.md index a852b9b6..ceff3933 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -14,42 +14,19 @@ npx playwright install ### 2. Create Test User -You need a test user with TOTP enabled: +You need a test user with TOTP enabled. -**Option A: Using Phoenix Console (Recommended)** +**Quick Setup (Recommended):** -```elixir -# Start console +```bash +# From the Phoenix app directory cd .. -iex -S mix - -# Create test user -alias Towerops.{Accounts, Organizations, Repo} - -{:ok, user} = Accounts.register_user(%{ - email: "test@example.com", - password: "TestPassword123!", - password_confirmation: "TestPassword123!" -}) - -# Enable TOTP (this will print the secret) -{:ok, credential} = Accounts.create_user_credential(user, %{ - type: :totp, - totp_secret: "JBSWY3DPEHPK3PXP" # Use this secret in .env -}) - -# Create an organization for the test user -{:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id) +mix run e2e/scripts/create_test_user.exs ``` -**Option B: Using the Web UI** +This creates a test user with known credentials and outputs the configuration for your `.env` file. -1. Register a new user at http://localhost:4000/users/register -2. Log in and enable TOTP in settings -3. Get the TOTP secret from the database: - ```sql - SELECT totp_secret FROM user_credentials WHERE user_id = ''; - ``` +**For detailed TOTP setup instructions, see [TOTP_SETUP.md](TOTP_SETUP.md)** ### 3. Configure Environment diff --git a/e2e/TOTP_SETUP.md b/e2e/TOTP_SETUP.md new file mode 100644 index 00000000..662b6917 --- /dev/null +++ b/e2e/TOTP_SETUP.md @@ -0,0 +1,237 @@ +# TOTP Setup for E2E Tests + +The e2e tests need to authenticate using TOTP (Time-based One-Time Password). This guide explains how to set up a test user with TOTP enabled. + +## How It Works + +The tests use the `otplib` library to generate TOTP codes programmatically from a shared secret. This is the same secret that Google Authenticator or similar apps use. + +```typescript +import { authenticator } from 'otplib'; + +// Generate a valid 6-digit code from the secret +const token = authenticator.generate(totpSecret); +``` + +## Option 1: Create Test User with Known Secret (Recommended) + +This is the easiest approach - you create a test user with a known TOTP secret from the start. + +### Step 1: Run the Helper Script + +From the Phoenix app directory: + +```bash +cd .. # Go to towerops-web root +mix run e2e/scripts/create_test_user.exs +``` + +This will: +- Create a test user: `e2e-test@towerops.local` +- Set up TOTP with a known secret +- Create an organization for testing +- Output the credentials you need + +### Step 2: Copy Credentials to .env + +The script outputs something like: + +```bash +TEST_USER_EMAIL=e2e-test@towerops.local +TEST_USER_PASSWORD=TestPassword123! +TEST_USER_TOTP_SECRET=JBSWY3DPEHPK3PXP +``` + +Copy these to your `e2e/.env` file. + +### Step 3: Run Tests + +```bash +npm test +``` + +## Option 2: Use Existing User and Extract TOTP Secret + +If you already have a test user with TOTP enabled, you can extract the secret. + +### Get Secret from Database + +**Via IEx:** + +```elixir +# Start console +iex -S mix + +# Get the TOTP secret +user = Towerops.Accounts.get_user_by_email("your-test@example.com") +credential = Towerops.Repo.get_by(Towerops.Accounts.UserCredential, user_id: user.id, type: :totp) +IO.puts("TOTP Secret: #{credential.totp_secret}") +``` + +**Via SQL:** + +```sql +SELECT u.email, uc.totp_secret +FROM users u +JOIN user_credentials uc ON uc.user_id = u.id +WHERE u.email = 'your-test@example.com' + AND uc.type = 'totp'; +``` + +### Add to .env + +```bash +TEST_USER_EMAIL=your-test@example.com +TEST_USER_PASSWORD=YourPassword123! +TEST_USER_TOTP_SECRET= +``` + +## Option 3: Create Test User Manually + +If you prefer to create the test user manually in IEx: + +```elixir +# Start console +cd .. # Go to towerops-web root +iex -S mix + +# Create test user +alias Towerops.{Accounts, Organizations, Repo} + +{:ok, user} = Accounts.register_user(%{ + email: "e2e-test@towerops.local", + password: "TestPassword123!", + password_confirmation: "TestPassword123!" +}) + +# Set a known TOTP secret +totp_secret = "JBSWY3DPEHPK3PXP" + +{:ok, _credential} = Accounts.create_user_credential(user, %{ + type: :totp, + totp_secret: totp_secret, + enabled: true +}) + +# Create an organization +{:ok, _org} = Organizations.create_organization(%{ + name: "E2E Test Org" +}, user.id) + +# Print credentials +IO.puts("\n=== E2E Test User Created ===") +IO.puts("Email: e2e-test@towerops.local") +IO.puts("Password: TestPassword123!") +IO.puts("TOTP Secret: #{totp_secret}") +IO.puts("\nAdd these to e2e/.env:") +IO.puts("TEST_USER_EMAIL=e2e-test@towerops.local") +IO.puts("TEST_USER_PASSWORD=TestPassword123!") +IO.puts("TEST_USER_TOTP_SECRET=#{totp_secret}") +``` + +## Option 4: Disable TOTP (Not Recommended) + +If you want to skip TOTP entirely for testing (not recommended for production-like testing): + +### 1. Disable TOTP for Test User + +```elixir +user = Accounts.get_user_by_email("test@example.com") +credential = Repo.get_by(Accounts.UserCredential, user_id: user.id, type: :totp) +if credential, do: Repo.delete(credential) +``` + +### 2. Update Auth Setup + +Edit `tests/auth.setup.ts` to skip TOTP: + +```typescript +// Remove or comment out TOTP section +await page.getByLabel('Email').fill(email); +await page.getByLabel('Password').fill(password); +await page.getByRole('button', { name: 'Sign in' }).click(); + +// Skip TOTP - go straight to dashboard +await page.waitForURL('**/orgs'); +``` + +### 3. Remove TOTP_SECRET requirement + +```typescript +const totpSecret = process.env.TEST_USER_TOTP_SECRET; +// Remove the error check +``` + +## Troubleshooting + +### "Invalid authentication code" errors + +**Cause:** The TOTP secret doesn't match what's in the database, or there's clock skew. + +**Solution:** +1. Verify the secret matches: `SELECT totp_secret FROM user_credentials WHERE user_id = '...'` +2. Check system time is correct: `date` +3. Try generating a code manually to test: + ```bash + node -e "console.log(require('otplib').authenticator.generate('YOUR_SECRET'))" + ``` + +### "User not found" or login fails + +**Cause:** Test user doesn't exist or credentials are wrong. + +**Solution:** +1. Verify user exists: `SELECT * FROM users WHERE email = 'e2e-test@towerops.local'` +2. Check password in `.env` matches what you set +3. Try logging in manually via the web UI to confirm credentials + +### "No organization found" + +**Cause:** Test user doesn't belong to any organization. + +**Solution:** +```elixir +user = Accounts.get_user_by_email("e2e-test@towerops.local") +{:ok, _org} = Organizations.create_organization(%{name: "E2E Test Org"}, user.id) +``` + +### Tests fail at TOTP step + +**Cause:** TOTP element not found or timing issue. + +**Solution:** +1. Verify TOTP is enabled for the user +2. Check the selector in `auth.setup.ts` matches your login form +3. Add more specific selectors or waiting logic + +## Security Notes + +- **Never commit `.env` file** - It's in `.gitignore` for a reason +- **Use dedicated test accounts** - Don't reuse production credentials +- **Rotate secrets regularly** - Especially for staging environments +- **Limit test user permissions** - Only give access to test organizations + +## Common TOTP Secrets for Testing + +These are publicly known test secrets, safe for local development: + +``` +JBSWY3DPEHPK3PXP # "Hello!" in Base32 +GEZDGNBVGY3TQOJQ # "12345678" in Base32 +``` + +**Never use these in production!** + +## Advanced: Custom TOTP Secret + +To generate your own TOTP secret: + +```elixir +# In IEx +:crypto.strong_rand_bytes(20) |> Base.encode32(padding: false) +``` + +Or in bash: +```bash +node -e "console.log(require('otplib').authenticator.generateSecret())" +``` diff --git a/e2e/scripts/create_test_user.exs b/e2e/scripts/create_test_user.exs new file mode 100644 index 00000000..04152de9 --- /dev/null +++ b/e2e/scripts/create_test_user.exs @@ -0,0 +1,121 @@ +# Script to create an e2e test user with TOTP enabled +# +# Usage: +# mix run e2e/scripts/create_test_user.exs + +alias Towerops.Accounts +alias Towerops.Organizations +alias Towerops.Repo + +email = "e2e-test@towerops.local" +password = "TestPassword123!" +totp_secret = "JBSWY3DPEHPK3PXP" + +IO.puts("\nšŸŽ­ Creating E2E Test User...") +IO.puts("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") + +# Check if user already exists +case Accounts.get_user_by_email(email) do + nil -> + # Create new user + IO.puts("šŸ“ Registering user: #{email}") + + case Accounts.register_user(%{ + email: email, + password: password, + password_confirmation: password + }) do + {:ok, user} -> + IO.puts("āœ… User created successfully!") + + # Set up TOTP + IO.puts("šŸ” Setting up TOTP authentication...") + + case Accounts.create_user_credential(user, %{ + type: :totp, + totp_secret: totp_secret, + enabled: true + }) do + {:ok, _credential} -> + IO.puts("āœ… TOTP enabled!") + + # Create organization + IO.puts("šŸ¢ Creating test organization...") + + case Organizations.create_organization(%{name: "E2E Test Org"}, user.id) do + {:ok, org} -> + IO.puts("āœ… Organization created: #{org.name}") + + # Print credentials + IO.puts("\n" <> String.duplicate("━", 40)) + IO.puts("\n✨ E2E Test User Ready!") + IO.puts("\nAdd these to e2e/.env:\n") + IO.puts("TEST_USER_EMAIL=#{email}") + IO.puts("TEST_USER_PASSWORD=#{password}") + IO.puts("TEST_USER_TOTP_SECRET=#{totp_secret}") + IO.puts("\n" <> String.duplicate("━", 40) <> "\n") + + {:error, changeset} -> + IO.puts("āŒ Failed to create organization:") + IO.inspect(changeset.errors) + end + + {:error, changeset} -> + IO.puts("āŒ Failed to set up TOTP:") + IO.inspect(changeset.errors) + end + + {:error, changeset} -> + IO.puts("āŒ Failed to create user:") + IO.inspect(changeset.errors) + end + + user -> + IO.puts("āš ļø User already exists: #{email}") + IO.puts("\nExisting user details:") + + # Get TOTP credential + credential = Repo.get_by(Accounts.UserCredential, user_id: user.id, type: :totp) + + if credential do + IO.puts("\n" <> String.duplicate("━", 40)) + IO.puts("\n✨ E2E Test User Credentials:") + IO.puts("\nAdd these to e2e/.env:\n") + IO.puts("TEST_USER_EMAIL=#{email}") + IO.puts("TEST_USER_PASSWORD=#{password}") + IO.puts("TEST_USER_TOTP_SECRET=#{credential.totp_secret}") + IO.puts("\n" <> String.duplicate("━", 40) <> "\n") + else + IO.puts("āš ļø No TOTP credential found for this user") + IO.puts("\nTo set up TOTP manually, run:") + + IO.puts(""" + + user = Towerops.Accounts.get_user_by_email("#{email}") + Towerops.Accounts.create_user_credential(user, %{ + type: :totp, + totp_secret: "#{totp_secret}", + enabled: true + }) + """) + end + + # Check for organizations + orgs = + user.id + |> Organizations.list_user_organizations() + |> Enum.filter(&(&1.role == :owner)) + + if Enum.empty?(orgs) do + IO.puts("\nāš ļø User has no organizations") + IO.puts("\nTo create one, run:") + + IO.puts(""" + + user = Towerops.Accounts.get_user_by_email("#{email}") + Towerops.Organizations.create_organization(%{name: "E2E Test Org"}, user.id) + """) + else + IO.puts("\nāœ… User has #{length(orgs)} organization(s)") + end +end