forked from UKSOURCE/cms.hailearning.edu.vn
213 lines
6.4 KiB
JavaScript
213 lines
6.4 KiB
JavaScript
/**
|
|
* 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);
|