Files
oysqn.app/tests/integration/auth-session.test.ts
Patrick Toal c789454810 docs: Update architecture for supabase
test: Add tests for auth workflow
2026-04-12 10:14:44 -04:00

127 lines
4.2 KiB
TypeScript

/**
* Integration tests for auth session creation via magic link flow.
*
* Requires local Supabase to be running:
* DOCKER_HOST=unix:///run/user/1000/podman/podman.sock npx supabase start
*
* Run with:
* yarn test:integration
*
* Uses Supabase admin API (service role key) to:
* 1. Create a temporary test user
* 2. Generate a magic link token
* 3. Exchange the token for a session
* 4. Verify the session is valid
* 5. Clean up the test user
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createClient } from '@supabase/supabase-js'
// Local Supabase defaults (from `npx supabase status`)
const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://localhost:54321'
const SUPABASE_ANON_KEY = process.env.SUPABASE_KEY ?? ''
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? ''
const TEST_EMAIL = `test-auth-${Date.now()}@oysqn.test`
let adminClient: ReturnType<typeof createClient>
let anonClient: ReturnType<typeof createClient>
let testUserId: string | undefined
beforeAll(() => {
if (!SUPABASE_SERVICE_ROLE_KEY) {
throw new Error(
'SUPABASE_SERVICE_ROLE_KEY is required for integration tests.\n' +
'Run `npx supabase status` to get the service_role key and set it as an env var.'
)
}
adminClient = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: { autoRefreshToken: false, persistSession: false },
})
anonClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: { autoRefreshToken: false, persistSession: false },
})
})
afterAll(async () => {
if (testUserId) {
await adminClient.auth.admin.deleteUser(testUserId)
}
})
describe('magic link login — session creation', () => {
it('creates a confirmed test user via admin API', async () => {
const { data, error } = await adminClient.auth.admin.createUser({
email: TEST_EMAIL,
email_confirm: true,
})
expect(error).toBeNull()
expect(data.user).toBeDefined()
expect(data.user!.email).toBe(TEST_EMAIL)
testUserId = data.user!.id
})
it('generates a magic link token for the test user', async () => {
const { data, error } = await adminClient.auth.admin.generateLink({
type: 'magiclink',
email: TEST_EMAIL,
})
expect(error).toBeNull()
expect(data.properties?.hashed_token).toBeTruthy()
})
it('exchanging the OTP token creates a valid session in Supabase', async () => {
// Generate a fresh link for this test (tokens are single-use)
const { data: linkData, error: linkError } = await adminClient.auth.admin.generateLink({
type: 'magiclink',
email: TEST_EMAIL,
})
expect(linkError).toBeNull()
const token = linkData.properties?.hashed_token
expect(token).toBeTruthy()
// Exchange the token — this is what happens when the user clicks the magic link
// and /auth/callback calls supabase.auth.verifyOtp (hash-based) or
// supabase.auth.exchangeCodeForSession (PKCE)
const { data: sessionData, error: sessionError } = await anonClient.auth.verifyOtp({
email: TEST_EMAIL,
token: token!,
type: 'magiclink',
})
expect(sessionError).toBeNull()
expect(sessionData.session).not.toBeNull()
expect(sessionData.session!.access_token).toBeTruthy()
expect(sessionData.session!.user.email).toBe(TEST_EMAIL)
})
it('session allows access to authenticated Supabase queries', async () => {
const { data: linkData } = await adminClient.auth.admin.generateLink({
type: 'magiclink',
email: TEST_EMAIL,
})
const { data: sessionData } = await anonClient.auth.verifyOtp({
email: TEST_EMAIL,
token: linkData.properties!.hashed_token!,
type: 'magiclink',
})
// Create an authenticated client using the session token
const authedClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: { autoRefreshToken: false, persistSession: false },
global: {
headers: { Authorization: `Bearer ${sessionData.session!.access_token}` },
},
})
// Verify the session resolves to the correct user
const { data: { user }, error } = await authedClient.auth.getUser()
expect(error).toBeNull()
expect(user).not.toBeNull()
expect(user!.email).toBe(TEST_EMAIL)
})
})