geocrop-platform./apps/nextgen/server/tests/refresh-roles.test.js

145 lines
5.3 KiB
JavaScript

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