修正权限
This commit is contained in:
+82
-4
@@ -37,7 +37,7 @@ assert.doesNotMatch(mysqlAdapterSource, /ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS/i, 'My
|
||||
assert.match(mysqlAdapterSource, /for \(const statement of mysqlSchema\) await pool\.query\(statement\)/, 'MySQL DDL 应使用文本协议执行');
|
||||
assert.match(mysqlAdapterSource, /existingResultLockTriggers\.has\(name\)\) await pool\.query\(statement\)/, 'MySQL 触发器不得通过预处理协议创建');
|
||||
assert.doesNotMatch(mysqlAdapterSource, /\.execute\(\s*['"`]\s*(?:CREATE|ALTER|DROP|SHOW)\b/i, 'MySQL DDL 和 SHOW 语句不得通过预处理协议执行');
|
||||
assert.match(mysqlAdapterSource, /existingAppTables\.length && \(!hasSchemaMetadata \|\| existingSchemaVersion !== 15\)/, 'MySQL 应重建未完成或过期的开发结构');
|
||||
assert.match(mysqlAdapterSource, /existingAppTables\.length && \(!hasSchemaMetadata \|\| !\[15, 16\]\.includes\(existingSchemaVersion\)\)/, 'MySQL 应保留可迁移的 v15 结构并重建更旧或未完成的开发结构');
|
||||
assert.match(mysqlAdapterSource, /\[\.\.\.mysqlTableNames\]\.reverse\(\)/, 'MySQL 半成品表应按外键依赖逆序清理');
|
||||
const serverSource = await readFile(resolve(root, 'server.mjs'), 'utf8');
|
||||
assert.doesNotMatch(serverSource, /src\/data\/seed\.mjs|createSeedDatabase/, '服务启动不得引用测试数据生成器');
|
||||
@@ -177,6 +177,7 @@ try {
|
||||
const userColumns = inspector.prepare('PRAGMA table_info(users)').all().map(row => row.name);
|
||||
const registrationColumns = inspector.prepare('PRAGMA table_info(registrations)').all().map(row => row.name);
|
||||
const seededSchoolCount = inspector.prepare('SELECT COUNT(*) AS count FROM schools').get().count;
|
||||
const seededExamCount = inspector.prepare('SELECT COUNT(*) AS count FROM exams').get().count;
|
||||
const seededCandidateCount = inspector.prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'candidate'").get().count;
|
||||
const seededRegistrationCounts = inspector.prepare(`
|
||||
SELECT status, payment_status, COUNT(*) AS count
|
||||
@@ -185,10 +186,46 @@ try {
|
||||
`).all();
|
||||
const seededArrangementCount = inspector.prepare('SELECT COUNT(*) AS count FROM exam_arrangement_plans').get().count;
|
||||
const seededAdmitCardCount = inspector.prepare('SELECT COUNT(*) AS count FROM admit_cards').get().count;
|
||||
const examPartitions = inspector.prepare('SELECT * FROM exam_data_partitions ORDER BY exam_id').all();
|
||||
const schoolPartitions = inspector.prepare('SELECT * FROM school_student_partitions ORDER BY school_id').all();
|
||||
const examPartitionCoverage = examPartitions.map(partition => {
|
||||
for (const name of [partition.candidates_table, partition.admissions_table, partition.results_table, partition.centers_table]) {
|
||||
assert.match(name, /^[a-z][a-z0-9_]{0,63}$/, '考试分表名必须是安全的数据库标识符');
|
||||
assert.ok(tableNames.includes(name), `考试专属表 ${name} 必须真实存在`);
|
||||
}
|
||||
return {
|
||||
examId: partition.exam_id,
|
||||
candidates: inspector.prepare(`SELECT COUNT(*) AS count FROM "${partition.candidates_table}"`).get().count,
|
||||
expectedCandidates: inspector.prepare('SELECT COUNT(*) AS count FROM registrations WHERE exam_id = ?').get(partition.exam_id).count,
|
||||
admissions: inspector.prepare(`SELECT COUNT(*) AS count FROM "${partition.admissions_table}"`).get().count,
|
||||
expectedAdmissions: inspector.prepare(`SELECT COUNT(*) AS count FROM registration_subjects selected
|
||||
JOIN registrations registration ON registration.id = selected.registration_id WHERE registration.exam_id = ?`).get(partition.exam_id).count,
|
||||
results: inspector.prepare(`SELECT COUNT(*) AS count FROM "${partition.results_table}"`).get().count,
|
||||
expectedResults: inspector.prepare(`SELECT COUNT(*) AS count FROM results result
|
||||
JOIN registrations registration ON registration.id = result.registration_id WHERE registration.exam_id = ?`).get(partition.exam_id).count
|
||||
};
|
||||
});
|
||||
const schoolPartitionCoverage = schoolPartitions.map(partition => {
|
||||
assert.match(partition.students_table, /^[a-z][a-z0-9_]{0,63}$/, '学校学生分表名必须是安全的数据库标识符');
|
||||
assert.ok(tableNames.includes(partition.students_table), `学校学生专属表 ${partition.students_table} 必须真实存在`);
|
||||
return {
|
||||
schoolId: partition.school_id,
|
||||
students: inspector.prepare(`SELECT COUNT(*) AS count FROM "${partition.students_table}"`).get().count,
|
||||
expectedStudents: inspector.prepare(`SELECT COUNT(*) AS count FROM users user
|
||||
LEFT JOIN candidate_profiles profile ON profile.user_id = user.id
|
||||
WHERE user.role = 'candidate' AND COALESCE(profile.school_id, user.school_id) = ?`).get(partition.school_id).count
|
||||
};
|
||||
});
|
||||
inspector.close();
|
||||
assert.deepEqual(tableNames, [...relationalTables].sort(), '业务数据必须按关系模型分表存储');
|
||||
assert.ok(relationalTables.every(table => tableNames.includes(table)), '所有关系模型总表与分表登记表都必须存在');
|
||||
assert.ok(!tableNames.includes('app_state'), '不得使用单表 JSON 状态存储');
|
||||
assert.equal(schemaVersion, 15, '线下缴费确认应使用 v15 数据结构');
|
||||
assert.equal(schemaVersion, 16, '考试与学校物理分表应使用 v16 数据结构');
|
||||
assert.ok(examPartitions.length > 0, '每场考试都应登记一组专属物理表');
|
||||
assert.equal(examPartitions.length, seededExamCount, '考试分表登记不得缺漏');
|
||||
assert.ok(examPartitionCoverage.every(item => item.candidates === item.expectedCandidates && item.admissions === item.expectedAdmissions && item.results === item.expectedResults), '考试专属表应与该场考试的考生、准考信息和成绩数据一致');
|
||||
assert.ok(schoolPartitions.length > 0, '每所学校都应登记学生专属物理表');
|
||||
assert.equal(schoolPartitions.length, seededSchoolCount, '学校学生分表登记不得缺漏');
|
||||
assert.ok(schoolPartitionCoverage.every(item => item.students === item.expectedStudents), '学校学生专属表应仅保存本校学生且数量一致');
|
||||
assert.ok(['archived_at', 'archived_by'].every(column => examColumns.includes(column)), '考试应保存不可逆归档时间和超级管理员');
|
||||
assert.equal(resultLockTriggers.length, 3, '数据库应从插入、更新、删除三个方向永久锁定归档成绩');
|
||||
assert.ok(['archived_at', 'archived_by'].every(column => userColumns.includes(column)), '账户应保存独立归档状态和校方操作人');
|
||||
@@ -273,6 +310,21 @@ try {
|
||||
assert.match(selfRegister.data.registrationNumber, /^2026-HZ03-F-\d{4}$/);
|
||||
await admin.request('/api/admin/settings/self-registration', { method: 'PUT', body: { enabled: false } });
|
||||
|
||||
const schoolDirectory = await admin.request('/api/admin/schools');
|
||||
assert.equal(schoolDirectory.response.status, 200, '超级管理员应有学校管理入口');
|
||||
assert.ok(schoolDirectory.data.schools.every(item => Number.isInteger(item.classCount) && Number.isInteger(item.adminCount)), '学校目录应汇总班级和管理员数量');
|
||||
assert.equal((await schoolAdmin.request('/api/admin/schools')).response.status, 403, '校级管理员不得跨校维护学校档案');
|
||||
const createSchool = await admin.request('/api/admin/schools', { method: 'POST', body: { name: '海州市第四中学', code: 'hz04', address: '海州市测试区学校路 4 号', active: true } });
|
||||
assert.equal(createSchool.response.status, 201, '超级管理员应能创建学校');
|
||||
assert.equal(createSchool.data.school.code, 'HZ04', '学校代码应规范化为大写');
|
||||
const createdSchoolId = createSchool.data.school.id;
|
||||
assert.equal((await admin.request('/api/admin/schools', { method: 'POST', body: { name: '重复代码学校', code: 'HZ04' } })).response.status, 409, '学校代码必须唯一');
|
||||
const disableSchool = await admin.request(`/api/admin/schools/${createdSchoolId}`, { method: 'PATCH', body: { name: '海州市第四实验中学', active: false } });
|
||||
assert.equal(disableSchool.response.status, 200, '超级管理员应能编辑和停用学校');
|
||||
assert.equal(disableSchool.data.school.active, false);
|
||||
assert.equal(disableSchool.data.school.name, '海州市第四实验中学');
|
||||
assert.equal((await schoolAdmin.request(`/api/admin/schools/${createdSchoolId}`, { method: 'PATCH', body: { active: true } })).response.status, 403, '校级管理员不得启停学校');
|
||||
|
||||
const adminDirectory = await admin.request('/api/admin/admins');
|
||||
assert.ok(adminDirectory.data.admins.filter(item => item.adminLevel === 'school' && item.schoolId === 'school_hz1').length >= 2, '同一学校应支持多个同级管理员');
|
||||
const schoolCenters = await schoolAdmin.request('/api/admin/centers');
|
||||
@@ -450,6 +502,14 @@ try {
|
||||
assert.equal(createExam.data.exam.subjects[2].passRule, 'rank_percent', '每科应可独立按排名比例计算及格线');
|
||||
assert.equal(createExam.data.exam.subjects[2].passScore, null, '排名比例不应伪造固定分数线');
|
||||
const exam = createExam.data.exam;
|
||||
const createdExamInspector = new DatabaseSync(testDb, { readOnly: true });
|
||||
const createdExamPartition = createdExamInspector.prepare('SELECT * FROM exam_data_partitions WHERE exam_id = ?').get(exam.id);
|
||||
assert.ok(createdExamPartition, '创建考试时应同步登记该场考试的专属物理表');
|
||||
for (const table of [createdExamPartition.candidates_table, createdExamPartition.admissions_table, createdExamPartition.results_table, createdExamPartition.centers_table]) {
|
||||
assert.match(table, /^[a-z][a-z0-9_]{0,63}$/);
|
||||
assert.ok(createdExamInspector.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table), `新考试专属表 ${table} 应立即创建`);
|
||||
}
|
||||
createdExamInspector.close();
|
||||
const schoolCannotCreateExam = await schoolAdmin.request('/api/admin/exams', { method: 'POST', body: { name: '越权考试' } });
|
||||
assert.equal(schoolCannotCreateExam.response.status, 403, '校级管理员不得管理全局考试计划');
|
||||
|
||||
@@ -742,6 +802,24 @@ try {
|
||||
assert.ok(classResults.data.results.some(item => item.score === 126 && item.candidateName === '测试考生新名'), '班级管理员应可查看本班成绩');
|
||||
assert.equal((await classAdmin.request('/api/admin/results', { method: 'POST', body: { registrationId, subjectId: exam.subjects[0].id, score: 1 } })).response.status, 403, '班级管理员不得录入或发布成绩');
|
||||
|
||||
const livePartitionInspector = new DatabaseSync(testDb, { readOnly: true });
|
||||
const liveExamPartition = livePartitionInspector.prepare('SELECT * FROM exam_data_partitions WHERE exam_id = ?').get(exam.id);
|
||||
const partitionCandidate = livePartitionInspector.prepare(`SELECT * FROM "${liveExamPartition.candidates_table}" WHERE registration_id = ?`).get(registrationId);
|
||||
const partitionAdmissions = livePartitionInspector.prepare(`SELECT * FROM "${liveExamPartition.admissions_table}" WHERE registration_id = ? ORDER BY subject_id`).all(registrationId);
|
||||
const partitionResults = livePartitionInspector.prepare(`SELECT * FROM "${liveExamPartition.results_table}" WHERE registration_id = ? ORDER BY subject_id`).all(registrationId);
|
||||
const partitionCenters = livePartitionInspector.prepare(`SELECT * FROM "${liveExamPartition.centers_table}"`).all();
|
||||
assert.equal(partitionCandidate.candidate_number, candidateNumber, '考试考生专属表应保存该场报名考生');
|
||||
assert.equal(partitionCandidate.payment_status, 'paid', '考试考生专属表应同步报名与缴费状态');
|
||||
assert.equal(partitionAdmissions.length, 2, '考试准考证专属表应按报考科目保存考号和座位');
|
||||
assert.ok(partitionAdmissions.every(item => item.admission_number === generatedCard.number && item.seat), '考试准考证专属表应同步准考证号、考场和座位');
|
||||
assert.equal(partitionResults.length, 2, '考试成绩专属表应只保存该场考试成绩');
|
||||
assert.ok(partitionResults.every(item => item.published === 1), '考试成绩专属表应同步发布状态');
|
||||
assert.ok(partitionCenters.some(item => item.center_code === generatedCard.centerCode), '考试考点专属表应保存本场实际使用的考点');
|
||||
const schoolStudentPartition = livePartitionInspector.prepare('SELECT students_table FROM school_student_partitions WHERE school_id = ?').get('school_hz1');
|
||||
const partitionStudent = livePartitionInspector.prepare(`SELECT * FROM "${schoolStudentPartition.students_table}" WHERE candidate_number = ?`).get(candidateNumber);
|
||||
assert.equal(partitionStudent.candidate_name, '测试考生新名', '学校学生专属表应保存且同步本校学生资料');
|
||||
livePartitionInspector.close();
|
||||
|
||||
const appealResults = results.data.results.filter(item => [exam.subjects[0].id, exam.subjects[2].id].includes(item.subjectId));
|
||||
assert.equal(appealResults.length, 2, '测试考生应有两科可提交成绩复议');
|
||||
const submittedAppeals = [];
|
||||
@@ -821,7 +899,7 @@ try {
|
||||
assert.equal(updateWorkflow.response.status, 200, '超级管理员应可设计考生信息修改审批流程');
|
||||
|
||||
console.log('✓ 公开首页与通知读取');
|
||||
console.log(`✓ SQLite 关系型数据库初始化(${relationalTables.length} 张分表)`);
|
||||
console.log(`✓ SQLite 关系型数据库初始化(${relationalTables.length} 张核心表 + 按考试/学校动态专属分表)`);
|
||||
console.log('✓ 超级、校级、班级管理员的数据范围与权限隔离');
|
||||
console.log('✓ 固定报名号账户、首次强制改密、完整资料与注册开关');
|
||||
console.log('✓ 多科目考试创建与考生自主选科报名');
|
||||
|
||||
Reference in New Issue
Block a user