177 lines
10 KiB
JavaScript
177 lines
10 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { rm } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const root = resolve(process.cwd());
|
|
const port = 4182;
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
const testDb = resolve(root, 'data', 'test-db.json');
|
|
await rm(testDb, { force: true });
|
|
|
|
const server = spawn(process.execPath, ['server.mjs'], {
|
|
cwd: root,
|
|
env: { ...process.env, PORT: String(port), EXAM_DB_PATH: testDb },
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
});
|
|
|
|
let serverError = '';
|
|
server.stderr.on('data', chunk => { serverError += chunk.toString(); });
|
|
|
|
async function waitForServer() {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/public/home`);
|
|
if (response.ok) return;
|
|
} catch {}
|
|
await new Promise(resolveWait => setTimeout(resolveWait, 100));
|
|
}
|
|
throw new Error(`服务器未能启动:${serverError}`);
|
|
}
|
|
|
|
function createClient() {
|
|
let cookie = '';
|
|
return {
|
|
async request(path, options = {}) {
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
...options,
|
|
headers: { ...(cookie ? { Cookie: cookie } : {}), ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...options.headers },
|
|
body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body
|
|
});
|
|
const setCookie = response.headers.get('set-cookie');
|
|
if (setCookie) cookie = setCookie.split(';')[0];
|
|
const type = response.headers.get('content-type') || '';
|
|
const data = type.includes('application/json') ? await response.json() : await response.text();
|
|
return { response, data };
|
|
}
|
|
};
|
|
}
|
|
|
|
const admin = createClient();
|
|
const candidate = createClient();
|
|
const anonymous = createClient();
|
|
|
|
try {
|
|
await waitForServer();
|
|
|
|
const publicHome = await anonymous.request('/api/public/home');
|
|
assert.equal(publicHome.response.status, 200);
|
|
assert.ok(publicHome.data.notices.length >= 3, '公开首页应返回通知');
|
|
assert.ok(publicHome.data.exams.some(exam => exam.subjects.length > 1), '公开考试应包含多个科目');
|
|
|
|
const register = await candidate.request('/api/auth/register', {
|
|
method: 'POST',
|
|
body: { username: 'test_candidate', password: 'Test12345!', name: '测试考生', gender: '男', idNumber: '320101200801019999', phone: '13900009999', school: '海州市测试中学', grade: '高三(1)班' }
|
|
});
|
|
assert.equal(register.response.status, 201, '考生应可自主注册');
|
|
|
|
const loginCandidate = await candidate.request('/api/auth/login', { method: 'POST', body: { username: 'test_candidate', password: 'Test12345!' } });
|
|
assert.equal(loginCandidate.data.user.role, 'candidate');
|
|
|
|
const updateProfile = await candidate.request('/api/candidate/profile', {
|
|
method: 'PUT',
|
|
body: { name: '测试考生新名', gender: '男', idNumber: '320101200801019999', phone: '13900009999', email: 'test@example.com', school: '海州市测试中学', grade: '高三(1)班', address: '海州市测试区 1 号', emergencyContact: '测试家长', emergencyPhone: '13800008888' }
|
|
});
|
|
assert.equal(updateProfile.response.status, 200, '考生应可自主维护完整资料');
|
|
assert.equal(updateProfile.data.profile.status, 'pending', '资料修改后应重新进入审核');
|
|
const refreshedSession = await candidate.request('/api/auth/me');
|
|
assert.equal(refreshedSession.data.user.displayName, '测试考生新名', '考生姓名修改后账号显示名应同步');
|
|
|
|
const candidateCannotAdmin = await candidate.request('/api/admin/dashboard');
|
|
assert.equal(candidateCannotAdmin.response.status, 403, '考生不得访问管理接口');
|
|
|
|
const loginAdmin = await admin.request('/api/auth/login', { method: 'POST', body: { username: 'admin', password: 'Admin123!' } });
|
|
assert.equal(loginAdmin.data.user.role, 'admin');
|
|
|
|
const now = Date.now();
|
|
const hour = 60 * 60 * 1000;
|
|
const createExam = await admin.request('/api/admin/exams', {
|
|
method: 'POST',
|
|
body: {
|
|
name: '系统全流程测试考试', code: 'EX-TEST-FLOW', description: '自动化测试创建的多科目考试',
|
|
registrationStart: new Date(now - hour).toISOString(), registrationEnd: new Date(now + 24 * hour).toISOString(),
|
|
examStart: new Date(now + 48 * hour).toISOString(), examEnd: new Date(now + 60 * hour).toISOString(),
|
|
admitDownloadStart: new Date(now - hour).toISOString(), admitDownloadEnd: new Date(now + 47 * hour).toISOString(),
|
|
location: '测试考点', status: 'published', subjects: ['语文', '数学', '外语']
|
|
}
|
|
});
|
|
assert.equal(createExam.response.status, 201);
|
|
assert.equal(createExam.data.exam.subjects.length, 3, '管理员应可创建多科目考试');
|
|
const exam = createExam.data.exam;
|
|
|
|
const beforeApproval = await candidate.request('/api/candidate/registrations', { method: 'POST', body: { examId: exam.id, subjectIds: [exam.subjects[0].id] } });
|
|
assert.equal(beforeApproval.response.status, 403, '资料审核前不得报名考试');
|
|
|
|
const candidates = await admin.request('/api/admin/candidates');
|
|
const profile = candidates.data.candidates.find(item => item.username === 'test_candidate');
|
|
assert.ok(profile, '管理员应能看到新注册考生');
|
|
assert.equal(profile.address, '海州市测试区 1 号', '管理员应能审核考生自主填写的完整资料');
|
|
const approveProfile = await admin.request(`/api/admin/candidates/${profile.id}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '自动化测试审核通过' } });
|
|
assert.equal(approveProfile.data.profile.status, 'approved');
|
|
|
|
const submitRegistration = await candidate.request('/api/candidate/registrations', {
|
|
method: 'POST', body: { examId: exam.id, subjectIds: [exam.subjects[0].id, exam.subjects[2].id] }
|
|
});
|
|
assert.equal(submitRegistration.response.status, 201);
|
|
assert.equal(submitRegistration.data.registration.subjectIds.length, 2, '考生应可自主选择多个科目');
|
|
const registrationId = submitRegistration.data.registration.id;
|
|
|
|
const adminRegistrations = await admin.request('/api/admin/registrations');
|
|
const adminRegistration = adminRegistrations.data.registrations.find(item => item.id === registrationId);
|
|
assert.equal(adminRegistration.status, 'pending', '新报名应进入管理员审核队列');
|
|
|
|
const approveRegistration = await admin.request(`/api/admin/registrations/${registrationId}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '科目与资格核验通过' } });
|
|
assert.equal(approveRegistration.data.registration.status, 'approved');
|
|
|
|
const generateAdmit = await admin.request(`/api/admin/registrations/${registrationId}/admit-card`, { method: 'POST', body: { testCenter: '海州市测试中学' } });
|
|
assert.ok(generateAdmit.data.admitCard.number, '管理员应能生成准考证号');
|
|
|
|
const candidateRegistrations = await candidate.request('/api/candidate/registrations');
|
|
const candidateRegistration = candidateRegistrations.data.registrations.find(item => item.id === registrationId);
|
|
assert.ok(candidateRegistration.admitCard, '考生端应看到已生成准考证');
|
|
|
|
const downloadAdmit = await candidate.request(`/api/candidate/registrations/${registrationId}/admit-card`);
|
|
assert.equal(downloadAdmit.response.status, 200, '规定时间内应可下载准考证');
|
|
assert.match(downloadAdmit.data, /测试考生新名/);
|
|
assert.match(downloadAdmit.response.headers.get('content-disposition') || '', /attachment/);
|
|
|
|
const futureExamResponse = await admin.request('/api/admin/exams', {
|
|
method: 'POST',
|
|
body: {
|
|
name: '准考证窗口限制测试', code: 'EX-TEST-WINDOW', description: '验证下载时间限制',
|
|
registrationStart: new Date(now - hour).toISOString(), registrationEnd: new Date(now + 24 * hour).toISOString(),
|
|
examStart: new Date(now + 96 * hour).toISOString(), examEnd: new Date(now + 100 * hour).toISOString(),
|
|
admitDownloadStart: new Date(now + 48 * hour).toISOString(), admitDownloadEnd: new Date(now + 95 * hour).toISOString(),
|
|
location: '测试考点', status: 'published', subjects: ['综合能力']
|
|
}
|
|
});
|
|
const futureExam = futureExamResponse.data.exam;
|
|
const futureRegistration = await candidate.request('/api/candidate/registrations', { method: 'POST', body: { examId: futureExam.id, subjectIds: [futureExam.subjects[0].id] } });
|
|
const futureRegistrationId = futureRegistration.data.registration.id;
|
|
await admin.request(`/api/admin/registrations/${futureRegistrationId}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '通过' } });
|
|
await admin.request(`/api/admin/registrations/${futureRegistrationId}/admit-card`, { method: 'POST', body: { testCenter: '海州市测试中学' } });
|
|
const earlyDownload = await candidate.request(`/api/candidate/registrations/${futureRegistrationId}/admit-card`);
|
|
assert.equal(earlyDownload.response.status, 403, '准考证下载窗口开放前必须拒绝下载');
|
|
|
|
const publishNotice = await admin.request('/api/admin/notices', { method: 'POST', body: { title: '系统测试成绩发布通知', summary: '测试通知将同步到首页', content: '系统测试考试成绩现已发布。', category: '成绩通知', pinned: true, status: 'published' } });
|
|
assert.equal(publishNotice.response.status, 201);
|
|
const refreshedHome = await anonymous.request('/api/public/home');
|
|
assert.ok(refreshedHome.data.notices.some(item => item.title === '系统测试成绩发布通知'), '管理员发布通知后首页应可见');
|
|
|
|
const publishResult = await admin.request('/api/admin/results', { method: 'POST', body: { registrationId, subjectId: exam.subjects[0].id, score: 126, grade: 'A', published: true } });
|
|
assert.equal(publishResult.response.status, 200);
|
|
const results = await candidate.request('/api/candidate/results');
|
|
assert.ok(results.data.results.some(item => item.score === 126 && item.subjectName === '语文'), '已发布成绩应在考生端可查询');
|
|
|
|
console.log('✓ 公开首页与通知读取');
|
|
console.log('✓ 考生自主注册、完整资料维护与管理员审核');
|
|
console.log('✓ 多科目考试创建与考生自主选科报名');
|
|
console.log('✓ 报名审核、准考证生成、开放期下载与窗口限制');
|
|
console.log('✓ 成绩录入、发布与考生查询');
|
|
console.log('✓ 候选人与管理员角色权限隔离');
|
|
} finally {
|
|
server.kill('SIGTERM');
|
|
await new Promise(resolveWait => server.once('exit', resolveWait));
|
|
await rm(testDb, { force: true });
|
|
}
|