Files
BizGaze_Remote/server/test/db-smoke.js
T
Sravan ad48829337 Retire the SQLite backend — Postgres is the only engine
The dual backend (SQLite via db.js + Postgres via schema.pg.sql) was a
maintenance foot-gun: a schema change could land on the SQLite path only and
silently 500 every read on prod (it just did, with #18/#13). Production has run
on Postgres for weeks, so SQLite is retired: ONE schema source of truth
(db/schema.pg.sql), no drift possible.

- dbx.js: default DB_BACKEND=pg; an unknown backend now fails loudly at require
  time instead of silently selecting a stale engine.
- Deleted server/db.js, server/db/sqlite.js, server/db/migrate-sqlite-to-pg.js,
  server/scripts/migrate-bizgaze-only.js (all SQLite-only, none in the runtime
  path — the running server loads db/pg.js).
- Tests (e2e, db-smoke) target Postgres now and fail-fast (skip) unless
  DATABASE_URL points at a disposable test DB — never SQLite, never prod.
- Removed the dead DB_PATH env + fixed misleading SQLite comments in the
  Dockerfile / docker-compose (kept the /data volume: it holds
  uploads/recordings/transcripts/downloads, not just the old data.db).
- CLAUDE.md: stack + repo-layout + run-locally updated for Postgres-only.

Runtime is unaffected (prod already sets DB_BACKEND=pg and pg is a prod dep);
this only removes the unused SQLite path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 08:20:26 +05:30

126 lines
7.1 KiB
JavaScript

// DB smoke test — the regression harness for the SQLite→Postgres migration.
//
// Deliberately focused on the DB-backed HTTP paths (the surface the async repo conversion touches):
// auth, users, messages, attachments, conversations/groups, reactions, mentions, edit/delete, polls,
// scheduled meetings, favorites, audit. It asserts against the CURRENT API shapes and runs to completion
// with a pass/fail count, so it stays a trustworthy before/after check as repos go async and, later, as we
// flip the backend from sqlite to pg. It does NOT exercise WebSocket signaling (in-memory, not the DB).
//
// Run: node test/db-smoke.js (uses a throwaway temp DB; DB_BACKEND env selects sqlite|pg)
// SQLite was retired 2026-08-12 — this suite runs against Postgres now. Point DATABASE_URL at a DISPOSABLE
// test database (NEVER production — the suite creates + mutates rows), e.g.:
// DATABASE_URL=postgres://bizgaze:PW@localhost:5432/bizgaze_test node test/db-smoke.js
process.env.DB_BACKEND = process.env.DB_BACKEND || 'pg';
if (!process.env.DATABASE_URL) {
console.log('SKIP db-smoke: set DATABASE_URL to a disposable Postgres test DB (SQLite retired 2026-08-12).');
process.exit(0);
}
const PORT = 8097;
process.env.PORT = PORT;
process.env.HTTPS_PORT = 8446;
const { server } = require('../server');
const BASE = `http://localhost:${PORT}`;
let passed = 0, failed = 0;
const fails = [];
function check(name, cond) {
if (cond) { passed++; } else { failed++; fails.push(name); console.log(' FAIL -', name); }
}
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function post(p, body, cookie) {
const r = await fetch(BASE + p, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}) }, body: body ? JSON.stringify(body) : undefined });
const sc = r.headers.get('set-cookie');
return { status: r.status, data: await r.json().catch(() => ({})), cookie: sc ? sc.split(';')[0] : cookie };
}
async function get(p, cookie) {
const r = await fetch(BASE + p, { headers: cookie ? { Cookie: cookie } : {} });
return { status: r.status, data: await r.json().catch(() => ({})) };
}
(async () => {
await wait(300);
console.log('DB smoke tests (backend=' + process.env.DB_BACKEND + '):');
// Auth
const reg = await post('/api/register', { email: 'admin@smoke.test', password: 'supersecret', teamName: 'Smoke Co' });
check('register', reg.status === 200 && reg.data.ok);
const login = await post('/api/login', { email: 'admin@smoke.test', password: 'supersecret' });
check('login sets cookie + tokens', !!login.cookie && !!login.data.token);
const cookie = login.cookie;
const me = await get('/api/me', cookie);
check('me = admin', me.status === 200 && me.data.role === 'admin');
const adminId = me.data.id;
// Second user + contacts
await post('/api/users', { email: 'bob@smoke.test', password: 'supersecret', name: 'Bob' }, cookie);
const bobLogin = await post('/api/login', { email: 'bob@smoke.test', password: 'supersecret' });
const bobCookie = bobLogin.cookie;
const contacts = await get('/api/messages/contacts', cookie);
check('contacts include bob', contacts.status === 200 && contacts.data.some((c) => c.email === 'bob@smoke.test'));
const bobId = (contacts.data.find((c) => c.email === 'bob@smoke.test') || {}).id;
// DM send + thread + conversations
const dm = await post('/api/messages', { to: bobId, body: 'hello bob' }, cookie);
check('send DM', dm.status === 200 && !!dm.data.id);
const th = await get('/api/messages/thread?with=' + bobId, cookie);
check('thread has the DM', Array.isArray(th.data) && th.data.some((m) => m.body === 'hello bob'));
const convs = await get('/api/messages/conversations', cookie);
check('conversations list DM', convs.status === 200 && convs.data.some((c) => c.kind === 'dm' && c.id === bobId));
// Attachment upload + send + authorized fetch
const up = await fetch(BASE + '/api/messages/upload', { method: 'POST', headers: { Cookie: cookie, 'Content-Type': 'image/png', 'X-Filename': encodeURIComponent('p.png') }, body: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) });
const upd = await up.json();
check('upload attachment', up.status === 200 && !!upd.id);
const dmA = await post('/api/messages', { to: bobId, attachmentId: upd.id }, cookie);
check('send DM with attachment', dmA.status === 200 && dmA.data.attachment && dmA.data.attachment.id === upd.id);
const fileOk = await fetch(BASE + '/files/' + upd.id, { headers: { Cookie: bobCookie } });
check('recipient can fetch the attachment (carrier auth)', fileOk.status === 200);
// Edit + delete
const em = await post('/api/messages/edit', { id: dm.data.id, body: 'hello bob (edited)' }, cookie);
check('edit message', em.status === 200);
const del = await post('/api/messages/delete', { id: dm.data.id }, cookie);
check('delete message', del.status === 200);
// Reactions
const rx = await post('/api/messages/react', { messageId: dmA.data.id, emoji: '👍' }, bobCookie);
check('react to message', rx.status === 200 && Array.isArray(rx.data.reactions) && rx.data.reactions.some((r) => r.emoji === '👍'));
// Group create + add + message + info + mention
const grp = await post('/api/groups', { name: 'Smoke Group', memberIds: [bobId] }, cookie);
check('create group', grp.status === 200 && !!grp.data.id);
const gid = grp.data.id;
const gmsg = await post('/api/messages', { group: gid, body: 'hi @everyone', mentions: ['everyone'] }, cookie);
check('group message with mention', gmsg.status === 200 && !!gmsg.data.id);
const ginfo = await get('/api/groups/info?group=' + gid, cookie);
check('group info members', ginfo.status === 200 && ginfo.data.members.length === 2);
// Poll create + vote
const poll = await post('/api/polls', { group: gid, question: 'Lunch?', options: ['Pizza', 'Sushi'] }, cookie);
check('create poll', poll.status === 200 && !!poll.data.id);
const pid = poll.data.id;
const vote = await post('/api/polls/vote', { pollId: pid, optionIdx: 0 }, bobCookie);
check('vote in poll', vote.status === 200);
// Scheduled meeting + paginated list
const sched = await post('/api/meetings/schedule', { group: gid, title: 'Sync', scheduledAt: Date.now() + 3600000 }, cookie);
check('schedule meeting', sched.status === 200 && /^\d{6}$/.test(sched.data.roomCode || ''));
const mlist = await get('/api/meetings', cookie);
check('meetings list (paginated) has it', mlist.status === 200 && Array.isArray(mlist.data.list) && mlist.data.list.some((m) => m.id === sched.data.id));
// Favorites
const fav = await post('/api/favorites', { kind: 'dm', id: bobId, on: true }, cookie);
check('set favorite', fav.status === 200 && fav.data.favorite === true);
// Audit trail
const audit = await get('/api/audit', cookie);
check('audit has login + register', Array.isArray(audit.data) && audit.data.some((a) => a.action === 'login') && audit.data.some((a) => a.action === 'user_registered'));
console.log(`\n${passed} passed, ${failed} failed.` + (failed ? ' Failing: ' + fails.join(', ') : ''));
server.close();
process.exit(failed ? 1 : 0);
})().catch((e) => { console.error('SMOKE ERROR:', e); server.close(); process.exit(2); });