test(db): focused DB smoke harness for the migration (22 checks, green on SQLite)
Covers the DB-backed HTTP paths the async repo conversion touches — auth, users, messages, attachments, conversations/groups, reactions, mentions, edit/delete, polls, scheduled meetings (paginated), favorites, audit — asserting current API shapes. Runs to completion with a pass/fail count and honours DB_BACKEND so it doubles as the sqlite-vs-pg parity check at cutover. No WS/signaling (in-memory, not the DB). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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); });
|
||||
Reference in New Issue
Block a user