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