/** * End-to-end check that POST /api/messages writes notifications for the * recipient. Uses an isolated SQLite DB and mounts the messages router * with a stub wsHub so we don't need the full Express bootstrap. */ const express = require('express'); const request = require('supertest'); const bcrypt = require('bcryptjs'); const Database = require('better-sqlite3'); const path = require('path'); const os = require('os'); const { jwtSecret: JWT_SECRET } = require('../src/config'); function makeDb() { const tmp = path.join(os.tmpdir(), `msg-notif-${Date.now()}.db`); const db = new Database(tmp); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, email TEXT, password TEXT, role TEXT, first_name TEXT, last_name TEXT, is_active INTEGER DEFAULT 1, is_deleted INTEGER DEFAULT 0 ); CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, sender_id INTEGER, recipient_id INTEGER, subject TEXT NOT NULL, body TEXT, is_read INTEGER DEFAULT 0, is_delivered INTEGER DEFAULT 0, is_announcement INTEGER DEFAULT 0, priority TEXT DEFAULT 'normal', group_id INTEGER, attachment_url TEXT, attachment_name TEXT, attachment_type TEXT, created_at DATETIME DEFAULT (datetime('now','localtime')), updated_at DATETIME DEFAULT (datetime('now','localtime')), last_synced_at DATETIME, sync_status TEXT DEFAULT 'pending', is_deleted INTEGER DEFAULT 0 ); CREATE TABLE notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, recipient_id INTEGER NOT NULL REFERENCES users(id), type TEXT NOT NULL CHECK (type IN ('in_app','email','sms')), subject TEXT, body TEXT, related_resource TEXT, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent','failed','read')), sent_at DATETIME, read_at DATETIME, error_message TEXT, metadata TEXT, created_at DATETIME DEFAULT (datetime('now','localtime')), updated_at DATETIME DEFAULT (datetime('now','localtime')), last_synced_at DATETIME, sync_status TEXT DEFAULT 'pending', is_deleted INTEGER DEFAULT 0 ); `); const hash = bcrypt.hashSync('secret123', 4); const alice = db.prepare(`INSERT INTO users (uid,email,password,role,first_name,last_name) VALUES (?,?,?,?,?,?)`) .run('u-alice', 'alice@x', hash, 'teacher', 'Alice', 'A'); const bob = db.prepare(`INSERT INTO users (uid,email,password,role,first_name,last_name) VALUES (?,?,?,?,?,?)`) .run('u-bob', 'bob@x', hash, 'teacher', 'Bob', 'B'); return { db, aliceId: alice.lastInsertRowid, bobId: bob.lastInsertRowid }; } function buildApp(db) { process.env.DB_PATH = db.name; // Force the controllers to re-import against the swapped DB path. for (const p of [ require.resolve('../src/controllers/notifications.controller'), require.resolve('../src/controllers/messages.controller'), ]) { delete require.cache[p]; } const messages = require('../src/controllers/messages.controller'); const app = express(); app.use(express.json()); // Stub wsHub so the controller can call .broadcastToUser without crashing. app.set('wsHub', { broadcastToUser: () => {} }); app.use('/api/messages', messages); return app; } function token(id, role) { const jwt = require('jsonwebtoken'); return jwt.sign({ id, email: 'x@x', role }, JWT_SECRET, { expiresIn: '5m' }); } describe('POST /api/messages creates a notification for the recipient', () => { let app; let db; let aliceToken; let bobToken; let aliceId; let bobId; beforeAll(() => { const built = makeDb(); db = built.db; aliceId = built.aliceId; bobId = built.bobId; app = buildApp(db); aliceToken = token(aliceId, 'teacher'); bobToken = token(bobId, 'teacher'); }); afterAll(() => { db.close(); }); test('direct message → recipient gets a notification, sender does not', async () => { const res = await request(app) .post('/api/messages') .set('Authorization', `Bearer ${aliceToken}`) .send({ recipient_id: bobId, subject: 'Hello', body: 'hi bob' }); expect(res.status).toBe(201); const bobInbox = db.prepare('SELECT * FROM notifications WHERE recipient_id = ?').all(bobId); expect(bobInbox.length).toBe(1); expect(bobInbox[0].subject).toBe('Hello'); expect(bobInbox[0].related_resource).toMatch(/^message:\d+$/); const aliceInbox = db.prepare('SELECT * FROM notifications WHERE recipient_id = ?').all(aliceId); expect(aliceInbox.length).toBe(0); }); test('400 when recipient_id is missing', async () => { const res = await request(app) .post('/api/messages') .set('Authorization', `Bearer ${aliceToken}`) .send({ subject: 'no recipient' }); expect(res.status).toBe(400); }); });