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

179
tests/api.property.test.js Normal file
View File

@@ -0,0 +1,179 @@
/**
* Property-Based Tests for API endpoints
* Feature: degree-management-refactor
* Uses: fast-check + mongodb-memory-server + jest + supertest
*/
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const fc = require('fast-check');
const express = require('express');
const request = require('supertest');
// Models
const Degree = require('../models/degree');
const Department = require('../models/department');
const Level = require('../models/level');
// Routes
const indexRoutes = require('../routes/index');
let mongod;
let app;
// Increase global timeout for slow MongoDB startup and PBT runs
jest.setTimeout(120000);
// ─── Setup / Teardown ────────────────────────────────────────────────────────
beforeAll(async () => {
// Set test API key before anything else
process.env.API_KEY = 'test-api-key-12345';
mongod = await MongoMemoryServer.create();
const uri = mongod.getUri();
await mongoose.connect(uri);
// Create a minimal Express app that mounts routes/index.js only
app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/', indexRoutes);
});
afterAll(async () => {
await mongoose.disconnect();
await mongod.stop();
});
afterEach(async () => {
await Degree.deleteMany({});
await Department.deleteMany({});
await Level.deleteMany({});
});
// ─── Seed Helpers ────────────────────────────────────────────────────────────
let _counter = 0;
function uid() {
return `${Date.now()}_${++_counter}_${Math.random().toString(36).slice(2)}`;
}
async function createDept() {
const id = uid();
return Department.create({ name: `dept_${id}`, slug: `dept-${id}` });
}
async function createLevel() {
const id = uid();
return Level.create({ type: `level_${id}` });
}
function makeDegreeData(dept, level, overrides = {}) {
const id = uid();
return {
qualification_number: `QN-${id}`,
student_name: `Student ${id}`,
program_name: `Program ${id}`,
type: 'qualification',
department: dept._id,
level: level._id,
issued_date: new Date('2024-01-01'),
status: 'active',
...overrides,
};
}
// ─── Property 15: API lookup trả về đúng thông tin Degree ───────────────────
// Feature: degree-management-refactor, Property 15: API lookup trả về đúng thông tin Degree
test('Property 15: GET /api/degree/:qualificationNumber returns correct Degree fields', async () => {
// Validates: Requirements 5.1, 5.2
await fc.assert(
fc.asyncProperty(
fc.record({
student_name: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim() === s && s.trim().length > 0),
program_name: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim() === s && s.trim().length > 0),
}),
async ({ student_name, program_name }) => {
const dept = await createDept();
const lvl = await createLevel();
const data = makeDegreeData(dept, lvl, { student_name, program_name });
const saved = await Degree.create(data);
const res = await request(app)
.get(`/api/degree/${saved.qualification_number}`)
.set('x-api-key', process.env.API_KEY);
expect(res.status).toBe(200);
expect(res.body.student_name).toBe(student_name);
expect(res.body.program_name).toBe(program_name);
expect(res.body.type).toBe(data.type);
expect(res.body.status).toBe(data.status);
expect(res.body.issued_date).toBeDefined();
}
),
{ numRuns: 100 }
);
}, 120000);
// ─── Property 16: API không có API key hợp lệ trả về 401 ────────────────────
// Feature: degree-management-refactor, Property 16: API không có API key hợp lệ trả về 401
test('Property 16: requests without valid x-api-key return 401', async () => {
// Validates: Requirements 5.3, 5.6
await fc.assert(
fc.asyncProperty(
fc.oneof(
fc.constant(null), // no header at all
fc.string({ minLength: 1, maxLength: 40 }) // random invalid key
.filter(s => s !== process.env.API_KEY)
),
async (invalidKey) => {
const dept = await createDept();
const lvl = await createLevel();
const data = makeDegreeData(dept, lvl);
await Degree.create(data);
let req = request(app).get(`/api/degree/${data.qualification_number}`);
if (invalidKey !== null) {
req = req.set('x-api-key', invalidKey);
}
const res = await req;
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'Unauthorized - Invalid API key' });
}
),
{ numRuns: 100 }
);
}, 120000);
// ─── Property 17: Degree bị thu hồi vẫn trả về với status revoked ───────────
// Feature: degree-management-refactor, Property 17: Degree bị thu hồi vẫn trả về với status revoked
test('Property 17: revoked Degree is returned by API with status "revoked" (not 404)', async () => {
// Validates: Requirements 5.5
await fc.assert(
fc.asyncProperty(
fc.constant(null), // no extra input needed
async () => {
const dept = await createDept();
const lvl = await createLevel();
const data = makeDegreeData(dept, lvl, { status: 'revoked' });
const saved = await Degree.create(data);
const res = await request(app)
.get(`/api/degree/${saved.qualification_number}`)
.set('x-api-key', process.env.API_KEY);
expect(res.status).toBe(200);
expect(res.body.status).toBe('revoked');
}
),
{ numRuns: 100 }
);
}, 120000);