You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/test/db.test.ts

81 lines
4.0 KiB
TypeScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { describe, it, expect } from 'vitest'
import { openDb } from '../src/db'
import { writeAudit, listAudit } from '../src/audit'
import { seedIfEmpty } from '../src/seed'
import { createClient } from '../src/repos-clients'
import { createModule, assignModule } from '../src/repos-modules'
describe('hq db', () => {
it('creates every HQ-1 table', async () => {
const db = openDb(':memory:')
const names = (await db.all<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table'`)).map(r => r.name)
for (const t of ['staff_user','session','client','module','module_price_book','client_module',
'tax_class','doc_series','document','payment','payment_allocation','document_event',
'email_account','email_log','setting','audit_log','stg_client','stg_invoice'])
expect(names, `missing table ${t}`).toContain(t)
})
it('audit writes and lists newest-first', async () => {
const db = openDb(':memory:')
await writeAudit(db, 'u1', 'create', 'client', 'c1', undefined, { name: 'Acme' })
await writeAudit(db, 'u1', 'update', 'client', 'c1', { name: 'Acme' }, { name: 'Acme Ltd' })
const rows = await listAudit(db)
expect(rows).toHaveLength(2)
expect(rows[0]!.action).toBe('update')
expect(JSON.parse(rows[0]!.after_json!)).toEqual({ name: 'Acme Ltd' })
})
// FIX 4 (regression): the APEX importer inserted client_module rows WITHOUT created_at, so
// every imported project with zero ticks was permanently NULL-aged. migrate() must backfill
// it from installed_on where that's known — using a real (non-:memory:) DB reopened twice,
// since the backfill runs as part of migrate() on every open, not just column-creation.
describe('client_module.created_at backfill (FIX 4)', () => {
it('backfills created_at from installed_on for a legacy row missing it', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-db-backfill-'))
try {
let db = openDb(dir)
await seedIfEmpty(db)
const client = await createClient(db, 'u1', { name: 'Legacy Client', stateCode: '32' })
const mod = await createModule(db, 'u1', { code: 'LEGACYBF', name: 'Legacy Module' })
const cm = await assignModule(db, 'u1', { clientId: client.id, moduleId: mod.id, kind: 'yearly' })
// Simulate the actual APEX-importer bug: installed_on known, created_at never stamped.
await db.run(`UPDATE client_module SET created_at=NULL, installed_on=? WHERE id=?`, '2025-11-03', cm.id)
await db.close()
db = openDb(dir) // reopening re-runs migrate() — the backfill must heal it unprompted
const row = await db.get<{ created_at: string | null; installed_on: string | null }>(
`SELECT created_at, installed_on FROM client_module WHERE id=?`, cm.id,
)
expect(row!.created_at).toBe('2025-11-03')
await db.close()
} finally {
fs.rmSync(dir, { recursive: true, force: true })
}
})
it('leaves created_at NULL when installed_on is also unknown — never fabricates a date', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-db-backfill-'))
try {
let db = openDb(dir)
await seedIfEmpty(db)
const client = await createClient(db, 'u1', { name: 'Legacy Client 2', stateCode: '32' })
const mod = await createModule(db, 'u1', { code: 'LEGACYBF2', name: 'Legacy Module 2' })
const cm = await assignModule(db, 'u1', { clientId: client.id, moduleId: mod.id, kind: 'yearly' })
await db.run(`UPDATE client_module SET created_at=NULL WHERE id=?`, cm.id) // installed_on stays NULL too
await db.close()
db = openDb(dir)
const row = await db.get<{ created_at: string | null }>(
`SELECT created_at FROM client_module WHERE id=?`, cm.id,
)
expect(row!.created_at).toBeNull()
await db.close()
} finally {
fs.rmSync(dir, { recursive: true, force: true })
}
})
})
})