ConusAI vendor v0.1.0-dev.20261027+9b1131fb
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const NOTE = '00000000-0000-4000-8000-0000000000aa'
|
||||
const mocks = vi.hoisted(() => ({
|
||||
log: vi.fn(async () => {}),
|
||||
revalidatePath: vi.fn(),
|
||||
loggerInfo: vi.fn(),
|
||||
loggerRecordException: vi.fn(),
|
||||
updated: undefined as { id: string } | undefined,
|
||||
}))
|
||||
|
||||
const fakeDb = {
|
||||
update: () => ({
|
||||
set: () => ({
|
||||
where: () => ({ returning: async () => (mocks.updated ? [mocks.updated] : []) }),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
|
||||
vi.mock('@/shared/api', () => ({
|
||||
db: () => fakeDb,
|
||||
audit: () => ({ log: mocks.log }),
|
||||
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
|
||||
}))
|
||||
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
|
||||
|
||||
import { updateNote } from '@/features/edit-note/api/actions'
|
||||
|
||||
function edit(title: string): FormData {
|
||||
const data = new FormData()
|
||||
data.set('title', title)
|
||||
data.set('body', '')
|
||||
return data
|
||||
}
|
||||
|
||||
describe('updateNote', () => {
|
||||
beforeEach(() => {
|
||||
mocks.log.mockClear()
|
||||
mocks.revalidatePath.mockClear()
|
||||
mocks.loggerInfo.mockClear()
|
||||
mocks.loggerRecordException.mockClear()
|
||||
mocks.updated = undefined
|
||||
})
|
||||
|
||||
it('audits note.updated exactly once with the actor and the note, and logs the outcome', async () => {
|
||||
mocks.updated = { id: NOTE }
|
||||
await updateNote(NOTE, edit('Renamed'))
|
||||
|
||||
expect(mocks.log).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.log).toHaveBeenCalledWith('note.updated', {
|
||||
actor: { kind: 'human', id: 'visitor-1' },
|
||||
target: { id: NOTE },
|
||||
})
|
||||
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.updated', { 'note.id': NOTE, outcome: 'ok' })
|
||||
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('audits nothing when the visitor did not write the note, and logs it denied', async () => {
|
||||
await updateNote(NOTE, edit('Renamed'))
|
||||
expect(mocks.log).not.toHaveBeenCalled()
|
||||
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.updated', { 'note.id': NOTE, outcome: 'denied' })
|
||||
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('logs the failure when the input is invalid', async () => {
|
||||
await expect(updateNote(NOTE, edit(' '))).rejects.toThrow()
|
||||
expect(mocks.log).not.toHaveBeenCalled()
|
||||
expect(mocks.loggerInfo).not.toHaveBeenCalled()
|
||||
expect(mocks.loggerRecordException).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'note.updated', 'note.id': NOTE })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { noteId, noteInput, noteRepository } from '@/entities/note'
|
||||
import { audit, db, logger } from '@/shared/api'
|
||||
import { ensureVisitor } from '@/shared/lib/visitor-session'
|
||||
|
||||
/** Edit a note the visitor wrote. A note they did not write is left untouched. */
|
||||
export async function updateNote(id: string, form: FormData): Promise<void> {
|
||||
try {
|
||||
const key = noteId.parse(id)
|
||||
const input = noteInput.parse({ title: form.get('title'), body: form.get('body') ?? '' })
|
||||
const actor = await ensureVisitor()
|
||||
const note = await noteRepository(db()).update(key, actor, input)
|
||||
if (!note) {
|
||||
logger()?.info('note.updated', { 'note.id': key, outcome: 'denied' })
|
||||
return
|
||||
}
|
||||
await audit().log('note.updated', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
|
||||
logger()?.info('note.updated', { 'note.id': note.id, outcome: 'ok' })
|
||||
revalidatePath('/')
|
||||
} catch (error) {
|
||||
logger()?.recordException(error, { 'conusai.action': 'note.updated', 'note.id': id })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { updateNote } from './api/actions'
|
||||
export { EditNoteForm } from './ui/EditNoteForm'
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Note } from '@/entities/note'
|
||||
import { updateNote } from '@/features/edit-note/api/actions'
|
||||
|
||||
export function EditNoteForm({ note }: { note: Note }) {
|
||||
return (
|
||||
<form action={updateNote.bind(null, note.id)}>
|
||||
<label>
|
||||
Title
|
||||
<input name="title" defaultValue={note.title} required maxLength={200} />
|
||||
</label>
|
||||
<label>
|
||||
Body
|
||||
<textarea name="body" rows={3} defaultValue={note.body} />
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user