/** * 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);