157 lines
6.4 KiB
TypeScript
157 lines
6.4 KiB
TypeScript
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 = <T>(response: { body: unknown }) => response.body as T;
|
|
function sessionCookie(response: { headers: Record<string, unknown> }) {
|
|
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: '<h2>年度说明</h2><p onclick="alert(1)">公开正文</p><script>alert(1)</script>',
|
|
isPinned: true,
|
|
sortOrder: 10,
|
|
})
|
|
.expect(201);
|
|
const disclosure =
|
|
body<ApiSuccessResponse<FinancialDisclosureDetailResponse>>(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<ApiSuccessResponse<PaginatedFinancialDisclosuresResponse>>(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: '<p>专项公开正文内容</p>',
|
|
})
|
|
.expect(201);
|
|
const item =
|
|
body<ApiSuccessResponse<FinancialDisclosureDetailResponse>>(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: '<p>覆盖已发布内容</p>',
|
|
})
|
|
.expect(409);
|
|
expect(body<ApiErrorResponse>(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);
|
|
});
|
|
});
|