/** * Property-Based Tests for Degree search and filter * Feature: degree-management-refactor * Uses: fast-check + mongodb-memory-server + jest */ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); const fc = require('fast-check'); // Models const Degree = require('../models/degree'); const Department = require('../models/department'); const Level = require('../models/level'); let mongod; jest.setTimeout(120000); // ─── Setup / Teardown ──────────────────────────────────────────────────────── beforeAll(async () => { mongod = await MongoMemoryServer.create(); const uri = mongod.getUri(); await mongoose.connect(uri); }); 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, }; } // Arbitraries for safe alphanumeric tokens (no regex special chars) const safeToken = fc .string({ minLength: 2, maxLength: 10 }) .filter(s => /^[a-zA-Z0-9]+$/.test(s)); // ─── Property 9: Kết quả tìm kiếm khớp với query ──────────────────────────── // Feature: degree-management-refactor, Property 9: Kết quả tìm kiếm khớp với query test('Property 9: every search result contains the search term in qualification_number, certification_number, or student_name', async () => { // Validates: Requirements 3.2 await fc.assert( fc.asyncProperty( // Generate 1-5 degrees and a search term fc.array( fc.record({ type: fc.constantFrom('qualification', 'certification'), suffix: safeToken, }), { minLength: 1, maxLength: 5 } ), safeToken, async (degreeSpecs, searchTerm) => { const dept = await createDept(); const lvl = await createLevel(); // Insert degrees; embed searchTerm into some of them for (let i = 0; i < degreeSpecs.length; i++) { const spec = degreeSpecs[i]; const overrides = { type: spec.type }; // Alternate which field gets the search term if (i % 3 === 0) { overrides.qualification_number = `QN-${searchTerm}-${spec.suffix}`; } else if (i % 3 === 1) { overrides.student_name = `${searchTerm} Student ${spec.suffix}`; } else { // certification type with certification_number containing term overrides.type = 'certification'; overrides.certification_number = `CN-${searchTerm}-${spec.suffix}`; } if (overrides.type === 'certification' && !overrides.certification_number) { overrides.certification_number = `CN-${uid()}`; } await Degree.create(makeDegreeData(dept, lvl, overrides)); } // Run the same $or $regex query as degreeController.index const filter = { $or: [ { qualification_number: { $regex: searchTerm, $options: 'i' } }, { certification_number: { $regex: searchTerm, $options: 'i' } }, { student_name: { $regex: searchTerm, $options: 'i' } }, ], }; const results = await Degree.find(filter); // Every result must contain the search term in at least one of the three fields for (const doc of results) { const term = searchTerm.toLowerCase(); const matchesQN = doc.qualification_number ? doc.qualification_number.toLowerCase().includes(term) : false; const matchesCN = doc.certification_number ? doc.certification_number.toLowerCase().includes(term) : false; const matchesSN = doc.student_name ? doc.student_name.toLowerCase().includes(term) : false; expect(matchesQN || matchesCN || matchesSN).toBe(true); } } ), { numRuns: 100 } ); }, 120000); // ─── Property 10: Kết quả lọc khớp với filter ─────────────────────────────── // Feature: degree-management-refactor, Property 10: Kết quả lọc khớp với filter test('Property 10: every filtered result matches the applied filter value exactly', async () => { // Validates: Requirements 3.3 await fc.assert( fc.asyncProperty( // Choose which field to filter on and what value to use fc.record({ filterField: fc.constantFrom('type', 'status'), filterValue: fc.constantFrom('qualification', 'certification', 'active', 'revoked'), }), // Generate 2-6 degrees with mixed types and statuses fc.array( fc.record({ type: fc.constantFrom('qualification', 'certification'), status: fc.constantFrom('active', 'revoked'), }), { minLength: 2, maxLength: 6 } ), async ({ filterField, filterValue }, degreeSpecs) => { // Skip invalid combinations (e.g. filtering type by 'active') const typeValues = ['qualification', 'certification']; const statusValues = ['active', 'revoked']; if (filterField === 'type' && !typeValues.includes(filterValue)) return; if (filterField === 'status' && !statusValues.includes(filterValue)) return; const dept = await createDept(); const lvl = await createLevel(); for (const spec of degreeSpecs) { const overrides = { type: spec.type, status: spec.status }; if (spec.type === 'certification') { overrides.certification_number = `CN-${uid()}`; } await Degree.create(makeDegreeData(dept, lvl, overrides)); } // Apply filter the same way degreeController.index does const filter = { [filterField]: filterValue }; const results = await Degree.find(filter); // Every result must match the filter value exactly for (const doc of results) { expect(doc[filterField]).toBe(filterValue); } } ), { numRuns: 100 } ); }, 120000);