Files
lindyson-ai/apps/api/tests/adminOverviewRoutes.test.ts
T
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、
迁移脚本、测试与自托管 docker-compose 示例。

开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读
publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为
VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注
Apache-2.0。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 02:10:46 +08:00

154 lines
5.3 KiB
TypeScript

import fastify from 'fastify';
import { beforeEach,describe,expect,it,vi } from 'vitest';
const mocks = vi.hoisted(() => ({
poolQuery: vi.fn(),
requireAdmin: vi.fn(),
}));
vi.mock('../src/db/pool.js', () => ({
pool: { query: mocks.poolQuery },
withTransaction: vi.fn(),
withCostReportingSession: vi.fn(),
}));
vi.mock('../src/auth/session.js', () => ({
requireAdmin: mocks.requireAdmin,
}));
import { registerAdminRoutes } from '../src/routes/adminRoutes.js';
describe('后台总览聚合', () => {
beforeEach(() => {
mocks.poolQuery.mockReset();
mocks.requireAdmin.mockReset();
mocks.requireAdmin.mockResolvedValue({ id: 'admin-1', email: 'admin@example.com', role: 'admin' });
});
it('从完整数据库集合返回北京时间聚合,不依赖列表分页结果', async () => {
mocks.poolQuery.mockResolvedValue(queryResult([{
total_users: 1200,
today_new_users: 8,
month_new_users: 75,
total_revenue: 980000,
today_revenue: 32000,
month_revenue: 380000,
seven_day_revenue: 120000,
total_jobs: 6000,
today_jobs: 32,
active_jobs: 5,
open_tickets: 6,
pending_invoices: 7,
seven_day_payment_failures: 3,
inactive_users: 84,
failed_jobs: 12,
seven_day_api_requests: 5,
seven_day_api_successes: 4,
revenue_trend: [
{ day: '2026-07-05', revenue: 10000, orders: 2 },
{ day: '2026-07-11', revenue: 32000, orders: 5 },
],
model_usage: [
{ model: 'Seedance 2.0', billableTokens: 108900 },
{ model: 'Doubao Seed 2.0 Lite', billableTokens: 8200 },
],
day_start: '2026-07-10T16:00:00.000Z',
month_start: '2026-06-30T16:00:00.000Z',
}]));
const app = makeApp();
try {
const response = await app.inject({ method: 'GET', url: '/api/admin/overview' });
expect(response.statusCode).toBe(200);
expect(response.json().summary).toEqual(expect.objectContaining({
totalUsers: 1200,
sevenDayPaymentFailures: 3,
inactiveUsers: 84,
failedJobs: 12,
sevenDayApiRequests: 5,
sevenDayApiSuccesses: 4,
sevenDayApiSuccessRate: 80,
revenueTrend: [
{ day: '2026-07-05', revenue: 10000, orders: 2 },
{ day: '2026-07-11', revenue: 32000, orders: 5 },
],
modelUsage: [
{ model: 'Seedance 2.0', billableTokens: 108900 },
{ model: 'Doubao Seed 2.0 Lite', billableTokens: 8200 },
],
timezone: 'Asia/Shanghai',
}));
expect(response.json().apiCostHealth).toEqual({
syncStatus: 'warning',
lastSyncedAt: '-',
matchState: 'cannot-reconcile',
residualTokens: null,
residualCostCny: null,
});
const sql = String(mocks.poolQuery.mock.calls[0]?.[0]);
expect(sql).toContain("AT TIME ZONE 'Asia/Shanghai'");
expect(sql).toContain('generate_series');
expect(sql).toContain("coalesce(failed_at, created_at) >= day_start - interval '6 days'");
expect(sql).toContain("last_active_at < day_start - interval '30 days'");
expect(sql).toContain("coalesce(completed_at, created_at) >= day_start - interval '6 days'");
expect(sql).toContain("pur.usage_unit = 'tokens'");
expect(sql).toContain('FROM image_provider_usage_records image_usage');
expect(sql).toContain('image_usage.total_tokens::bigint AS token_count');
expect(sql).toContain('FROM prompt_wizard_provider_requests prompt_usage');
expect(sql).toContain("prompt_usage.request_status = 'succeeded'");
expect(sql).toContain("THEN 'Doubao Seed 2.0 Lite'");
expect(sql).toContain('prompt_usage.input_tokens::bigint + prompt_usage.output_tokens::bigint');
expect(sql).toContain("CASE WHEN completed_at > now() THEN 'running' ELSE status END");
expect(sql).toContain('CASE WHEN completed_at > now() THEN NULL ELSE completed_at END');
expect(sql).toContain("status = 'success' AND completed_at <= now()");
expect(sql).toContain('AND started_at <= now()');
expect(sql).not.toContain('LIMIT');
} finally {
await app.close();
}
});
it('导航摘要只查询徽标所需计数', async () => {
mocks.poolQuery.mockResolvedValueOnce(queryResult([{
open_tickets: 4,
pending_invoices: 3,
active_jobs: 2,
manual_review_payments: 1,
manual_review_refunds: 5,
as_of: '2026-07-17T08:00:00.000Z',
}]));
const app = makeApp();
try {
const response = await app.inject({ method: 'GET', url: '/api/admin/navigation-summary' });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
openTickets: 4,
pendingInvoices: 3,
activeJobs: 2,
manualReviewPayments: 1,
manualReviewRefunds: 5,
asOf: '2026-07-17T08:00:00.000Z',
});
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).not.toContain('SELECT *');
} finally {
await app.close();
}
});
});
function makeApp() {
const app = fastify({ logger: false });
app.register(registerAdminRoutes);
app.setErrorHandler((error, _request, reply) => {
const value = error as { statusCode?: number; message?: string };
return reply.code(value.statusCode ?? 400).send({ message: value.message });
});
return app;
}
function queryResult(rows: unknown[]) {
return { rows, rowCount: rows.length };
}