2025-12-23 00:35:57 +08:00
|
|
|
import test from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
|
|
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
|
process.env.DB_PATH = ':memory:';
|
|
|
|
|
|
|
|
|
|
const jsonFetch = async (
|
|
|
|
|
baseUrl: string,
|
|
|
|
|
path: string,
|
|
|
|
|
options?: { method?: string; body?: unknown },
|
|
|
|
|
) => {
|
|
|
|
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
|
|
|
method: options?.method ?? 'GET',
|
|
|
|
|
headers: options?.body ? { 'Content-Type': 'application/json' } : undefined,
|
|
|
|
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
let json: any = null;
|
|
|
|
|
try {
|
|
|
|
|
json = text ? JSON.parse(text) : null;
|
|
|
|
|
} catch {
|
|
|
|
|
json = null;
|
|
|
|
|
}
|
|
|
|
|
return { status: res.status, json, text };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test('管理员任务分页统计接口返回结构正确', async () => {
|
|
|
|
|
const { initDatabase, run } = await import('../api/database');
|
|
|
|
|
await initDatabase();
|
|
|
|
|
|
|
|
|
|
const { app } = await import('../api/server');
|
|
|
|
|
const server = app.listen(0);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const addr = server.address();
|
|
|
|
|
assert.ok(addr && typeof addr === 'object');
|
|
|
|
|
const baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const userA = { id: randomUUID(), name: '测试甲', phone: '13800138001', password: '' };
|
|
|
|
|
const userB = { id: randomUUID(), name: '测试乙', phone: '13800138002', password: '' };
|
|
|
|
|
const subjectId = randomUUID();
|
|
|
|
|
const historyTaskId = randomUUID();
|
|
|
|
|
const upcomingTaskId = randomUUID();
|
|
|
|
|
const activeTaskId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [
|
|
|
|
|
userA.id,
|
|
|
|
|
userA.name,
|
|
|
|
|
userA.phone,
|
|
|
|
|
userA.password,
|
|
|
|
|
]);
|
|
|
|
|
await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [
|
|
|
|
|
userB.id,
|
|
|
|
|
userB.name,
|
|
|
|
|
userB.phone,
|
|
|
|
|
userB.password,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO exam_subjects (id, name, type_ratios, category_ratios, total_score, duration_minutes)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[
|
|
|
|
|
subjectId,
|
|
|
|
|
'测试科目',
|
|
|
|
|
JSON.stringify({ single: 100 }),
|
|
|
|
|
JSON.stringify({ 通用: 100 }),
|
|
|
|
|
100,
|
|
|
|
|
60,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题目1', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题目2', 'single', JSON.stringify(['A', 'B']), 'B', 5, '数学'],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const historyStartAt = new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
const historyEndAt = new Date(now - 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
const upcomingStartAt = new Date(now + 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
const upcomingEndAt = new Date(now + 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
const activeStartAt = new Date(now - 1 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
const activeEndAt = new Date(now + 1 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
|
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[historyTaskId, '历史任务', subjectId, historyStartAt, historyEndAt, null],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[upcomingTaskId, '未开始任务', subjectId, upcomingStartAt, upcomingEndAt, null],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[activeTaskId, '进行中任务', subjectId, activeStartAt, activeEndAt, null],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const linkUserToTask = async (taskId: string, userId: string) => {
|
|
|
|
|
await run(`INSERT INTO exam_task_users (id, task_id, user_id) VALUES (?, ?, ?)`, [
|
|
|
|
|
randomUUID(),
|
|
|
|
|
taskId,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await linkUserToTask(historyTaskId, userA.id);
|
|
|
|
|
await linkUserToTask(historyTaskId, userB.id);
|
|
|
|
|
await linkUserToTask(upcomingTaskId, userA.id);
|
|
|
|
|
await linkUserToTask(activeTaskId, userA.id);
|
|
|
|
|
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), userA.id, subjectId, historyTaskId, 90, 18, 20, new Date(now - 2.5 * 24 * 60 * 60 * 1000).toISOString()],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const overview = await jsonFetch(baseUrl, '/api/admin/dashboard/overview');
|
|
|
|
|
assert.equal(overview.status, 200);
|
|
|
|
|
assert.equal(overview.json?.success, true);
|
|
|
|
|
assert.equal(typeof overview.json?.data?.totalUsers, 'number');
|
|
|
|
|
assert.equal(typeof overview.json?.data?.activeSubjectCount, 'number');
|
|
|
|
|
assert.ok(Array.isArray(overview.json?.data?.questionCategoryStats));
|
|
|
|
|
assert.equal(typeof overview.json?.data?.taskStatusDistribution?.completed, 'number');
|
|
|
|
|
assert.equal(typeof overview.json?.data?.taskStatusDistribution?.ongoing, 'number');
|
|
|
|
|
assert.equal(typeof overview.json?.data?.taskStatusDistribution?.notStarted, 'number');
|
|
|
|
|
|
|
|
|
|
const history = await jsonFetch(baseUrl, '/api/admin/tasks/history-stats?page=1&limit=5');
|
|
|
|
|
assert.equal(history.status, 200);
|
|
|
|
|
assert.equal(history.json?.success, true);
|
|
|
|
|
assert.ok(Array.isArray(history.json?.data));
|
|
|
|
|
assert.equal(history.json?.pagination?.page, 1);
|
|
|
|
|
assert.equal(history.json?.pagination?.limit, 5);
|
|
|
|
|
assert.equal(history.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(history.json?.pagination?.pages, 1);
|
|
|
|
|
assert.equal(history.json?.data?.[0]?.taskName, '历史任务');
|
|
|
|
|
|
|
|
|
|
const upcoming = await jsonFetch(baseUrl, '/api/admin/tasks/upcoming-stats?page=1&limit=5');
|
|
|
|
|
assert.equal(upcoming.status, 200);
|
|
|
|
|
assert.equal(upcoming.json?.success, true);
|
|
|
|
|
assert.ok(Array.isArray(upcoming.json?.data));
|
|
|
|
|
assert.equal(upcoming.json?.pagination?.page, 1);
|
|
|
|
|
assert.equal(upcoming.json?.pagination?.limit, 5);
|
|
|
|
|
assert.equal(upcoming.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(upcoming.json?.pagination?.pages, 1);
|
|
|
|
|
assert.equal(upcoming.json?.data?.[0]?.taskName, '未开始任务');
|
|
|
|
|
|
2025-12-25 00:15:14 +08:00
|
|
|
const allStats = await jsonFetch(baseUrl, '/api/admin/tasks/all-stats?page=1&limit=5');
|
|
|
|
|
assert.equal(allStats.status, 200);
|
|
|
|
|
assert.equal(allStats.json?.success, true);
|
|
|
|
|
assert.ok(Array.isArray(allStats.json?.data));
|
|
|
|
|
assert.equal(allStats.json?.pagination?.page, 1);
|
|
|
|
|
assert.equal(allStats.json?.pagination?.limit, 5);
|
|
|
|
|
assert.equal(allStats.json?.pagination?.total, 3);
|
|
|
|
|
assert.equal(allStats.json?.pagination?.pages, 1);
|
|
|
|
|
|
|
|
|
|
const byName = (name: string) => (allStats.json?.data as any[]).find((d) => d.taskName === name);
|
|
|
|
|
assert.equal(byName('历史任务')?.status, '已完成');
|
|
|
|
|
assert.equal(byName('未开始任务')?.status, '未开始');
|
|
|
|
|
assert.equal(byName('进行中任务')?.status, '进行中');
|
|
|
|
|
|
|
|
|
|
const allCompleted = await jsonFetch(baseUrl, '/api/admin/tasks/all-stats?page=1&limit=5&status=completed');
|
|
|
|
|
assert.equal(allCompleted.status, 200);
|
|
|
|
|
assert.equal(allCompleted.json?.success, true);
|
|
|
|
|
assert.equal(allCompleted.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(allCompleted.json?.data?.[0]?.taskName, '历史任务');
|
|
|
|
|
assert.equal(allCompleted.json?.data?.[0]?.status, '已完成');
|
|
|
|
|
|
|
|
|
|
const allOngoing = await jsonFetch(baseUrl, '/api/admin/tasks/all-stats?page=1&limit=5&status=ongoing');
|
|
|
|
|
assert.equal(allOngoing.status, 200);
|
|
|
|
|
assert.equal(allOngoing.json?.success, true);
|
|
|
|
|
assert.equal(allOngoing.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(allOngoing.json?.data?.[0]?.taskName, '进行中任务');
|
|
|
|
|
assert.equal(allOngoing.json?.data?.[0]?.status, '进行中');
|
|
|
|
|
|
|
|
|
|
const allNotStarted = await jsonFetch(baseUrl, '/api/admin/tasks/all-stats?page=1&limit=5&status=notStarted');
|
|
|
|
|
assert.equal(allNotStarted.status, 200);
|
|
|
|
|
assert.equal(allNotStarted.json?.success, true);
|
|
|
|
|
assert.equal(allNotStarted.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(allNotStarted.json?.data?.[0]?.taskName, '未开始任务');
|
|
|
|
|
assert.equal(allNotStarted.json?.data?.[0]?.status, '未开始');
|
|
|
|
|
|
|
|
|
|
const inHistoryEndAtRange = await jsonFetch(
|
|
|
|
|
baseUrl,
|
|
|
|
|
`/api/admin/tasks/all-stats?page=1&limit=5&endAtStart=${encodeURIComponent(
|
|
|
|
|
new Date(now - 2.6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
|
|
|
)}&endAtEnd=${encodeURIComponent(new Date(now - 1.9 * 24 * 60 * 60 * 1000).toISOString())}`,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(inHistoryEndAtRange.status, 200);
|
|
|
|
|
assert.equal(inHistoryEndAtRange.json?.success, true);
|
|
|
|
|
assert.equal(inHistoryEndAtRange.json?.pagination?.total, 1);
|
|
|
|
|
assert.equal(inHistoryEndAtRange.json?.data?.[0]?.taskName, '历史任务');
|
|
|
|
|
|
|
|
|
|
const invalidEndAtStart = await jsonFetch(baseUrl, '/api/admin/tasks/all-stats?endAtStart=not-a-date');
|
|
|
|
|
assert.equal(invalidEndAtStart.status, 400);
|
|
|
|
|
assert.equal(invalidEndAtStart.json?.success, false);
|
|
|
|
|
|
2025-12-25 21:54:52 +08:00
|
|
|
const firstGenerate = await jsonFetch(baseUrl, '/api/quiz/generate', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { userId: userA.id, taskId: activeTaskId },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(firstGenerate.status, 200);
|
|
|
|
|
assert.equal(firstGenerate.json?.success, true);
|
|
|
|
|
assert.ok(Array.isArray(firstGenerate.json?.data?.questions));
|
|
|
|
|
|
2025-12-26 18:39:17 +08:00
|
|
|
const countSubjectId = randomUUID();
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO exam_subjects (id, name, type_ratios, category_ratios, total_score, duration_minutes)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[
|
|
|
|
|
countSubjectId,
|
|
|
|
|
'题量科目',
|
|
|
|
|
JSON.stringify({ single: 2, multiple: 1, judgment: 1, text: 0 }),
|
|
|
|
|
JSON.stringify({ 通用: 4 }),
|
|
|
|
|
40,
|
|
|
|
|
60,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题量-单选1', 'single', JSON.stringify(['A', 'B', 'C', 'D']), 'A', 10, '通用'],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题量-单选2', 'single', JSON.stringify(['A', 'B', 'C', 'D']), 'B', 10, '通用'],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题量-多选1', 'multiple', JSON.stringify(['A', 'B', 'C', 'D']), JSON.stringify(['A', 'B']), 10, '通用'],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), '题量-判断1', 'judgment', null, 'A', 10, '通用'],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const countGenerate = await jsonFetch(baseUrl, '/api/quiz/generate', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { userId: userA.id, subjectId: countSubjectId },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(countGenerate.status, 200);
|
|
|
|
|
assert.equal(countGenerate.json?.success, true);
|
|
|
|
|
assert.equal(countGenerate.json?.data?.timeLimit, 60);
|
|
|
|
|
assert.equal(countGenerate.json?.data?.totalScore, 40);
|
|
|
|
|
assert.ok(Array.isArray(countGenerate.json?.data?.questions));
|
|
|
|
|
assert.equal(countGenerate.json?.data?.questions?.length, 4);
|
|
|
|
|
|
|
|
|
|
const byType = (countGenerate.json?.data?.questions as any[]).reduce((acc, q) => {
|
|
|
|
|
acc[q.type] = (acc[q.type] || 0) + 1;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, number>);
|
|
|
|
|
assert.equal(byType.single, 2);
|
|
|
|
|
assert.equal(byType.multiple, 1);
|
|
|
|
|
assert.equal(byType.judgment, 1);
|
|
|
|
|
assert.equal(byType.text || 0, 0);
|
|
|
|
|
|
2025-12-25 21:54:52 +08:00
|
|
|
await run(
|
|
|
|
|
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), userA.id, subjectId, activeTaskId, 10, 2, 20, new Date(now - 1000).toISOString()],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), userA.id, subjectId, activeTaskId, 30, 6, 20, new Date(now - 900).toISOString()],
|
|
|
|
|
);
|
|
|
|
|
await run(
|
|
|
|
|
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[randomUUID(), userA.id, subjectId, activeTaskId, 20, 4, 20, new Date(now - 800).toISOString()],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const userTasks = await jsonFetch(baseUrl, `/api/exam-tasks/user/${userA.id}`);
|
|
|
|
|
assert.equal(userTasks.status, 200);
|
|
|
|
|
assert.equal(userTasks.json?.success, true);
|
|
|
|
|
assert.ok(Array.isArray(userTasks.json?.data));
|
|
|
|
|
assert.equal(userTasks.json?.data?.[0]?.id, activeTaskId);
|
|
|
|
|
assert.equal(userTasks.json?.data?.[0]?.usedAttempts, 3);
|
|
|
|
|
assert.equal(userTasks.json?.data?.[0]?.maxAttempts, 3);
|
|
|
|
|
assert.equal(userTasks.json?.data?.[0]?.bestScore, 30);
|
|
|
|
|
|
|
|
|
|
const fourthGenerate = await jsonFetch(baseUrl, '/api/quiz/generate', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { userId: userA.id, taskId: activeTaskId },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(fourthGenerate.status, 400);
|
|
|
|
|
assert.equal(fourthGenerate.json?.success, false);
|
|
|
|
|
assert.ok(String(fourthGenerate.json?.message || '').includes('考试次数已用尽'));
|
|
|
|
|
|
2025-12-23 00:35:57 +08:00
|
|
|
const invalidPageFallback = await jsonFetch(baseUrl, '/api/admin/tasks/history-stats?page=0&limit=0');
|
|
|
|
|
assert.equal(invalidPageFallback.status, 200);
|
|
|
|
|
assert.equal(invalidPageFallback.json?.pagination?.page, 1);
|
|
|
|
|
assert.equal(invalidPageFallback.json?.pagination?.limit, 5);
|
|
|
|
|
} finally {
|
|
|
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
|
|
|
}
|
|
|
|
|
});
|