/** * Phase 7 — validator-level guarantees for the send-out flow. * * §14.7 mitigation: recipient typo (the strict email regex is the first * line of defense; the confirmation modal is the second). */ import { describe, expect, it } from 'vitest'; import { sendBerthPdfSchema, sendBrochureSchema, previewBodySchema, listSendsQuerySchema, } from '@/lib/validators/document-sends'; import { createBrochureSchema, registerBrochureVersionSchema } from '@/lib/validators/brochures'; describe('sendBerthPdfSchema', () => { it('requires either clientId or email', () => { const r = sendBerthPdfSchema.safeParse({ berthId: 'b1', recipient: { interestId: 'i1' }, }); expect(r.success).toBe(false); }); it('accepts clientId-only recipient', () => { const r = sendBerthPdfSchema.safeParse({ berthId: 'b1', recipient: { clientId: 'c1' }, }); expect(r.success).toBe(true); }); it('rejects an obviously bad email', () => { const r = sendBerthPdfSchema.safeParse({ berthId: 'b1', recipient: { email: 'not an email' }, }); expect(r.success).toBe(false); }); it('caps custom body length at 50KB', () => { const r = sendBerthPdfSchema.safeParse({ berthId: 'b1', recipient: { clientId: 'c1' }, customBodyMarkdown: 'x'.repeat(60_000), }); expect(r.success).toBe(false); }); }); describe('sendBrochureSchema', () => { it('allows brochureId to be omitted (defaults at service level)', () => { const r = sendBrochureSchema.safeParse({ recipient: { clientId: 'c1' }, }); expect(r.success).toBe(true); }); }); describe('previewBodySchema', () => { it('requires documentKind', () => { const r = previewBodySchema.safeParse({ recipient: { clientId: 'c1' } }); expect(r.success).toBe(false); }); it('accepts a minimal preview payload', () => { const r = previewBodySchema.safeParse({ documentKind: 'berth_pdf', recipient: { clientId: 'c1' }, berthId: 'b1', }); expect(r.success).toBe(true); }); }); describe('listSendsQuerySchema', () => { it('coerces limit from string', () => { const r = listSendsQuerySchema.safeParse({ limit: '50' }); expect(r.success).toBe(true); if (r.success) expect(r.data.limit).toBe(50); }); it('rejects out-of-range limit', () => { const r = listSendsQuerySchema.safeParse({ limit: '99999' }); expect(r.success).toBe(false); }); }); describe('createBrochureSchema', () => { it('requires a non-empty label', () => { const r = createBrochureSchema.safeParse({ label: ' ' }); expect(r.success).toBe(false); }); it('caps label at 120 chars', () => { const r = createBrochureSchema.safeParse({ label: 'a'.repeat(200) }); expect(r.success).toBe(false); }); }); describe('registerBrochureVersionSchema', () => { it('rejects path-traversal in storageKey', () => { const r = registerBrochureVersionSchema.safeParse({ storageKey: '../etc/passwd', fileName: 'b.pdf', fileSizeBytes: 100, contentSha256: 'a'.repeat(64), }); expect(r.success).toBe(false); }); it('rejects malformed sha256', () => { const r = registerBrochureVersionSchema.safeParse({ storageKey: 'port/brochures/abc/x.pdf', fileName: 'b.pdf', fileSizeBytes: 100, contentSha256: 'NOTHEX', }); expect(r.success).toBe(false); }); it('rejects upload over 100MB', () => { const r = registerBrochureVersionSchema.safeParse({ storageKey: 'port/brochures/abc/x.pdf', fileName: 'b.pdf', fileSizeBytes: 200 * 1024 * 1024, contentSha256: 'a'.repeat(64), }); expect(r.success).toBe(false); }); it('accepts a valid payload', () => { const r = registerBrochureVersionSchema.safeParse({ storageKey: 'port/brochures/abc/x.pdf', fileName: 'b.pdf', fileSizeBytes: 1024, contentSha256: 'a'.repeat(64), }); expect(r.success).toBe(true); }); });