/** * 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 let anonClient: ReturnType 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({ token_hash: 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({ token_hash: 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) }) })