towerops/e2e/TOTP_SETUP.md
Graham McIntire 0bdec8653f
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
2026-03-06 15:06:23 -06:00

5.9 KiB

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.

import { authenticator } from 'otplib';

// Generate a valid 6-digit code from the secret
const token = authenticator.generate(totpSecret);

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:

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:

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

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:

# 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:

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

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:

# 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}")

If you want to skip TOTP entirely for testing (not recommended for production-like testing):

1. Disable TOTP for Test User

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:

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

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

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:

# In IEx
:crypto.strong_rand_bytes(20) |> Base.encode32(padding: false)

Or in bash:

node -e "console.log(require('otplib').authenticator.generateSecret())"