first commit

This commit is contained in:
2026-04-11 14:08:27 +07:00
parent e86e5d2c46
commit 6b7655aa16
389 changed files with 5387 additions and 60861 deletions

View File

@@ -0,0 +1,212 @@
/**
* 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);