149 lines
4.9 KiB
JavaScript
149 lines
4.9 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
}); |