towerops/e2e/TOTP_SETUP.md
Graham McIntire b3af717013
fix: update e2e scripts to use correct TOTP API
- Fix create_test_user.exs to use create_totp_device instead of create_user_credential
- Update TOTP_SETUP.md to reference UserTotpDevice schema and user_totp_devices table
- Fix function signatures to match Accounts.create_totp_device(user_id, device_name, secret)
- Add terms_of_service_consent and privacy_policy_consent to user registration
2026-03-06 15:46:11 -06:00

232 lines
5.8 KiB
Markdown

# 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")
device = Towerops.Repo.get_by(Towerops.Accounts.UserTotpDevice, user_id: user.id)
IO.puts("TOTP Secret: #{device.totp_secret}")
```
**Via SQL:**
```sql
SELECT u.email, utd.totp_secret
FROM users u
JOIN user_totp_devices utd ON utd.user_id = u.id
WHERE u.email = 'your-test@example.com';
```
### 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, _device, _secret} = Accounts.create_totp_device(user.id, "E2E Test Device", totp_secret)
# 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")
device = Repo.get_by(Accounts.UserTotpDevice, user_id: user.id)
if device, do: Repo.delete(device)
```
### 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())"
```