145 lines
5.3 KiB
JavaScript
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);
|
|
});
|
|
}); |