forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
212
tests/auth-routes.property.test.js
Normal file
212
tests/auth-routes.property.test.js
Normal 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);
|
||||
Reference in New Issue
Block a user