Files
accounted/lib/auth/__tests__/require-write.test.ts
T
Jakob Wennberg c31933b15b perf(api): write routes stop re-resolving the active company (#1928)
* perf(api): write routes stop re-resolving the active company

withRouteContext resolves the active company (one resolve_active_company
RPC, ~40 ms p50 on prod) and then, for the 256 routes that pass
requireWrite: true, called requireWritePermission(), which resolved it a
second time before its role select. Two sequential round trips repeating
work the wrapper had just done, on every mutating request.

requireWritePermission() and getCompanyRole() now accept an optional
`known` context; the wrapper passes { companyId }, so the helper goes
straight to the membership select. Callers that pass nothing behave
exactly as before, and the shared selectRole() keeps both helpers on the
same query. The role is still looked up, never trusted from the caller.

Tests: known companyId skips resolution, known role skips the select, a
known viewer is still 403, a known company without a membership row is
still 403, legacy calls unchanged; new lib/api/__tests__/with-route-
context.test.ts pins that the wrapper resolves the company exactly once,
hands it to the guard, never calls the guard on read routes, passes the
guard's 403 through with a request id, and emits Server-Timing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(customers): viewer gate expects the wrapper to hand over the resolved company

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 13:55:48 +02:00

199 lines
6.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createMockSupabase } from '@/tests/helpers'
vi.mock('@/lib/company/context', () => ({
getActiveCompanyId: vi.fn(),
}))
import { requireWritePermission, getCompanyRole } from '../require-write'
import { getActiveCompanyId } from '@/lib/company/context'
describe('requireWritePermission', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns ok for owner', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'owner' } })
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(true)
})
it('returns ok for admin', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'admin' } })
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(true)
})
it('returns ok for member', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'member' } })
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(true)
})
it('returns 403 for viewer', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'viewer' } })
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.response.status).toBe(403)
const body = await result.response.json()
expect(body.error).toContain('läsbehörighet')
}
})
it('returns 403 when user has no membership', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: null })
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.response.status).toBe(403)
}
})
it('returns 403 when there is no active company', async () => {
const { supabase } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue(null)
const result = await requireWritePermission(supabase, 'user-1')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.response.status).toBe(403)
const body = await result.response.json()
expect(body.error).toContain('aktivt företag')
}
})
})
describe('requireWritePermission with a known route context', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('skips the active-company resolution when companyId is known', async () => {
const { supabase, mockResult } = createMockSupabase()
mockResult({ data: { role: 'member' } })
const result = await requireWritePermission(supabase, 'user-1', { companyId: 'company-9' })
expect(result.ok).toBe(true)
expect(getActiveCompanyId).not.toHaveBeenCalled()
expect(supabase.from).toHaveBeenCalledTimes(1)
expect(supabase.from).toHaveBeenCalledWith('company_members')
})
it('skips the membership select when the role is known too', async () => {
const { supabase } = createMockSupabase()
const result = await requireWritePermission(supabase, 'user-1', {
companyId: 'company-9',
role: 'admin',
})
expect(result.ok).toBe(true)
expect(getActiveCompanyId).not.toHaveBeenCalled()
expect(supabase.from).not.toHaveBeenCalled()
})
it('still rejects a known viewer role with 403', async () => {
const { supabase } = createMockSupabase()
const result = await requireWritePermission(supabase, 'user-1', {
companyId: 'company-9',
role: 'viewer',
})
expect(result.ok).toBe(false)
if (!result.ok) expect(result.response.status).toBe(403)
expect(supabase.from).not.toHaveBeenCalled()
})
it('a known company with no membership row is rejected, not trusted', async () => {
const { supabase, mockResult } = createMockSupabase()
mockResult({ data: null })
const result = await requireWritePermission(supabase, 'user-1', { companyId: 'company-9' })
expect(result.ok).toBe(false)
if (!result.ok) expect(result.response.status).toBe(403)
})
it('falls back to resolution when no context is passed (legacy callers)', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'owner' } })
const result = await requireWritePermission(supabase, 'user-1', undefined)
expect(result.ok).toBe(true)
expect(getActiveCompanyId).toHaveBeenCalledTimes(1)
})
})
describe('getCompanyRole', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns role and companyId for owner', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'owner' } })
const result = await getCompanyRole(supabase, 'user-1')
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.role).toBe('owner')
expect(result.companyId).toBe('company-1')
}
})
it('returns role for viewer (does not block)', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: { role: 'viewer' } })
const result = await getCompanyRole(supabase, 'user-1')
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.role).toBe('viewer')
expect(result.companyId).toBe('company-1')
}
})
it('returns 403 when user has no membership', async () => {
const { supabase, mockResult } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
mockResult({ data: null })
const result = await getCompanyRole(supabase, 'user-1')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.response.status).toBe(403)
}
})
it('returns 403 when there is no active company', async () => {
const { supabase } = createMockSupabase()
vi.mocked(getActiveCompanyId).mockResolvedValue(null)
const result = await getCompanyRole(supabase, 'user-1')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.response.status).toBe(403)
const body = await result.response.json()
expect(body.error).toContain('aktivt företag')
}
})
})