Add candidate account archiving and password reset
This commit is contained in:
+28
-1
@@ -95,10 +95,12 @@ try {
|
||||
const admitCardColumns = inspector.prepare('PRAGMA table_info(admit_cards)').all().map(row => row.name);
|
||||
const admitSubjectColumns = inspector.prepare('PRAGMA table_info(admit_card_subjects)').all().map(row => row.name);
|
||||
const examSubjectColumns = inspector.prepare('PRAGMA table_info(exam_subjects)').all().map(row => row.name);
|
||||
const userColumns = inspector.prepare('PRAGMA table_info(users)').all().map(row => row.name);
|
||||
inspector.close();
|
||||
assert.deepEqual(tableNames, [...relationalTables].sort(), '业务数据必须按关系模型分表存储');
|
||||
assert.ok(!tableNames.includes('app_state'), '不得使用单表 JSON 状态存储');
|
||||
assert.equal(schemaVersion, 12, '排名比例、排名等级与复议改分应使用 v12 数据结构');
|
||||
assert.equal(schemaVersion, 13, '账户归档应使用 v13 数据结构');
|
||||
assert.ok(['archived_at', 'archived_by'].every(column => userColumns.includes(column)), '账户应保存独立归档状态和校方操作人');
|
||||
assert.ok(['center_code', 'center_address'].every(column => admitCardColumns.includes(column)), '准考证应保存考点代码与详细地址快照');
|
||||
assert.ok(['building', 'floor'].every(column => admitSubjectColumns.includes(column)), '分科考场应保存楼栋楼层快照');
|
||||
assert.ok(['pass_rule', 'pass_value'].every(column => examSubjectColumns.includes(column)), '科目应保存独立及格线计算方式和规则数值');
|
||||
@@ -122,6 +124,8 @@ try {
|
||||
assert.equal((await schoolAdmin2.request('/api/auth/login', { method: 'POST', body: { username: 'school_admin_2', password: 'School123!' } })).data.user.adminLevel, 'school');
|
||||
assert.equal((await classAdmin.request('/api/auth/login', { method: 'POST', body: { username: 'class_admin', password: 'Class123!' } })).data.user.adminLevel, 'class');
|
||||
assert.equal((await classAdmin2.request('/api/auth/login', { method: 'POST', body: { username: 'class_admin_2', password: 'Class123!' } })).data.user.adminLevel, 'class');
|
||||
assert.equal((await classAdmin2.request('/api/auth/change-password', { method: 'POST', body: { currentPassword: 'Class123!', newPassword: 'ClassChanged123!' } })).response.status, 200, '管理员登录后应可修改自己的密码');
|
||||
assert.equal((await createClient().request('/api/auth/login', { method: 'POST', body: { username: 'class_admin_2', password: 'Class123!' } })).response.status, 401, '管理员改密后旧密码应立即失效');
|
||||
|
||||
assert.equal((await admin.request('/api/admin/candidate-accounts', { method: 'POST', body: {} })).response.status, 404, '超级管理员不得再从旧入口直接生成报名号');
|
||||
const singleAccountBatch = await schoolAdmin.request('/api/admin/candidate-account-batches', { method: 'POST', body: { quotas: [
|
||||
@@ -284,6 +288,29 @@ try {
|
||||
const firstIssued = returnedBatch.items[0];
|
||||
const batchLogin = await batchCandidate.request('/api/auth/login', { method: 'POST', body: { username: firstIssued.candidateNumber, password: firstIssued.initialPassword } });
|
||||
assert.equal(batchLogin.data.user.mustChangePassword, true, '批量下发账户首次登录也必须强制修改密码');
|
||||
const archiveTarget = returnedBatch.items.find(item => item.classId === 'class_hz1_301');
|
||||
const archiveCandidate = createClient();
|
||||
assert.equal((await archiveCandidate.request('/api/auth/login', { method: 'POST', body: { username: archiveTarget.candidateNumber, password: archiveTarget.initialPassword } })).response.status, 200);
|
||||
assert.equal((await admin.request('/api/admin/candidate-accounts/archive', { method: 'POST', body: { scopeType: 'class', scopeValue: 'class_hz1_301', archived: true } })).response.status, 403, '超级管理员不得代替学校归档考生账户');
|
||||
assert.equal((await classAdmin.request('/api/admin/candidate-accounts/archive', { method: 'POST', body: { scopeType: 'class', scopeValue: 'class_hz1_301', archived: true } })).response.status, 403, '班级管理员不得批量归档考生账户');
|
||||
const archivedAccounts = await schoolAdmin.request('/api/admin/candidate-accounts/archive', { method: 'POST', body: { scopeType: 'class', scopeValue: 'class_hz1_301', archived: true } });
|
||||
assert.equal(archivedAccounts.response.status, 200, '校级管理员应可按班级归档考生账户');
|
||||
assert.ok(archivedAccounts.data.count >= 2, '按班级归档应批量更新该班考生');
|
||||
const archivedDirectory = await schoolAdmin.request('/api/admin/candidates');
|
||||
const archivedProfile = archivedDirectory.data.candidates.find(item => item.candidateNumber === archiveTarget.candidateNumber);
|
||||
assert.equal(archivedProfile.accountArchived, true, '考生目录应显示归档状态');
|
||||
assert.equal((await archiveCandidate.request('/api/auth/me')).data.user, null, '归档应立即使考生现有会话失效');
|
||||
assert.equal((await createClient().request('/api/auth/login', { method: 'POST', body: { username: archiveTarget.candidateNumber, password: archiveTarget.initialPassword } })).response.status, 401, '归档账户不得登录');
|
||||
assert.equal((await admin.request(`/api/admin/candidates/${archivedProfile.id}/reset-password`, { method: 'POST' })).response.status, 409, '归档账户恢复前不得重置密码');
|
||||
const restoredAccounts = await schoolAdmin.request('/api/admin/candidate-accounts/archive', { method: 'POST', body: { scopeType: 'grade', scopeValue: '高三', archived: false } });
|
||||
assert.equal(restoredAccounts.response.status, 200, '校级管理员应可按年级恢复考生账户');
|
||||
const resetPassword = await admin.request(`/api/admin/candidates/${archivedProfile.id}/reset-password`, { method: 'POST' });
|
||||
assert.equal(resetPassword.response.status, 200, '超级管理员应可为未归档考生重置密码');
|
||||
assert.match(resetPassword.data.temporaryPassword, /^Reset-/, '重置服务应返回一次性临时密码');
|
||||
assert.equal((await createClient().request('/api/auth/login', { method: 'POST', body: { username: archiveTarget.candidateNumber, password: archiveTarget.initialPassword } })).response.status, 401, '重置后原密码应失效');
|
||||
const resetLogin = await createClient().request('/api/auth/login', { method: 'POST', body: { username: archiveTarget.candidateNumber, password: resetPassword.data.temporaryPassword } });
|
||||
assert.equal(resetLogin.data.user.mustChangePassword, true, '使用重置临时密码登录后必须先改密');
|
||||
assert.equal((await schoolAdmin.request(`/api/admin/candidates/${archivedProfile.id}`, { method: 'DELETE' })).response.status, 405, '任何管理员都不得删除考生账户');
|
||||
|
||||
const now = Date.now();
|
||||
const hour = 60 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user