import { describe, expect, it } from 'vitest'; import { openDb } from './index'; import { runMigrations } from './migrate'; import { makeDb, seedFixture } from '../testing/helpers'; const throwsConstraint = (fn: () => unknown) => expect(fn).toThrow(/CHECK|UNIQUE|FOREIGN KEY|constraint/i); describe('migrations', () => { it('apply once and are then a no-op', () => { const db = openDb(':memory:'); expect(runMigrations(db)).toEqual(['001_init.sql']); expect(runMigrations(db)).toEqual([]); }); }); describe('schema rules', () => { const orderFor = (db: ReturnType) => { const f = seedFixture(db); const id = Number( db .prepare( `INSERT INTO orders (shop_id, draw_date, source, created_by_user_id, raw_text) VALUES (?, '2026-11-03', 'whatsapp_text', ?, '0888')`, ) .run(f.shops.a, f.admin.id).lastInsertRowid, ); return { f, orderId: id }; }; it('accepts a normal order line, including leading zeros', () => { const db = makeDb(); const { orderId } = orderFor(db); db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, ?)').run(orderId, '0057', 2); const row = db.prepare('SELECT ending, qty FROM order_lines').get() as { ending: string; qty: number }; expect(row).toEqual({ ending: '0057', qty: 2 }); }); it('rejects an ending that is not exactly 4 digits', () => { const db = makeDb(); const { orderId } = orderFor(db); const add = (ending: string) => () => db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, 1)').run(orderId, ending); throwsConstraint(add('888')); throwsConstraint(add('08888')); throwsConstraint(add('08a8')); }); it('rejects a quantity of zero or less', () => { const db = makeDb(); const { orderId } = orderFor(db); const add = (qty: number) => () => db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, ?)').run(orderId, '0888', qty); throwsConstraint(add(0)); throwsConstraint(add(-1)); }); it('requires a well-formed draw date on every order', () => { const db = makeDb(); const f = seedFixture(db); const add = (date: string | null) => () => db .prepare("INSERT INTO orders (shop_id, draw_date, source, created_by_user_id, raw_text) VALUES (?, ?, 'web', ?, '0888')") .run(f.shops.a, date, f.admin.id); expect(add('2026-11-03')).not.toThrow(); throwsConstraint(add(null)); throwsConstraint(add('3/11/2026')); }); it('only allows prize tiers 4 to 9', () => { const db = makeDb(); const draw = Number( db.prepare("INSERT INTO draws (draw_date, scheme, draw_code) VALUES ('2026-11-03', 'TEST', 'TS-1')").run().lastInsertRowid, ); const add = (tier: number) => () => db.prepare('INSERT INTO prizes (draw_id, tier, amount, ending) VALUES (?, ?, 100, ?)').run(draw, tier, '1234'); expect(add(4)).not.toThrow(); throwsConstraint(add(3)); throwsConstraint(add(10)); }); it('keeps phone numbers unique but allows several users without one', () => { const db = makeDb(); const f = seedFixture(db); const add = (username: string, phone: string | null) => () => db .prepare('INSERT INTO users (username, password_hash, phone) VALUES (?, ?, ?)') .run(username, 'x', phone); throwsConstraint(add('dup', f.admin.phone)); expect(add('nophone1', null)).not.toThrow(); expect(add('nophone2', null)).not.toThrow(); }); it('refuses a phone number that is not digits of a sensible length', () => { const db = makeDb(); const add = (phone: string) => () => db.prepare('INSERT INTO users (username, password_hash, phone) VALUES (?, ?, ?)').run('u' + phone, 'x', phone); throwsConstraint(add('+919847049032')); throwsConstraint(add('98470 49032')); throwsConstraint(add('1234567')); // too short expect(add('919847049032')).not.toThrow(); }); it('keeps usernames unique', () => { const db = makeDb(); seedFixture(db); const add = () => db.prepare("INSERT INTO users (username, password_hash) VALUES ('admin', 'x')").run(); throwsConstraint(add); }); it('refuses a duplicate WhatsApp message id', () => { const db = makeDb(); const add = () => db.prepare("INSERT INTO ingest_log (wa_message_id, kind, outcome) VALUES ('M1', 'text', 'accepted')").run(); expect(add).not.toThrow(); throwsConstraint(add); }); it('deletes an order\'s lines together with the order', () => { const db = makeDb(); const { orderId } = orderFor(db); db.prepare('INSERT INTO order_lines (order_id, ending, qty) VALUES (?, ?, 1)').run(orderId, '0888'); db.prepare('DELETE FROM orders WHERE id = ?').run(orderId); expect(db.prepare('SELECT COUNT(*) AS n FROM order_lines').get()).toEqual({ n: 0 }); }); });