forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
201
tests/search-filter.property.test.js
Normal file
201
tests/search-filter.property.test.js
Normal 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);
|
||||
Reference in New Issue
Block a user