/** * Property-Based Tests for auth guard and routes * Feature: degree-management-refactor * Uses: fast-check + jest + supertest (no MongoDB needed) */ const fc = require('fast-check'); const express = require('express'); const session = require('express-session'); const request = require('supertest'); // Routes const authRoutes = require('../routes/auth'); const adminRoutes = require('../routes/admin'); const indexRoutes = require('../routes/index'); // Middleware const { ensureAuthenticated } = require('../middleware/auth'); jest.setTimeout(120000); // ─── Minimal App Factory ───────────────────────────────────────────────────── /** * Build a minimal Express app that: * - Uses in-memory session (no MongoDB store) * - Mounts /auth, /admin, / routes * - Has a 404 handler */ function buildApp() { const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // In-memory session (no MongoStore needed for auth/route tests) app.use( session({ secret: 'test-secret', resave: false, saveUninitialized: false, }) ); // Stub flash so auth routes don't crash app.use((req, res, next) => { req.flash = () => {}; next(); }); // Stub res.locals used by views/middleware app.use((req, res, next) => { res.locals.user = null; res.locals.currentPath = req.path; res.locals.frontendUrl = ''; next(); }); // Mount routes app.use('/auth', authRoutes); app.use('/admin', adminRoutes); app.use('/', indexRoutes); // 404 handler (mirrors server.js) app.use((req, res) => { res.status(404); if (req.accepts('json')) return res.json({ error: 'Not found' }); res.type('txt').send('Not found'); }); return app; } // ─── Known route prefixes (whitelist) ──────────────────────────────────────── const KNOWN_PREFIXES = ['/api/degree/', '/api/certificate/', '/admin/', '/auth/']; function isKnownRoute(path) { return KNOWN_PREFIXES.some((prefix) => path.startsWith(prefix)); } // ─── Property 1: Route ngoài phạm vi trả về 404 ────────────────────────────── // Feature: degree-management-refactor, Property 1: Route ngoài phạm vi trả về 404 test('Property 1: unknown routes return 404', async () => { // Validates: Requirements 1.3 const app = buildApp(); // Arbitrary: random path segments that don't start with known prefixes const unknownPathArb = fc .array(fc.stringMatching(/^[a-z0-9_-]{1,10}$/), { minLength: 1, maxLength: 4 }) .map((parts) => '/' + parts.join('/')) .filter((path) => !isKnownRoute(path)); await fc.assert( fc.asyncProperty(unknownPathArb, async (path) => { const res = await request(app).get(path).set('Accept', 'application/json'); expect(res.status).toBe(404); }), { numRuns: 100 } ); }, 120000); // ─── Property 11: Authentication guard trên admin routes ───────────────────── // Feature: degree-management-refactor, Property 11: Authentication guard trên admin routes test('Property 11: unauthenticated requests to /admin/* are redirected to /auth/login', async () => { // Validates: Requirements 3.11 const app = buildApp(); // Known admin sub-paths to test const adminSubPathArb = fc .constantFrom( '/admin/dashboard', '/admin/degree', '/admin/degree/create', '/admin/department', '/admin/level', '/admin/audit-logs' ); await fc.assert( fc.asyncProperty(adminSubPathArb, async (path) => { // No session cookie → not authenticated const res = await request(app).get(path); expect(res.status).toBe(302); expect(res.headers.location).toBe('/auth/login'); }), { numRuns: 100 } ); }, 120000); // ─── Property 18: Login, logout, request /admin/* → redirect ───────────────── // Feature: degree-management-refactor, Property 18: Login, logout, request /admin/* → redirect test('Property 18: after logout, requests to /admin/* redirect to /auth/login', async () => { // Validates: Requirements 6.5 const app = buildApp(); const adminSubPathArb = fc.constantFrom( '/admin/dashboard', '/admin/degree', '/admin/department', '/admin/level', '/admin/audit-logs' ); await fc.assert( fc.asyncProperty(adminSubPathArb, async (path) => { // Step 1: Simulate an authenticated session by injecting isAuthenticated // We do this by adding a one-time setup route that sets the session flag const testApp = express(); testApp.use(express.json()); testApp.use(express.urlencoded({ extended: true })); testApp.use( session({ secret: 'test-secret', resave: false, saveUninitialized: false, }) ); testApp.use((req, res, next) => { req.flash = () => {}; next(); }); testApp.use((req, res, next) => { res.locals.user = null; res.locals.currentPath = req.path; res.locals.frontendUrl = ''; next(); }); // Helper route: sets isAuthenticated = true in session testApp.get('/__test_login', (req, res) => { req.session.isAuthenticated = true; req.session.user = { id: 'test', username: 'testuser' }; res.json({ ok: true }); }); // Helper route: destroys session (simulates logout) testApp.get('/__test_logout', (req, res) => { req.session.destroy(() => res.json({ ok: true })); }); testApp.use('/auth', authRoutes); testApp.use('/admin', adminRoutes); testApp.use('/', indexRoutes); testApp.use((req, res) => { res.status(404); if (req.accepts('json')) return res.json({ error: 'Not found' }); res.type('txt').send('Not found'); }); const agent = request.agent(testApp); // Step 2: Login (set session) await agent.get('/__test_login'); // Step 3: Logout (destroy session) await agent.get('/__test_logout'); // Step 4: Request admin route — should redirect to /auth/login const res = await agent.get(path); expect(res.status).toBe(302); expect(res.headers.location).toBe('/auth/login'); }), { numRuns: 100 } ); }, 120000);