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:
parent
882a5d7eab
commit
0bdec8653f
4 changed files with 377 additions and 30 deletions
12
e2e/.gitignore
vendored
12
e2e/.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
237
e2e/TOTP_SETUP.md
Normal 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())"
|
||||
```
|
||||
121
e2e/scripts/create_test_user.exs
Normal file
121
e2e/scripts/create_test_user.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue