forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
325
tests/degree.property.test.js
Normal file
325
tests/degree.property.test.js
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Property-Based Tests for Degree model
|
||||
* 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;
|
||||
|
||||
// Increase global timeout for slow MongoDB startup and PBT runs
|
||||
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'),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Property 2: Validation các trường bắt buộc của Degree ──────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 2: Validation các trường bắt buộc của Degree
|
||||
test('Property 2: saving Degree without each required field throws ValidationError', async () => {
|
||||
// Validates: Requirements 2.1
|
||||
const requiredFields = [
|
||||
'qualification_number',
|
||||
'student_name',
|
||||
'program_name',
|
||||
'type',
|
||||
'department',
|
||||
'level',
|
||||
'issued_date',
|
||||
];
|
||||
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.constantFrom(...requiredFields),
|
||||
async (missingField) => {
|
||||
const dept = await createDept();
|
||||
const lvl = await createLevel();
|
||||
const data = makeDegreeData(dept, lvl);
|
||||
delete data[missingField];
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
const doc = new Degree(data);
|
||||
await doc.save();
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
// Should be a ValidationError (or pre-save Error for type-related)
|
||||
const isValidation =
|
||||
err.name === 'ValidationError' ||
|
||||
(err.message && err.message.includes('required'));
|
||||
expect(isValidation).toBe(true);
|
||||
}
|
||||
expect(threw).toBe(true);
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
// ─── Property 3: Conditional validation type-number ─────────────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 3: Conditional validation type-number
|
||||
test('Property 3: certification type without certification_number throws; qualification saves ok', async () => {
|
||||
// Validates: Requirements 2.3, 2.4
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.boolean(), // true = certification (should fail), false = qualification (should pass)
|
||||
async (isCertification) => {
|
||||
const dept = await createDept();
|
||||
const lvl = await createLevel();
|
||||
|
||||
if (isCertification) {
|
||||
// certification without certification_number → must throw
|
||||
const data = makeDegreeData(dept, lvl, { type: 'certification' });
|
||||
// no certification_number
|
||||
let threw = false;
|
||||
try {
|
||||
const doc = new Degree(data);
|
||||
await doc.save();
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err.message).toMatch(/certification_number/i);
|
||||
}
|
||||
expect(threw).toBe(true);
|
||||
} else {
|
||||
// qualification without certification_number → must succeed
|
||||
const data = makeDegreeData(dept, lvl, { type: 'qualification' });
|
||||
const doc = new Degree(data);
|
||||
const saved = await doc.save();
|
||||
expect(saved._id).toBeDefined();
|
||||
await Degree.deleteOne({ _id: saved._id });
|
||||
}
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
// ─── Property 4: Uniqueness của qualification_number ────────────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 4: Uniqueness của qualification_number
|
||||
test('Property 4: two Degrees with same qualification_number → second save throws duplicate key error', async () => {
|
||||
// Validates: Requirements 2.5, 2.7
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
async (qn) => {
|
||||
const dept = await createDept();
|
||||
const lvl = await createLevel();
|
||||
|
||||
const data1 = makeDegreeData(dept, lvl, { qualification_number: `QN-${qn}` });
|
||||
const data2 = makeDegreeData(dept, lvl, { qualification_number: `QN-${qn}` });
|
||||
|
||||
await Degree.create(data1);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await Degree.create(data2);
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
// MongoServerError code 11000 = duplicate key
|
||||
expect(err.code === 11000 || err.name === 'MongoServerError').toBe(true);
|
||||
}
|
||||
expect(threw).toBe(true);
|
||||
|
||||
// cleanup
|
||||
await Degree.deleteMany({ qualification_number: `QN-${qn}` });
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
// ─── Property 5: Sparse uniqueness của certification_number ─────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 5: Sparse uniqueness của certification_number
|
||||
test('Property 5: same non-null certification_number → duplicate error; both null → both save ok', async () => {
|
||||
// Validates: Requirements 2.6, 2.7
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.boolean(), // true = test duplicate, false = test sparse (both null)
|
||||
async (testDuplicate) => {
|
||||
const dept = await createDept();
|
||||
const lvl = await createLevel();
|
||||
|
||||
if (testDuplicate) {
|
||||
const cn = `CN-${uid()}`;
|
||||
const data1 = makeDegreeData(dept, lvl, {
|
||||
type: 'certification',
|
||||
certification_number: cn,
|
||||
});
|
||||
const data2 = makeDegreeData(dept, lvl, {
|
||||
type: 'certification',
|
||||
certification_number: cn,
|
||||
});
|
||||
|
||||
await Degree.create(data1);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await Degree.create(data2);
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err.code === 11000 || err.name === 'MongoServerError').toBe(true);
|
||||
}
|
||||
expect(threw).toBe(true);
|
||||
|
||||
await Degree.deleteMany({ certification_number: cn });
|
||||
} else {
|
||||
// Both without certification_number (sparse index allows multiple nulls)
|
||||
const data1 = makeDegreeData(dept, lvl, { type: 'qualification' });
|
||||
const data2 = makeDegreeData(dept, lvl, { type: 'qualification' });
|
||||
|
||||
const saved1 = await Degree.create(data1);
|
||||
const saved2 = await Degree.create(data2);
|
||||
|
||||
expect(saved1._id).toBeDefined();
|
||||
expect(saved2._id).toBeDefined();
|
||||
|
||||
await Degree.deleteMany({ _id: { $in: [saved1._id, saved2._id] } });
|
||||
}
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
// ─── Property 6: Round-trip tạo Degree ──────────────────────────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 6: Round-trip tạo Degree
|
||||
test('Property 6: save Degree then findById returns matching fields', async () => {
|
||||
// Validates: Requirements 3.5
|
||||
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 found = await Degree.findById(saved._id);
|
||||
|
||||
expect(found).not.toBeNull();
|
||||
expect(found.qualification_number).toBe(data.qualification_number);
|
||||
expect(found.student_name).toBe(student_name);
|
||||
expect(found.program_name).toBe(program_name);
|
||||
expect(found.type).toBe(data.type);
|
||||
expect(found.department.toString()).toBe(dept._id.toString());
|
||||
expect(found.level.toString()).toBe(lvl._id.toString());
|
||||
|
||||
await Degree.deleteOne({ _id: saved._id });
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
// ─── Property 7: Round-trip cập nhật Degree ─────────────────────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 7: Round-trip cập nhật Degree
|
||||
test('Property 7: update student_name then findById returns updated value', async () => {
|
||||
// Validates: Requirements 3.8
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim() === s && s.trim().length > 0),
|
||||
async (newName) => {
|
||||
const dept = await createDept();
|
||||
const lvl = await createLevel();
|
||||
const data = makeDegreeData(dept, lvl);
|
||||
|
||||
const saved = await Degree.create(data);
|
||||
await Degree.findByIdAndUpdate(saved._id, { student_name: newName });
|
||||
const found = await Degree.findById(saved._id);
|
||||
|
||||
expect(found).not.toBeNull();
|
||||
expect(found.student_name).toBe(newName);
|
||||
|
||||
await Degree.deleteOne({ _id: saved._id });
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
// ─── Property 8: Round-trip xóa Degree ──────────────────────────────────────
|
||||
|
||||
// Feature: degree-management-refactor, Property 8: Round-trip xóa Degree
|
||||
test('Property 8: delete Degree then findById returns null', async () => {
|
||||
// Validates: Requirements 3.9
|
||||
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);
|
||||
|
||||
const saved = await Degree.create(data);
|
||||
await Degree.findByIdAndDelete(saved._id);
|
||||
const found = await Degree.findById(saved._id);
|
||||
|
||||
expect(found).toBeNull();
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
}, 120000);
|
||||
Reference in New Issue
Block a user