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.
81 lines
4.0 KiB
TypeScript
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 })
|
|
}
|
|
})
|
|
})
|
|
})
|