/** * Regression test for /api/auth/refresh-roles. * * Guards against the bug where the refresh-roles endpoint returned a * stripped-down user payload (id/email/role/effective_roles only), * causing the client's auth store to blank out first_name/last_name/ * profile_image on every app load. Profile edits appeared to "disappear" * after a refresh because the in-memory copy was overwritten by the * minimal payload from /api/auth/refresh-roles. */ const express = require('express'); const request = require('supertest'); const bcrypt = require('bcryptjs'); const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); // Build an isolated app instance to avoid the global server bootstrap // (`src/index.js` starts listening on import). We mount just the auth // router under test plus a tiny stub for what the endpoint expects. const { jwtSecret: JWT_SECRET } = require('../src/config'); function buildAuthRouter(db) { const app = express(); app.use(express.json()); const auth = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'No token provided' }); try { const jwt = require('jsonwebtoken'); req.user = jwt.verify(token, JWT_SECRET); next(); } catch { res.status(401).json({ error: 'Invalid token' }); } }; app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; if (!email || !password) return res.status(400).json({ error: 'Missing credentials' }); const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); if (!bcrypt.compareSync(password, user.password)) return res.status(401).json({ error: 'Invalid credentials' }); const jwt = require('jsonwebtoken'); const token = jwt.sign({ id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '5m' }); res.json({ token, user: { id: user.id, email: user.email, role: user.role, first_name: user.first_name, last_name: user.last_name, gender: user.gender, profile_image: user.profile_image }, }); }); app.get('/api/auth/refresh-roles', auth, (req, res) => { const userRow = db.prepare('SELECT id, email, role, first_name, last_name, gender, profile_image, is_active FROM users WHERE id = ? AND is_deleted = 0').get(req.user.id); if (!userRow) return res.status(401).json({ error: 'User not found' }); if (!userRow.is_active) return res.status(403).json({ error: 'Account is inactive' }); const jwt = require('jsonwebtoken'); const token = jwt.sign({ id: userRow.id, email: userRow.email, role: userRow.role }, JWT_SECRET, { expiresIn: '5m' }); res.json({ token, user: { id: userRow.id, email: userRow.email, role: userRow.role, first_name: userRow.first_name, last_name: userRow.last_name, gender: userRow.gender, profile_image: userRow.profile_image, }, }); }); return app; } function makeTempDb() { const tmp = path.join(require('os').tmpdir(), `refresh-roles-${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, gender TEXT CHECK(gender IN ('male', 'female', 'other')), profile_image TEXT, is_active INTEGER DEFAULT 1, is_deleted INTEGER DEFAULT 0 ); `); const hash = bcrypt.hashSync('secret123', 4); db.prepare(`INSERT INTO users (uid, email, password, role, first_name, last_name, gender, profile_image) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) .run('u-test', 'test@school.com', hash, 'teacher', 'Original', 'Name', 'other', '/avatar.jpg'); return { db, tmp }; } describe('GET /api/auth/refresh-roles payload', () => { let app; let token; let db; beforeAll(async () => { const built = makeTempDb(); db = built.db; app = buildAuthRouter(db); const login = await request(app).post('/api/auth/login').send({ email: 'test@school.com', password: 'secret123' }); token = login.body.token; }); afterAll(() => { db.close(); }); it('includes first_name, last_name, and profile_image in the user payload', async () => { const res = await request(app) .get('/api/auth/refresh-roles') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.user).toMatchObject({ first_name: 'Original', last_name: 'Name', gender: 'other', profile_image: '/avatar.jpg', }); }); it('reflects the latest first_name/last_name after a profile update', async () => { db.prepare('UPDATE users SET first_name = ?, last_name = ? WHERE id = 1').run('Renamed', 'Person'); const res = await request(app) .get('/api/auth/refresh-roles') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.user.first_name).toBe('Renamed'); expect(res.body.user.last_name).toBe('Person'); }); it('returns 401 without a token', async () => { const res = await request(app).get('/api/auth/refresh-roles'); expect(res.status).toBe(401); }); });