All files / web/e2e auth.setup.ts

0% Statements 0/105
0% Branches 0/1
0% Functions 0/1
0% Lines 0/105

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106                                                                                                                                                                                                                   
/**
 * Playwright global setup: forge an admin JWT and save storage state.
 *
 * This runs before all test projects. It mints an encrypted NextAuth v5 JWT
 * with admin credentials, sets the session cookie, and persists the browser
 * storage state so every test automatically has admin auth.
 *
 * Handles both local (HTTP) and production (HTTPS) environments:
 * - HTTP:  cookie name = `authjs.session-token`
 * - HTTPS: cookie name = `__Secure-authjs.session-token`
 * The HKDF salt is the cookie name, so the derived key differs per environment.
 */

import { test as setup } from '@playwright/test'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { hkdf } from '@panva/hkdf'
import { EncryptJWT } from 'jose'

const STORAGE_STATE_PATH = resolve(__dirname, '.auth/admin.json')

/** Production AUTH_SECRET (from k8s secret app-env in abaci namespace) */
const PROD_AUTH_SECRET = 'hh5xTZLHrs0euq3l6e30fJsBHVhAkX2ROWfQ0dBDpiI='

/** Admin user IDs differ between local dev DB and production DB */
const LOCAL_ADMIN_USER_ID = 'g1c8pkb2fa4qiv5qc46m4js9'
const PROD_ADMIN_USER_ID = 'urzg0d1wnpu11bgvc62nl1t0'

/**
 * Read AUTH_SECRET from .env.local (grep-style to avoid bash multiline issues).
 */
function getLocalAuthSecret(): string {
  const envPath = resolve(__dirname, '../.env.local')
  const content = readFileSync(envPath, 'utf-8')
  for (const line of content.split('\n')) {
    const trimmed = line.trim()
    if (trimmed.startsWith('AUTH_SECRET=')) {
      let value = trimmed.slice('AUTH_SECRET='.length)
      if (
        (value.startsWith('"') && value.endsWith('"')) ||
        (value.startsWith("'") && value.endsWith("'"))
      ) {
        value = value.slice(1, -1)
      }
      return value
    }
  }
  throw new Error('AUTH_SECRET not found in .env.local')
}

/**
 * Mint an encrypted NextAuth v5 JWT.
 *
 * Uses the same encryption as NextAuth: HKDF-SHA256 key derivation with
 * A256CBC-HS512 encryption. The HKDF info string includes the cookie name,
 * which differs between HTTP and HTTPS environments.
 */
async function mintAdminJWT(secret: string, cookieName: string, userId: string): Promise<string> {
  const info = `Auth.js Generated Encryption Key (${cookieName})`
  const key = await hkdf('sha256', secret, cookieName, info, 64)

  const now = Math.floor(Date.now() / 1000)
  const payload = {
    name: 'Test Admin',
    email: 'hallock@gmail.com',
    picture: null,
    sub: userId,
    iat: now,
    exp: now + 86400,
  }

  return new EncryptJWT(payload)
    .setProtectedHeader({ alg: 'dir', enc: 'A256CBC-HS512' })
    .setIssuedAt()
    .setExpirationTime('24h')
    .encrypt(new Uint8Array(key))
}

setup('forge admin JWT and save storage state', async ({ browser }) => {
  const baseURL = setup.info().project.use.baseURL || 'http://localhost:3002'
  const url = new URL(baseURL)
  const isSecure = url.protocol === 'https:'

  const secret = isSecure ? PROD_AUTH_SECRET : getLocalAuthSecret()
  const cookieName = isSecure ? '__Secure-authjs.session-token' : 'authjs.session-token'
  const userId = isSecure ? PROD_ADMIN_USER_ID : LOCAL_ADMIN_USER_ID
  const token = await mintAdminJWT(secret, cookieName, userId)

  const context = await browser.newContext()

  await context.addCookies([
    {
      name: cookieName,
      value: token,
      domain: url.hostname,
      path: '/',
      httpOnly: true,
      secure: isSecure,
      sameSite: 'Lax',
    },
  ])

  await context.storageState({ path: STORAGE_STATE_PATH })
  await context.close()
})