forked from UKSOURCE/cms.hailearning.edu.vn
202 lines
7.0 KiB
JavaScript
202 lines
7.0 KiB
JavaScript
/**
|
|
* 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);
|