diff --git a/__tests__/auth-helpers.test.ts b/__tests__/auth-helpers.test.ts index b0f8004..adc92c5 100644 --- a/__tests__/auth-helpers.test.ts +++ b/__tests__/auth-helpers.test.ts @@ -1,6 +1,44 @@ import { describe, it, expect } from 'vitest' import { z } from 'zod' -import { parseInput } from '@/lib/auth-helpers' +import { + parseInput, + getAuthMember, + requireMember, + requireMemberWithRole, + type ServerSupabase, +} from '@/lib/auth-helpers' +import { ADMIN_ROLES } from '@/lib/permissions' + +/** + * Minimal mock of the query the auth helpers issue: + * supabase.auth.getUser() + * supabase.from('space_members').select(...).eq(...).in(...).single() + */ +function mockClient(opts: { + user?: { id: string } | null + member?: Record | null +}): ServerSupabase { + const chain = { + select: () => chain, + eq: () => chain, + in: () => chain, + single: () => Promise.resolve({ data: opts.member ?? null }), + } + return { + auth: { getUser: () => Promise.resolve({ data: { user: opts.user ?? null } }) }, + from: () => chain, + } as unknown as ServerSupabase +} + +const activeMember = (role: string, status = 'current') => ({ + id: 'm1', + space_id: 'space-A', + user_id: 'u1', + role, + status, + display_name: 'M', + handle: 'm', +}) describe('parseInput', () => { const schema = z.object({ @@ -41,3 +79,66 @@ describe('parseInput', () => { } }) }) + +describe('getAuthMember', () => { + it('returns null when there is no authenticated user', async () => { + expect(await getAuthMember(mockClient({ user: null }))).toBeNull() + }) + + it('returns null when the user has no active membership', async () => { + expect(await getAuthMember(mockClient({ user: { id: 'u1' }, member: null }))).toBeNull() + }) + + it('returns the active member for an authenticated user', async () => { + const m = await getAuthMember(mockClient({ user: { id: 'u1' }, member: activeMember('member') })) + expect(m?.space_id).toBe('space-A') + expect(m?.role).toBe('member') + }) +}) + +describe('requireMember', () => { + it('errors when unauthenticated', async () => { + const r = await requireMember(mockClient({ user: null })) + expect(r.ok).toBe(false) + }) + + it('ok for an active member', async () => { + const r = await requireMember(mockClient({ user: { id: 'u1' }, member: activeMember('member') })) + expect(r.ok).toBe(true) + }) +}) + +describe('requireMemberWithRole', () => { + it('rejects an unverified member even with an admin role (require_approval gate)', async () => { + const r = await requireMemberWithRole( + mockClient({ user: { id: 'u1' }, member: activeMember('admin', 'unverified') }), + ADMIN_ROLES, + ) + expect(r.ok).toBe(false) + if (!r.ok) expect(r.error).toMatch(/pending approval/i) + }) + + it('rejects a current member whose role is not allowed', async () => { + const r = await requireMemberWithRole( + mockClient({ user: { id: 'u1' }, member: activeMember('member') }), + ADMIN_ROLES, + ) + expect(r.ok).toBe(false) + }) + + it('allows a current admin', async () => { + const r = await requireMemberWithRole( + mockClient({ user: { id: 'u1' }, member: activeMember('admin') }), + ADMIN_ROLES, + ) + expect(r.ok).toBe(true) + }) + + it('allows a late member to keep their privileged role (dues lapse is not an authz downgrade)', async () => { + const r = await requireMemberWithRole( + mockClient({ user: { id: 'u1' }, member: activeMember('board', 'late') }), + ADMIN_ROLES, + ) + expect(r.ok).toBe(true) + }) +})