64 lines
3.0 KiB
TypeScript
64 lines
3.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
|
||
const mocks = vi.hoisted(() => ({
|
||
exportToS3: vi.fn(async () => 'exports/visitor-1/now.json' as string | null),
|
||
notes: [{ id: 'n-1', title: 'Plan' }],
|
||
loggerInfo: vi.fn(),
|
||
loggerRecordException: vi.fn(),
|
||
log: vi.fn(async () => {}),
|
||
captureException: vi.fn(),
|
||
}))
|
||
|
||
vi.mock('@/shared/api', () => ({
|
||
db: () => ({
|
||
select: () => ({ from: () => ({ where: () => ({ orderBy: () => ({ limit: async () => mocks.notes }) }) }) }),
|
||
}),
|
||
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
|
||
audit: () => ({ log: mocks.log }),
|
||
serverErrors: () => ({ captureException: mocks.captureException }),
|
||
}))
|
||
vi.mock('@/shared/lib/optional', () => ({ exportToS3: mocks.exportToS3 }))
|
||
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
|
||
|
||
import { exportNotes } from '@/features/export-notes/api/actions'
|
||
|
||
describe('exportNotes', () => {
|
||
beforeEach(() => {
|
||
mocks.exportToS3.mockClear()
|
||
mocks.loggerInfo.mockClear()
|
||
mocks.loggerRecordException.mockClear()
|
||
mocks.log.mockClear()
|
||
mocks.captureException.mockClear()
|
||
})
|
||
|
||
it('writes the visitor’s own notes to S3 once, logs the outcome, and audits notes.exported with the key', async () => {
|
||
const result = await exportNotes(null, new FormData())
|
||
expect(mocks.exportToS3).toHaveBeenCalledTimes(1)
|
||
expect(mocks.exportToS3).toHaveBeenCalledWith('visitor-1', mocks.notes)
|
||
expect(result).toEqual({ ok: true, key: 'exports/visitor-1/now.json', size: expect.any(Number) })
|
||
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'ok' })
|
||
expect(mocks.log).toHaveBeenCalledWith('notes.exported', {
|
||
actor: { kind: 'human', id: 'visitor-1' },
|
||
target: { id: 'exports/visitor-1/now.json' },
|
||
})
|
||
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('reports S3 as not linked, logs it as skipped, and audits nothing, when exportToS3 returns null', async () => {
|
||
mocks.exportToS3.mockResolvedValueOnce(null)
|
||
const result = await exportNotes(null, new FormData())
|
||
expect(result).toEqual({ ok: false, message: 'S3 is not linked; nothing was exported.' })
|
||
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'skipped' })
|
||
expect(mocks.log).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('reports the error, logs and captures it, when the S3 write throws', async () => {
|
||
mocks.exportToS3.mockRejectedValueOnce(new Error('bucket unreachable'))
|
||
const result = await exportNotes(null, new FormData())
|
||
expect(result).toEqual({ ok: false, message: 'bucket unreachable' })
|
||
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'notes.exported', 'actor.id': 'visitor-1' })
|
||
expect(mocks.captureException).toHaveBeenCalledTimes(1)
|
||
expect(mocks.log).not.toHaveBeenCalled()
|
||
})
|
||
})
|