/** @jest-environment node */ import { NextRequest } from 'next/server' import { mkdtemp, writeFile, rm } from 'fs/promises' import { tmpdir } from 'os' import path from 'path' import { GET } from '@/app/api/external/certificate-reports/route' import { GET as download } from '@/app/api/external/download/route' import { prisma } from '@/lib/prisma' import { validateApiKey, logApiCall } from '@/lib/external-api' import { verifyDownloadToken, generateDownloadUrl } from '@/lib/download-token' import { parseCertificateReportQuery as parse, buildCertificateReportWhere as where, certificateToday, certificateExpiryStatus, findExternalCertificateReports, resolveCertificateReportSource } from '@/lib/external-certificate-reports' jest.mock('@/lib/prisma', () => ({ prisma: { material: { findMany: jest.fn(), count: jest.fn(), findUnique: jest.fn(), update: jest.fn(), updateMany: jest.fn() }, $transaction: jest.fn() } })) jest.mock('@/lib/public-app-url', () => ({ getPublicAppOrigin: () => 'https://sc.jiansun.vip' })) jest.mock('@/lib/external-api', () => ({ ...jest.requireActual('@/lib/external-api'), validateApiKey: jest.fn(), logApiCall: jest.fn().mockResolvedValue(undefined) })) const db = prisma as unknown as { material: { findMany: jest.Mock; count: jest.Mock; findUnique: jest.Mock; update: jest.Mock; updateMany: jest.Mock }; $transaction: jest.Mock } const id = '02000000-0000-4000-8000-000000000001' const a = (assetType = 'report-pdf', fileName = '报告.pdf', fileUrl = '/uploads/report.pdf') => ({ id: 'file-1', assetType, fileName, fileUrl, fileSize: BigInt(100), sortOrder: 0 }) const material = (extra: Record = {}) => ({ id, materialType: '证书-检测报告', reportKind: '原材料', reportTitle: '板材报告', productModel: '', productCategoryName: '板材', platformName: '大货', certificateNumber: 'CERT-01', certificateTestedOn: new Date('2026-01-01Z'), certificateExpiresOn: new Date('2027-01-01Z'), certificateUsageInstructions: null, reportUsageRules: '核对检测项目和使用范围', createdAt: new Date('2026-01-02Z'), updatedAt: new Date('2026-01-03Z'), taskStatus: 'completed', assets: [a()], ...extra }) const q = (value = '') => parse(new URLSearchParams(value)) const request = (value = '', key = 'test') => new NextRequest('https://sc.jiansun.vip/api/external/certificate-reports?' + value, { headers: key ? { 'X-API-Key': key } : {} }) const oldSecret = process.env.DOWNLOAD_TOKEN_SECRET beforeEach(() => { jest.clearAllMocks(); process.env.DOWNLOAD_TOKEN_SECRET = 'local-certificate-report-test-secret' ;(validateApiKey as jest.Mock).mockResolvedValue({ id: 'key-1' }) db.material.count.mockResolvedValue(1); db.material.findMany.mockResolvedValue([material()]); db.material.findUnique.mockResolvedValue(material()) db.$transaction.mockImplementation((operations: unknown[]) => Promise.all(operations)) }) afterAll(() => { if (oldSecret === undefined) delete process.env.DOWNLOAD_TOKEN_SECRET; else process.env.DOWNLOAD_TOKEN_SECRET = oldSecret }) test.each(['sku=A', 'barcode=A', 'search=', 'search=%00', 'page=0', 'pageSize=51', 'page=1&page=2', 'reportKind=其他', 'platform=顾家', 'fileKind=zip', 'expiryStatus=valid', 'testedFrom=2026-02-30', 'testedTo=2026-10-01&testedFrom=2026-10-02', 'expiresFrom=2027-01-01&expiresTo=2026-01-01', 'expiryStatus=unknown&expiresFrom=2026-01-01', 'materialId=not-uuid'])('rejects invalid condition %s', value => { expect(() => q(value)).toThrow() }) test('combines all report fields, inclusive date ranges and status without overwriting conditions', () => { const query = q(`search=板材&reportKind=成品&platform=品牌&productCategory=桌子&spu=spu-01&certificateNumber=Cert-01&materialId=${id}&testedFrom=2026-01-01&testedTo=2026-10-01&expiresFrom=2026-01-01&expiresTo=2027-01-01&expiryStatus=unexpired&fileKind=pdf&page=2&pageSize=5`) const actual = where(query, new Date('2026-10-16Z')) expect(actual).toMatchObject({ materialType: '证书-检测报告', deletedAt: null, isIncompleteShell: false, taskDetailPrivate: false, id, reportTitle: { contains: '板材', mode: 'insensitive' }, reportKind: '成品', platformName: '品牌', productCategoryName: '桌子', productModel: { equals: 'spu-01', mode: 'insensitive' }, certificateNumber: { equals: 'Cert-01', mode: 'insensitive' }, certificateTestedOn: { gte: new Date('2026-01-01Z'), lte: new Date('2026-10-01Z') }, AND: [{ certificateExpiresOn: { gte: new Date('2026-01-01Z'), lte: new Date('2027-01-01Z') } }, { certificateExpiresOn: { gte: new Date('2026-10-16Z') } }], assets: { some: { assetType: { in: ['report-pdf'] } } } }) }) test('expiry date is inclusive through the UTC+8 business day and missing dates remain unknown', () => { expect(certificateToday(new Date('2026-10-15T15:59:59Z')).toISOString()).toBe('2026-10-15T00:00:00.000Z') const today = certificateToday(new Date('2026-10-15T16:00:00Z')) expect(certificateExpiryStatus(new Date('2026-10-16Z'), today)).toBe('unexpired') expect(certificateExpiryStatus(new Date('2026-10-15Z'), today)).toBe('expired') expect(certificateExpiryStatus(null, today)).toBe('unknown') expect(where(q('expiryStatus=unknown')).certificateExpiresOn).toBeNull() }) test('auth precedes lookup and query validation; credentials in URL are not accepted', async () => { ;(validateApiKey as jest.Mock).mockResolvedValue(null) expect((await GET(request('apiKey=secret', ''))).status).toBe(401) expect(validateApiKey).toHaveBeenCalledWith(null); expect(db.$transaction).not.toHaveBeenCalled() }) test('returns raw-material report without SPU, original files only, metadata and isolated tokens; does not mutate task state', async () => { db.material.findMany.mockResolvedValue([material({ assets: [a(), { ...a('report-image', '报告.png'), id: 'file-2', sortOrder: 1 }, a('preview', 'preview.jpg'), a('zip', 'all.zip'), a('report-pdf', 'bad.exe')] })]) const response = await GET(request('reportKind=原材料')) expect(response.status).toBe(200); expect(response.headers.get('cache-control')).toContain('no-store') const data = (await response.json()).data, item = data.items[0] expect(item).toMatchObject({ spu: null, reportKind: '原材料', testedOn: '2026-01-01', expiresOn: '2027-01-01', reportUsageRules: '核对检测项目和使用范围' }) expect(item.files).toHaveLength(2) expect(verifyDownloadToken(new URL(item.files[0].downloadUrl).searchParams.get('token')!)).toMatchObject({ materialId: id, assetId: 'file-1', downloadType: 'certificate-report-source' }) expect(JSON.stringify(data)).not.toContain('/uploads/'); expect(item.files.map((f: { fileKind: string }) => f.fileKind)).toEqual(['pdf', 'image']) expect(db.material.update).not.toHaveBeenCalled(); expect(db.material.updateMany).not.toHaveBeenCalled() expect(logApiCall).toHaveBeenCalledWith(expect.objectContaining({ endpoint: '/api/external/certificate-reports', statusCode: 200 })) }) test('uses bounded report pagination and consistent count/list snapshot; an unknown ID returns an empty list', async () => { db.material.findMany.mockResolvedValue([]); db.material.count.mockResolvedValue(0) const result = await findExternalCertificateReports(q(`materialId=${id}&page=2&pageSize=5`)) expect(result).toMatchObject({ items: [], pagination: { totalReports: 0, hasMore: false } }) expect(db.material.findMany).toHaveBeenCalledWith(expect.objectContaining({ skip: 5, take: 5, orderBy: [{ updatedAt: 'desc' }, { id: 'asc' }] })) expect(db.$transaction.mock.calls[0][1]).toEqual({ isolationLevel: 'RepeatableRead' }) }) test('reports missing originals explicitly and preserves null legacy dates', async () => { db.material.findMany.mockResolvedValue([material({ assets: [], certificateTestedOn: null, certificateExpiresOn: null })]) expect((await findExternalCertificateReports(q())).items[0]).toMatchObject({ testedOn: null, expiresOn: null, expiryStatus: 'unknown', files: [], issues: [{ code: 'REPORT_FILES_NOT_AVAILABLE' }] }) }) test('invalid queries and database failures are safely reported', async () => { expect((await GET(request('pageSize=100'))).status).toBe(400) expect(db.$transaction).not.toHaveBeenCalled() db.$transaction.mockRejectedValue(new Error('postgres://secret@private')) const log = jest.spyOn(console, 'error').mockImplementation(() => {}) const response = await GET(request()) expect(response.status).toBe(500); expect(JSON.stringify(await response.json())).not.toContain('secret') log.mockRestore() }) test.each([{ deletedAt: new Date() }, { taskDetailPrivate: true }, { isIncompleteShell: true }, { materialType: '任务-AI原图' }])('rechecks download visibility %j', extra => { expect(resolveCertificateReportSource(material(extra), 'file-1')).toBeNull() }) test('rejects assets of another report, ZIPs and preview substitutes', () => { expect(resolveCertificateReportSource(material(), 'foreign')).toBeNull() expect(resolveCertificateReportSource(material(), undefined)).toBeNull() expect(resolveCertificateReportSource(material({ assets: [a('preview', 'preview.png')] }), 'file-1')).toBeNull() expect(resolveCertificateReportSource(material({ assets: [a('zip', 'source.zip')] }), 'file-1')).toBeNull() }) describe('report originals through existing streaming downloader', () => { let dir: string beforeAll(async () => { dir = await mkdtemp(path.join(tmpdir(), 'certificate-api-test-')) }) afterAll(async () => { await rm(dir, { recursive: true, force: true }) }) test.each(['pdf', 'png'])('delivers exact %s bytes, Range and invalid-range response', async kind => { const bytes = kind === 'pdf' ? Buffer.from('%PDF-1.4\n' + 'report '.repeat(100)) : Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j5nUAAAAASUVORK5CYII=', 'base64') const file = path.join(dir, '原文件.' + kind); await writeFile(file, bytes) db.material.findUnique.mockResolvedValue(material({ assets: [a(kind === 'pdf' ? 'report-pdf' : 'report-image', '原文件.' + kind, file)] })) const url = generateDownloadUrl('https://sc.jiansun.vip', { materialId: id, assetId: 'file-1', downloadType: 'certificate-report-source' }) const full = await download(new NextRequest(url)) expect(full.status).toBe(200); expect(Buffer.from(await full.arrayBuffer())).toEqual(bytes) expect(full.headers.get('content-type')).toBe(kind === 'pdf' ? 'application/pdf' : 'image/png') expect(full.headers.get('cache-control')).toContain('no-store') const part = await download(new NextRequest(url, { headers: { Range: 'bytes=0-15' } })) expect(part.status).toBe(206); expect(Buffer.from(await part.arrayBuffer())).toEqual(bytes.subarray(0, 16)) const invalid = await download(new NextRequest(url, { headers: { Range: 'bytes=99999-' } })) expect(invalid.status).toBe(416) }) test('a previously issued report token cannot download a different asset type', async () => { db.material.findUnique.mockResolvedValue(material({ assets: [a('zip', 'all.zip')] })) const url = generateDownloadUrl('https://sc.jiansun.vip', { materialId: id, assetId: 'file-1', downloadType: 'certificate-report-source' }) expect((await download(new NextRequest(url))).status).toBe(404) }) })