118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import type { ActorContext } from '@devrunbook/application'
|
|
import type { PlaybookCatalogSummary } from '@devrunbook/db'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { AuthenticatedWorkspaceContextError } from '../../../../server/authenticated-workspace-context'
|
|
import {
|
|
handleListPlaybooks,
|
|
type ListPlaybooksRouteDependencies,
|
|
} from './route'
|
|
|
|
const actor: ActorContext = {
|
|
userId: '00000000-0000-4000-8000-000000000001',
|
|
workspaceId: '00000000-0000-4000-8000-000000000002',
|
|
instanceRole: 'user',
|
|
workspaceRole: 'viewer',
|
|
}
|
|
|
|
function item(index: number): PlaybookCatalogSummary {
|
|
return {
|
|
id: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
|
|
slug: `playbook-${index}`,
|
|
title: `Playbook ${index}`,
|
|
summary: 'A governed outcome.',
|
|
category: 'audit',
|
|
source: 'built_in',
|
|
currentVersion: '1.0.0',
|
|
lifecycle: 'validated',
|
|
riskTier: 'low',
|
|
type: 'guided',
|
|
defaultMode: 'inspect',
|
|
defaultAutonomy: 'diagnose',
|
|
supportedModes: ['inspect'],
|
|
autonomyMin: 'observe',
|
|
autonomyMax: 'verify',
|
|
stacks: ['typescript'],
|
|
qualityStatus: 'technical-reviewed',
|
|
publishedAt: new Date('2026-01-01T00:00:00Z'),
|
|
favorite: false,
|
|
tags: ['audit'],
|
|
matchReasons: [],
|
|
digest: String(index).padStart(64, '0'),
|
|
}
|
|
}
|
|
|
|
function dependencies(
|
|
overrides: Partial<ListPlaybooksRouteDependencies> = {},
|
|
): ListPlaybooksRouteDependencies {
|
|
return {
|
|
resolveContext: vi.fn(async () => actor),
|
|
search: vi.fn(async (query) =>
|
|
Object.keys(query).length ? [item(1), item(2)] : [item(1), item(2)],
|
|
),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('playbook list route', () => {
|
|
it('authorizes, scopes filters and returns deterministic cursor pagination', async () => {
|
|
const context = dependencies()
|
|
const response = await handleListPlaybooks(
|
|
new Request(
|
|
'https://runbook.example.test/api/v1/playbooks?type=guided&limit=1',
|
|
),
|
|
context,
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(body.items).toHaveLength(1)
|
|
expect(body.nextCursor).toBeTypeOf('string')
|
|
expect(body.search).toEqual({ status: 'ready', total: 2 })
|
|
expect(context.search).toHaveBeenNthCalledWith(
|
|
1,
|
|
{ type: ['guided'] },
|
|
{ workspaceId: actor.workspaceId, userId: actor.userId },
|
|
)
|
|
})
|
|
|
|
it('authenticates before reporting query validation details', async () => {
|
|
const response = await handleListPlaybooks(
|
|
new Request(
|
|
'https://runbook.example.test/api/v1/playbooks?unsupported=true',
|
|
),
|
|
dependencies({
|
|
resolveContext: vi.fn(async () => {
|
|
throw new AuthenticatedWorkspaceContextError(
|
|
'authentication_required',
|
|
)
|
|
}),
|
|
}),
|
|
)
|
|
|
|
expect(response.status).toBe(401)
|
|
expect(await response.json()).toMatchObject({
|
|
error: { code: 'authentication_required' },
|
|
})
|
|
})
|
|
|
|
it('returns governed validation and degraded catalog states', async () => {
|
|
const invalid = await handleListPlaybooks(
|
|
new Request('https://runbook.example.test/api/v1/playbooks?limit=0'),
|
|
dependencies(),
|
|
)
|
|
const degraded = await handleListPlaybooks(
|
|
new Request('https://runbook.example.test/api/v1/playbooks'),
|
|
dependencies({
|
|
search: vi.fn(async () => {
|
|
throw new Error('postgresql://operator:secret@database/internal')
|
|
}),
|
|
}),
|
|
)
|
|
|
|
expect(invalid.status).toBe(422)
|
|
expect(degraded.status).toBe(503)
|
|
expect(JSON.stringify(await degraded.json())).not.toContain('secret')
|
|
})
|
|
})
|