Files
accounted/tests/pg/transactions-document-id.pg.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

154 lines
5.6 KiB
TypeScript

import { randomUUID } from 'crypto'
import { describe, expect, it } from 'vitest'
import { seedCompany } from '@/tests/pg/fixtures'
import { getPool } from '@/tests/pg/setup'
/**
* Smoke for transactions.document_id (added 20260505140000, hardened in
* 20260506090000 and 20260506100000).
*
* Locks in:
* - FK exists and points at document_attachments(id).
* - ON DELETE RESTRICT: deleting a doc that is pinned to any tx is blocked.
* - enforce_transactions_document_immutability trigger blocks UPDATE on
* transactions.document_id when the previously-pinned document has
* propagated to a journal entry (BFL 5 kap 6 §).
*/
async function insertDocument(params: {
userId: string
companyId: string
}): Promise<string> {
const id = randomUUID()
const storagePath = `documents/${params.companyId}/test.pdf`
const sha256 = randomUUID().replace(/-/g, '').padEnd(64, '0')
await getPool().query(
`INSERT INTO public.document_attachments
(id, user_id, company_id, file_name, mime_type, file_size_bytes,
storage_path, sha256_hash, upload_source)
VALUES ($1, $2, $3, 'test.pdf', 'application/pdf', 1024,
$4, $5, 'file_upload')`,
[id, params.userId, params.companyId, storagePath, sha256],
)
return id
}
async function insertTransaction(params: {
userId: string
companyId: string
documentId?: string | null
}): Promise<string> {
const id = randomUUID()
await getPool().query(
`INSERT INTO public.transactions
(id, user_id, company_id, date, description, amount, currency, document_id)
VALUES ($1, $2, $3, '2026-05-01', 'Test tx', -1000, 'SEK', $4)`,
[id, params.userId, params.companyId, params.documentId ?? null],
)
return id
}
async function insertJournalEntry(params: {
userId: string
companyId: string
fiscalPeriodId: string
}): Promise<string> {
const id = randomUUID()
await getPool().query(
`INSERT INTO public.journal_entries
(id, user_id, company_id, fiscal_period_id, voucher_number, voucher_series,
entry_date, description, source_type, status)
VALUES ($1, $2, $3, $4, 1, 'A', '2026-05-01', 'Test', 'manual', 'draft')`,
[id, params.userId, params.companyId, params.fiscalPeriodId],
)
return id
}
describe('transactions.document_id.pg', () => {
it('attaches a document to a transaction and reads it back', async () => {
const { userId, companyId } = await seedCompany()
const docId = await insertDocument({ userId, companyId })
const txId = await insertTransaction({ userId, companyId, documentId: docId })
const res = await getPool().query<{ document_id: string | null }>(
`SELECT document_id FROM public.transactions WHERE id = $1`,
[txId],
)
expect(res.rows[0]!.document_id).toBe(docId)
})
it('ON DELETE RESTRICT: blocks deletion of a doc still pinned to a tx', async () => {
const { userId, companyId } = await seedCompany()
const docId = await insertDocument({ userId, companyId })
await insertTransaction({ userId, companyId, documentId: docId })
await expect(
getPool().query(`DELETE FROM public.document_attachments WHERE id = $1`, [docId]),
).rejects.toThrow(/violates foreign key constraint|still referenced/)
})
it('detaching first then deleting the doc succeeds', async () => {
const { userId, companyId } = await seedCompany()
const docId = await insertDocument({ userId, companyId })
const txId = await insertTransaction({ userId, companyId, documentId: docId })
await getPool().query(
`UPDATE public.transactions SET document_id = NULL WHERE id = $1`,
[txId],
)
await getPool().query(`DELETE FROM public.document_attachments WHERE id = $1`, [docId])
const res = await getPool().query<{ count: string }>(
`SELECT COUNT(*)::text as count FROM public.document_attachments WHERE id = $1`,
[docId],
)
expect(res.rows[0]!.count).toBe('0')
})
it('blocks UPDATE that detaches a document already linked to a journal entry', async () => {
const { userId, companyId, fiscalPeriodId } = await seedCompany()
const docId = await insertDocument({ userId, companyId })
const txId = await insertTransaction({ userId, companyId, documentId: docId })
const jeId = await insertJournalEntry({ userId, companyId, fiscalPeriodId })
// Simulate the categorize propagation: doc is now räkenskapsinformation.
await getPool().query(
`UPDATE public.document_attachments SET journal_entry_id = $1 WHERE id = $2`,
[jeId, docId],
)
await expect(
getPool().query(
`UPDATE public.transactions SET document_id = NULL WHERE id = $1`,
[txId],
),
).rejects.toThrow(/BFL_DOCUMENT_IMMUTABILITY/)
// And blocks swapping to a different document.
const otherDocId = await insertDocument({ userId, companyId })
await expect(
getPool().query(
`UPDATE public.transactions SET document_id = $1 WHERE id = $2`,
[otherDocId, txId],
),
).rejects.toThrow(/BFL_DOCUMENT_IMMUTABILITY/)
})
it('allows detach when the document is not yet on a journal entry', async () => {
const { userId, companyId } = await seedCompany()
const docId = await insertDocument({ userId, companyId })
const txId = await insertTransaction({ userId, companyId, documentId: docId })
await getPool().query(
`UPDATE public.transactions SET document_id = NULL WHERE id = $1`,
[txId],
)
const res = await getPool().query<{ document_id: string | null }>(
`SELECT document_id FROM public.transactions WHERE id = $1`,
[txId],
)
expect(res.rows[0]!.document_id).toBeNull()
})
})