fix(edge-fn): use user.id instead of claims.sub; fixes 500s and false cert_required fix(migrations): drop broad reservations SELECT policy; add reservation_slots view with security_invoker=false fix(tests): correct weekSlot() keys from start/end to start_time/end_time fix(tests): spread overlap test slots across separate ISO weeks fix(tests): update e2e assertion to match actual authenticated home text fix(app): hide IonMenu before user is authenticated feat(dx): add test:all script running unit, integration, and e2e in sequence docs(claude-md): document SELinux fix, Edge Function auth pattern, security_invoker behaviour
125 lines
4.2 KiB
TypeScript
125 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({
|
|
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)
|
|
})
|
|
})
|