geocrop-platform./apps/nextgen/server/tests/messages-create-notificatio...

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);
});
});