/** * Test setup — creates a temp SQLite DB, runs migrations, boots Express app. * Usage: const { app, db, getToken } = require('./setup'); */ const path = require('path'); const os = require('os'); const fs = require('fs'); // Unique temp DB per test run const tmpDb = path.join(os.tmpdir(), `ls_test_${Date.now()}_${Math.random().toString(36).slice(2)}.db`); process.env.DB_PATH = tmpDb; process.env.JWT_SECRET = 'test_secret_that_is_at_least_32_characters_long!!'; process.env.JWT_EXPIRES_IN = '1h'; process.env.NODE_ENV = 'test'; // Now require app modules (they will use the temp DB) const db = require('../src/db/db'); require('../src/db/migrations-runner').run(); // Seed permissions const { seedDefaults } = require('../src/controllers/permissionsController'); seedDefaults(); // Seed achievements const { seedAchievements } = require('../src/controllers/gamificationController'); seedAchievements(); // Build Express app (without listen) const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json({ limit: '1mb' })); app.use('/api/auth', require('../src/routes/auth')); app.use('/api/sessions', require('../src/routes/sessions')); app.use('/api/shop', require('../src/routes/shop')); app.use('/api/classes', require('../src/routes/classes')); app.use('/api/gamification', require('../src/routes/gamification')); app.use('/api/admin', require('../src/routes/admin')); app.use('/api/subjects', require('../src/routes/subjects')); app.use('/api/questions', require('../src/routes/questions')); // Additional routes for integration tests app.use('/api/permissions', require('../src/routes/permissions')); app.use('/api/access', require('../src/routes/access')); app.use('/api/lab', require('../src/routes/lab')); app.use('/api/courses', require('../src/routes/courses')); app.use('/api/roles', require('../src/routes/roles')); // Feature-gated routes (requireFeature checks app_settings in DB) const { requireFeature } = require('../src/middleware/features'); app.use('/api/pet', requireFeature('pet'), require('../src/routes/pet')); app.use('/api/biochem', requireFeature('biochem'), require('../src/routes/biochem')); // Error handler app.use((err, _req, res, _next) => { res.status(err.status || 500).json({ error: err.message || 'Server error' }); }); /* ── Helper: register + get token ─────────────────────────────────────── */ let _userCounter = 0; async function getToken(role = 'student', nameOverride) { const n = ++_userCounter; const email = `test${n}@example.com`; const name = nameOverride || `Test User ${n}`; const password = 'password123'; // Register via API const res = await inject('POST', '/api/auth/register', { email, password, name }); const token = res.body.token; const userId = res.body.user.id; // If role is not student, set it directly in DB if (role !== 'student') { db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, userId); } return { token, userId, email, name }; } /* ── HTTP injection (no real TCP) ─────────────────────────────────────── */ const http = require('http'); let _server; function ensureServer() { if (!_server) { _server = http.createServer(app); _server.listen(0); // random port } return _server; } async function inject(method, path, body, token) { const server = ensureServer(); const port = server.address().port; return new Promise((resolve, reject) => { const data = body ? JSON.stringify(body) : null; const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; if (data) headers['Content-Length'] = Buffer.byteLength(data); const req = http.request({ hostname: '127.0.0.1', port, path, method, headers }, (res) => { let chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => { const raw = Buffer.concat(chunks).toString(); let parsed; try { parsed = JSON.parse(raw); } catch { parsed = raw; } resolve({ status: res.statusCode, body: parsed, headers: res.headers }); }); }); req.on('error', reject); if (data) req.write(data); req.end(); }); } /* ── Cleanup ──────────────────────────────────────────────────────────── */ function cleanup() { if (_server) _server.close(); try { fs.unlinkSync(tmpDb); } catch {} } module.exports = { app, db, getToken, inject, cleanup };