diff --git a/server/test/db-smoke.js b/server/test/db-smoke.js new file mode 100644 index 0000000..8105dfe --- /dev/null +++ b/server/test/db-smoke.js @@ -0,0 +1,123 @@ +// 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); });