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, '未开始任务'); 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); 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)); 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('考试次数已用尽')); 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((resolve) => server.close(() => resolve())); } });