Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
/**
|
|
* Pure-function tests for the logo sharp pipeline (no DB / storage).
|
|
* The `setPortLogo` write-path is exercised by integration tests + Playwright.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import sharp from 'sharp';
|
|
|
|
import { processLogoUpload } from '@/lib/services/logo.service';
|
|
|
|
async function makePng(
|
|
w: number,
|
|
h: number,
|
|
color: { r: number; g: number; b: number; alpha: number } = { r: 255, g: 0, b: 0, alpha: 1 },
|
|
) {
|
|
return sharp({ create: { width: w, height: h, channels: 4, background: color } })
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
describe('processLogoUpload - sharp pipeline', () => {
|
|
it('accepts a healthy PNG with alpha and resizes if needed', async () => {
|
|
const buf = await makePng(2400, 800);
|
|
const result = await processLogoUpload(buf);
|
|
expect(result.originalFormat).toBe('png');
|
|
expect(result.finalDimensions.width).toBeLessThanOrEqual(1200);
|
|
expect(result.finalDimensions.height).toBeLessThanOrEqual(1200);
|
|
expect(result.pngBuffer.subarray(1, 4).toString('ascii')).toBe('PNG');
|
|
expect(result.warnings).toContain('trimmed');
|
|
expect(result.warnings).toContain('resized');
|
|
});
|
|
|
|
it('rejects undersized images', async () => {
|
|
const buf = await makePng(100, 100);
|
|
await expect(processLogoUpload(buf)).rejects.toThrow(/too small/i);
|
|
});
|
|
|
|
it('rejects empty buffers', async () => {
|
|
await expect(processLogoUpload(Buffer.alloc(0))).rejects.toThrow(/empty/i);
|
|
});
|
|
|
|
it('rejects buffers that exceed the raw size cap', async () => {
|
|
// 6 MB of zero bytes - fails the raw size cap before sharp parses.
|
|
const buf = Buffer.alloc(6 * 1024 * 1024);
|
|
await expect(processLogoUpload(buf)).rejects.toThrow(/MB/i);
|
|
});
|
|
|
|
it('rejects non-image bytes', async () => {
|
|
const buf = Buffer.from('this is not an image at all');
|
|
await expect(processLogoUpload(buf)).rejects.toThrow(/supported image format/i);
|
|
});
|
|
|
|
it('rejects out-of-bounds crop coordinates', async () => {
|
|
const buf = await makePng(800, 800);
|
|
await expect(processLogoUpload(buf, { x: 0, y: 0, width: 1000, height: 1000 })).rejects.toThrow(
|
|
/out of image bounds/i,
|
|
);
|
|
});
|
|
|
|
it('accepts an in-bounds crop', async () => {
|
|
const buf = await makePng(800, 800);
|
|
const result = await processLogoUpload(buf, { x: 100, y: 100, width: 500, height: 500 });
|
|
expect(result.finalDimensions.width).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('rasterizes SVG input to PNG', async () => {
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
|
|
<rect width="400" height="400" fill="#1d4ed8"/>
|
|
<circle cx="200" cy="200" r="100" fill="#ffffff"/>
|
|
</svg>`;
|
|
const buf = Buffer.from(svg, 'utf8');
|
|
const result = await processLogoUpload(buf);
|
|
expect(result.originalFormat).toBe('svg');
|
|
expect(result.warnings).toContain('svg-rasterized');
|
|
// Output is PNG even though input was SVG.
|
|
expect(result.pngBuffer.subarray(1, 4).toString('ascii')).toBe('PNG');
|
|
});
|
|
|
|
it('rejects SVG with embedded script', async () => {
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
|
|
<script>alert('xss')</script>
|
|
<rect width="400" height="400" fill="#1d4ed8"/>
|
|
</svg>`;
|
|
const buf = Buffer.from(svg, 'utf8');
|
|
await expect(processLogoUpload(buf)).rejects.toThrow(/disallowed nodes/i);
|
|
});
|
|
|
|
it('rejects SVG with external href', async () => {
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
|
|
<image href="https://evil.example.com/x.png" width="400" height="400"/>
|
|
</svg>`;
|
|
const buf = Buffer.from(svg, 'utf8');
|
|
await expect(processLogoUpload(buf)).rejects.toThrow(/disallowed nodes/i);
|
|
});
|
|
|
|
it('flags JPEG sources with no alpha', async () => {
|
|
const buf = await sharp({
|
|
create: { width: 1200, height: 1200, channels: 3, background: { r: 200, g: 50, b: 50 } },
|
|
})
|
|
.jpeg({ quality: 80 })
|
|
.toBuffer();
|
|
const result = await processLogoUpload(buf);
|
|
expect(result.warnings).toContain('jpeg-source');
|
|
expect(result.warnings).toContain('no-alpha');
|
|
});
|
|
});
|