143 lines
3.9 KiB
TypeScript
143 lines
3.9 KiB
TypeScript
|
|
/**
|
||
|
|
* 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);
|
||
|
|
});
|
||
|
|
});
|