Files
accounted/lib/api/__tests__/with-route-context.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

121 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { NextResponse } from 'next/server'
import { createMockSupabase } from '@/tests/helpers'
const authState = vi.hoisted(() => ({
user: { id: 'user-1' } as { id: string } | null,
}))
const requireWriteMock = vi.hoisted(() => vi.fn())
vi.mock('@/lib/auth/require-auth', () => ({
requireAuth: vi.fn(async () => {
if (!authState.user) {
return { error: NextResponse.json({ error: 'unauthorized' }, { status: 401 }) }
}
return { user: authState.user, supabase: supabaseRef.supabase }
}),
}))
vi.mock('@/lib/company/context', () => ({
getActiveCompanyId: vi.fn(),
}))
vi.mock('@/lib/auth/require-write', () => ({
requireWritePermission: (...args: unknown[]) => requireWriteMock(...args),
}))
const supabaseRef = vi.hoisted(() => ({ supabase: null as unknown }))
import { withRouteContext } from '../with-route-context'
import { getActiveCompanyId } from '@/lib/company/context'
const EMPTY_PARAMS = { params: Promise.resolve({}) }
describe('withRouteContext', () => {
beforeEach(() => {
vi.clearAllMocks()
authState.user = { id: 'user-1' }
supabaseRef.supabase = createMockSupabase().supabase
vi.mocked(getActiveCompanyId).mockResolvedValue('company-1')
requireWriteMock.mockResolvedValue({ ok: true })
})
it('resolves the company once and hands it to the write guard on write routes', async () => {
const handler = vi.fn(async () => NextResponse.json({ ok: true }))
const route = withRouteContext('test.write', handler, { requireWrite: true })
const res = await route(new Request('http://localhost/api/test', { method: 'POST' }), EMPTY_PARAMS)
expect(res.status).toBe(200)
expect(getActiveCompanyId).toHaveBeenCalledTimes(1)
expect(requireWriteMock).toHaveBeenCalledTimes(1)
expect(requireWriteMock).toHaveBeenCalledWith(supabaseRef.supabase, 'user-1', {
companyId: 'company-1',
})
expect(handler).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({ companyId: 'company-1', user: { id: 'user-1' } }),
EMPTY_PARAMS,
)
})
it('never invokes the write guard on read routes', async () => {
const route = withRouteContext('test.read', async () => NextResponse.json({ ok: true }))
const res = await route(new Request('http://localhost/api/test'), EMPTY_PARAMS)
expect(res.status).toBe(200)
expect(getActiveCompanyId).toHaveBeenCalledTimes(1)
expect(requireWriteMock).not.toHaveBeenCalled()
})
it('passes the guard 403 through with a request id and skips the handler', async () => {
requireWriteMock.mockResolvedValue({
ok: false,
response: NextResponse.json({ error: 'viewer' }, { status: 403 }),
})
const handler = vi.fn(async () => NextResponse.json({ ok: true }))
const route = withRouteContext('test.write', handler, { requireWrite: true })
const res = await route(new Request('http://localhost/api/test', { method: 'POST' }), EMPTY_PARAMS)
expect(res.status).toBe(403)
expect(res.headers.get('X-Request-Id')).toMatch(/^req_/)
expect(handler).not.toHaveBeenCalled()
})
it('returns COMPANY_CONTEXT_MISSING before the guard when no company resolves', async () => {
vi.mocked(getActiveCompanyId).mockResolvedValue(null)
const route = withRouteContext('test.write', async () => NextResponse.json({ ok: true }), {
requireWrite: true,
})
const res = await route(new Request('http://localhost/api/test', { method: 'POST' }), EMPTY_PARAMS)
expect(res.status).toBe(400)
expect(requireWriteMock).not.toHaveBeenCalled()
})
it('returns 401 from requireAuth untouched except for the request id', async () => {
authState.user = null
const route = withRouteContext('test.read', async () => NextResponse.json({ ok: true }))
const res = await route(new Request('http://localhost/api/test'), EMPTY_PARAMS)
expect(res.status).toBe(401)
expect(res.headers.get('X-Request-Id')).toMatch(/^req_/)
expect(getActiveCompanyId).not.toHaveBeenCalled()
})
it('emits a Server-Timing header with the auth, company and handler phases', async () => {
const route = withRouteContext('test.read', async () => NextResponse.json({ ok: true }))
const res = await route(new Request('http://localhost/api/test'), EMPTY_PARAMS)
expect(res.headers.get('Server-Timing')).toMatch(
/^auth;dur=\d+, company;dur=\d+, handler;dur=\d+$/,
)
})
})