// 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) const fs = require('fs'); const os = require('os'); const path = require('path'); const DB = path.join(os.tmpdir(), 'bzc-smoke.db'); process.env.DB_PATH = DB; for (const f of [DB, DB + '-wal', DB + '-shm']) { try { fs.unlinkSync(f); } catch {} } 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 || 'sqlite') + '):'); // 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); });