docs: add TOTP setup guide and test user creation script

- Add comprehensive TOTP_SETUP.md with multiple setup options
- Create scripts/create_test_user.exs for automated test user creation
- Update .gitignore to exclude Playwright browser binaries and artifacts
- Update README to reference TOTP setup guide
This commit is contained in:
Graham McIntire 2026-03-06 15:06:23 -06:00
parent 882a5d7eab
commit 0bdec8653f
No known key found for this signature in database
4 changed files with 377 additions and 30 deletions

12
e2e/.gitignore vendored
View file

@ -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

View file

@ -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 = '<user-id>';
```
**For detailed TOTP setup instructions, see [TOTP_SETUP.md](TOTP_SETUP.md)**
### 3. Configure Environment

237
e2e/TOTP_SETUP.md Normal file
View file

@ -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=<secret-from-above>
```
## 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())"
```

View file

@ -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