import { ValidationPipe, type INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import cookieParser from 'cookie-parser'; import type { Server } from 'node:http'; import request from 'supertest'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import type { ApiErrorResponse, ApiSuccessResponse, FinancialDisclosureDetailResponse, PaginatedFinancialDisclosuresResponse, } from '@lehuo/contracts'; import { AppModule } from '../src/app.module'; import { PasswordService } from '../src/auth/password.service'; import { ADMIN_SESSION_COOKIE } from '../src/auth/session.constants'; import { PrismaService } from '../src/database/prisma.service'; const ORIGIN = 'http://localhost:5173'; const body = (response: { body: unknown }) => response.body as T; function sessionCookie(response: { headers: Record }) { const values = response.headers['set-cookie']; const cookies: string[] = Array.isArray(values) ? values.filter((value): value is string => typeof value === 'string') : []; const found = cookies.find((value) => value.startsWith(`${ADMIN_SESSION_COOKIE}=`)); if (!found) throw new Error('no cookie'); return found.split(';')[0]!; } describe('financial disclosure publishing', () => { let app: INestApplication, prisma: PrismaService, passwords: PasswordService; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = module.createNestApplication(); app.use(cookieParser()); app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }), ); await app.init(); prisma = app.get(PrismaService); passwords = app.get(PasswordService); }); beforeEach(async () => { await prisma.auditLog.deleteMany(); await prisma.financialDisclosureStatusHistory.deleteMany(); await prisma.financialDisclosure.deleteMany(); await prisma.authSession.deleteMany(); await prisma.userRole.deleteMany(); await prisma.adminCredential.deleteMany(); await prisma.staffProfile.deleteMany(); }); afterAll(() => app.close()); async function admin() { const username = `finance.${crypto.randomUUID()}`; const role = await prisma.role.findUniqueOrThrow({ where: { code: 'ADMIN' } }); const user = await prisma.user.create({ data: { publicId: crypto.randomUUID(), userType: 'STAFF', staffProfile: { create: { displayName: '财务公开管理员' } }, adminCredential: { create: { username, passwordHash: await passwords.hash('correct horse battery staple') }, }, }, }); await prisma.userRole.create({ data: { userId: user.id, roleId: role.id } }); const response = await request(app.getHttpServer() as Server) .post('/api/v1/admin/auth/login') .set('origin', ORIGIN) .send({ username, password: 'correct horse battery staple' }) .expect(201); return sessionCookie(response); } it('keeps drafts private, sanitizes rich text, publishes and audits the document', async () => { const cookie = await admin(); const created = await request(app.getHttpServer() as Server) .post('/api/v1/admin/financial-disclosures') .set('origin', ORIGIN) .set('cookie', cookie) .send({ clientRequestId: crypto.randomUUID(), title: '2026年度财务公开', summary: '年度财务公开说明', richText: '

年度说明

公开正文

', isPinned: true, sortOrder: 10, }) .expect(201); const disclosure = body>(created).data.disclosure; expect(disclosure.richText).not.toMatch(/script|onclick/); await request(app.getHttpServer() as Server) .get(`/api/v1/financial-disclosures/${disclosure.publicId}`) .expect(404); await request(app.getHttpServer() as Server) .post(`/api/v1/admin/financial-disclosures/${disclosure.publicId}/transition`) .set('origin', ORIGIN) .set('cookie', cookie) .send({ clientRequestId: crypto.randomUUID(), targetStatus: 'PUBLISHED' }) .expect(201); const listed = await request(app.getHttpServer() as Server) .get('/api/v1/financial-disclosures?page=1&pageSize=20') .expect(200); expect( body>(listed).data.items, ).toHaveLength(1); expect(await prisma.financialDisclosureStatusHistory.count()).toBe(2); expect(await prisma.auditLog.count({ where: { resourceType: 'FINANCIAL_DISCLOSURE' } })).toBe( 2, ); }); it('rejects edits after publishing and hides archived documents', async () => { const cookie = await admin(), requestId = crypto.randomUUID(); const created = await request(app.getHttpServer() as Server) .post('/api/v1/admin/financial-disclosures') .set('origin', ORIGIN) .set('cookie', cookie) .send({ clientRequestId: requestId, title: '专项财务公开', richText: '

专项公开正文内容

', }) .expect(201); const item = body>(created).data.disclosure; await request(app.getHttpServer() as Server) .post(`/api/v1/admin/financial-disclosures/${item.publicId}/transition`) .set('origin', ORIGIN) .set('cookie', cookie) .send({ clientRequestId: crypto.randomUUID(), targetStatus: 'PUBLISHED' }) .expect(201); const rejected = await request(app.getHttpServer() as Server) .patch(`/api/v1/admin/financial-disclosures/${item.publicId}`) .set('origin', ORIGIN) .set('cookie', cookie) .send({ expectedRevision: item.revision + 1, title: item.title, richText: '

覆盖已发布内容

', }) .expect(409); expect(body(rejected).error.code).toBe('INVALID_STATE_TRANSITION'); await request(app.getHttpServer() as Server) .post(`/api/v1/admin/financial-disclosures/${item.publicId}/transition`) .set('origin', ORIGIN) .set('cookie', cookie) .send({ clientRequestId: crypto.randomUUID(), targetStatus: 'ARCHIVED' }) .expect(201); await request(app.getHttpServer() as Server) .get(`/api/v1/financial-disclosures/${item.publicId}`) .expect(404); }); });