diff --git a/scripts/generate-test-fixtures.mjs b/scripts/generate-test-fixtures.mjs index a5bbac2a..e821b37c 100644 --- a/scripts/generate-test-fixtures.mjs +++ b/scripts/generate-test-fixtures.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; /** * Generates the tiny media/document fixtures committed under tests/fixtures/. * Requires ffmpeg on PATH (or FFMPEG_PATH) and qpdf (or QPDF_PATH). @@ -154,20 +154,26 @@ await writeZip( ); // Encrypted PDF fixture derived from test-3page.pdf via qpdf (AES-256). -const qpdf = whichBin("QPDF_PATH", "qpdf"); -if (qpdf) { - const srcPdf = join(root, "tests/fixtures/document/valid/test-3page.pdf"); - const encPdf = join(root, "tests/fixtures/document/valid/encrypted.pdf"); - const qres = spawnSync(qpdf, [srcPdf, "--encrypt", "test123", "owner123", "256", "--", encPdf], { - stdio: "inherit", - }); - if (qres.status !== 0) { - console.error("qpdf encryption failed"); - process.exit(1); - } - console.log("encrypted.pdf written"); +// Skip if the file already exists -- qpdf encryption uses random IVs, so +// re-running produces different bytes and would break manifest hashes. +const encPdf = join(root, "tests/fixtures/document/valid/encrypted.pdf"); +if (existsSync(encPdf)) { + console.log("encrypted.pdf already exists; skipping (qpdf encryption is non-deterministic)"); } else { - console.warn("qpdf not found; skipping encrypted.pdf"); + const qpdf = whichBin("QPDF_PATH", "qpdf"); + if (qpdf) { + const srcPdf = join(root, "tests/fixtures/document/valid/test-3page.pdf"); + const qres = spawnSync(qpdf, [srcPdf, "--encrypt", "test123", "owner123", "256", "--", encPdf], { + stdio: "inherit", + }); + if (qres.status !== 0) { + console.error("qpdf encryption failed"); + process.exit(1); + } + console.log("encrypted.pdf written"); + } else { + console.warn("qpdf not found; skipping encrypted.pdf"); + } } // tiny.html fixture for html-to-pdf tests. diff --git a/tests/fixtures/document/formats/tiny.docx b/tests/fixtures/document/formats/tiny.docx index dc3da542..31e30c49 100644 Binary files a/tests/fixtures/document/formats/tiny.docx and b/tests/fixtures/document/formats/tiny.docx differ diff --git a/tests/fixtures/document/formats/tiny.epub b/tests/fixtures/document/formats/tiny.epub index 66a7c2fc..581beb3b 100644 Binary files a/tests/fixtures/document/formats/tiny.epub and b/tests/fixtures/document/formats/tiny.epub differ diff --git a/tests/fixtures/document/formats/tiny.xlsx b/tests/fixtures/document/formats/tiny.xlsx index c95b63e3..67274f80 100644 Binary files a/tests/fixtures/document/formats/tiny.xlsx and b/tests/fixtures/document/formats/tiny.xlsx differ diff --git a/tests/fixtures/gen-synthetic-content.mjs b/tests/fixtures/gen-synthetic-content.mjs index 78352097..7a39f355 100644 --- a/tests/fixtures/gen-synthetic-content.mjs +++ b/tests/fixtures/gen-synthetic-content.mjs @@ -14,7 +14,7 @@ // System deps on PATH: // ffmpeg -import { writeFileSync } from "node:fs"; +import { existsSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { execSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -22,13 +22,26 @@ import { createRequire } from "node:module"; const __dirname = dirname(fileURLToPath(import.meta.url)); const IMAGE_VALID = join(__dirname, "image/valid"); -const VIDEO_VALID = join(__dirname, "video/valid"); -const AUDIO_VALID = join(__dirname, "audio/valid"); const DOC_VALID = join(__dirname, "document/valid"); const require = createRequire(join(__dirname, "../../apps/api/package.json")); const sharp = require("sharp"); const QRCode = require("qrcode"); +const FORCE = process.argv.includes("--force"); + +// Write file only if it does not already exist (or --force is set). +// Committed synthetics must not be silently overwritten because +// encoder-version differences can change bytes and break manifest hashes. +function writeIfMissing(path, data, label) { + if (!FORCE && existsSync(path)) { + console.log(` SKIP (exists): ${label || path}`); + return false; + } + writeFileSync(path, data); + console.log(` ${label || path}`); + return true; +} + // ── Code 128B barcode encoder ────────────────────────────── // Standard Code 128 bar patterns (11 modules each, stop = 13 modules). @@ -215,10 +228,31 @@ function ff(args, label) { } } +// Like ff(), but skip if the output file already exists (unless --force). +function ffIfMissing(outPath, args, label) { + if (!FORCE && existsSync(outPath)) { + console.log(` SKIP (exists): ${label}`); + return; + } + ff(args, label); +} + // ── Main ─────────────────────────────────────────────────── +// +// Scoped outputs: +// - image/valid/ : QR codes, barcodes, OCR text, SVG logo, multi-face, animated GIF +// - document/valid/: alt-2page.pdf, multipage-6.pdf +// +// Deliberately EXCLUDED (committed real heroes, not synthetics): +// - video/valid/media-30s.mp4 (Big Buck Bunny CC-BY clip) +// - audio/valid/media-30s.wav (committed TTS/synthetic, hash-locked) +// - video/valid/speech-10s.mp4 (macOS TTS, committed) +// - audio/valid/speech-*.{wav,flac,ogg,m4a,aac,opus} (macOS TTS, committed) +// - video/valid/hero.{mov,webm,mkv,avi} (Big Buck Bunny CC-BY, committed) async function main() { - console.log("Regenerating Category A & B content fixtures...\n"); + console.log("Regenerating Category A & B content fixtures..."); + console.log(`Mode: ${FORCE ? "FORCE (overwrite all)" : "skip existing files (pass --force to overwrite)"}\n`); // ── QR codes (A) ── console.log("QR codes:"); @@ -226,28 +260,23 @@ async function main() { const qrPng = await QRCode.toBuffer(qrData, { width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" }, }); - writeFileSync(join(IMAGE_VALID, "qr-code.png"), qrPng); - console.log(" qr-code.png"); + writeIfMissing(join(IMAGE_VALID, "qr-code.png"), qrPng, "qr-code.png"); const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 }); - writeFileSync(join(IMAGE_VALID, "qr-code.svg"), qrSvg); - console.log(" qr-code.svg"); + writeIfMissing(join(IMAGE_VALID, "qr-code.svg"), qrSvg, "qr-code.svg"); const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer(); - writeFileSync(join(IMAGE_VALID, "qr-code.avif"), qrAvif); - console.log(" qr-code.avif"); + writeIfMissing(join(IMAGE_VALID, "qr-code.avif"), qrAvif, "qr-code.avif"); // ── Barcodes (A) ── console.log("Barcodes:"); const bcText = "SNAPOTTER-TEST-123"; const bcSvgStr = barcodeSvg(bcText, 699, 152); const bcPng = await sharp(Buffer.from(bcSvgStr)).png().toBuffer(); - writeFileSync(join(IMAGE_VALID, "barcode.png"), bcPng); - console.log(" barcode.png"); + writeIfMissing(join(IMAGE_VALID, "barcode.png"), bcPng, "barcode.png"); const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer(); - writeFileSync(join(IMAGE_VALID, "barcode.avif"), bcAvif); - console.log(" barcode.avif"); + writeIfMissing(join(IMAGE_VALID, "barcode.avif"), bcAvif, "barcode.avif"); // ── OCR text images (A) ── console.log("OCR text images:"); @@ -256,8 +285,7 @@ async function main() { The quick brown fox 12345 `; const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer(); - writeFileSync(join(IMAGE_VALID, "ocr-clean.png"), ocrEngPng); - console.log(" ocr-clean.png"); + writeIfMissing(join(IMAGE_VALID, "ocr-clean.png"), ocrEngPng, "ocr-clean.png"); const jpText = "日本語の表記においては,漢字や仮名だけで" + @@ -276,73 +304,61 @@ async function main() { ${lines.map((line, i) => `${line}`).join("\n ")} `; const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer(); - writeFileSync(join(IMAGE_VALID, "ocr-japanese.png"), jpPng); - console.log(" ocr-japanese.png"); + writeIfMissing(join(IMAGE_VALID, "ocr-japanese.png"), jpPng, "ocr-japanese.png"); - // ── PDFs (A) ── + // ── PDFs (A: deterministic generator, safe to overwrite) ── console.log("PDFs:"); - writeFileSync(join(DOC_VALID, "alt-2page.pdf"), generatePdf(2)); - console.log(" alt-2page.pdf"); - writeFileSync(join(DOC_VALID, "multipage-6.pdf"), generatePdf(6)); - console.log(" multipage-6.pdf"); + writeIfMissing(join(DOC_VALID, "alt-2page.pdf"), generatePdf(2), "alt-2page.pdf"); + writeIfMissing(join(DOC_VALID, "multipage-6.pdf"), generatePdf(6), "multipage-6.pdf"); // ── SVG logo (B: replaces ConvertICO brand) ── console.log("SVG logo (replacing ConvertICO brand):"); - writeFileSync(join(IMAGE_VALID, "svg-logo.svg"), projectSvg()); - console.log(" svg-logo.svg"); + writeIfMissing(join(IMAGE_VALID, "svg-logo.svg"), projectSvg(), "svg-logo.svg"); // ── Multi-face placeholder (B: replaces Shutterstock photo) ── console.log("Multi-face placeholder (replacing Shutterstock stock photo):"); const mfSvg = multiFaceSvg(433, 280); const mfWebp = await sharp(Buffer.from(mfSvg)).webp({ quality: 80 }).toBuffer(); - writeFileSync(join(IMAGE_VALID, "multi-face.webp"), mfWebp); - console.log(" multi-face.webp"); + writeIfMissing(join(IMAGE_VALID, "multi-face.webp"), mfWebp, "multi-face.webp"); // ── Animated GIF (B: replaces copyrighted Simpsons clip) ── console.log("Animated GIF (replacing copyrighted Simpsons clip):"); const gifOut = join(IMAGE_VALID, "animated-simpsons.gif"); - ff( + ffIfMissing( + gifOut, `-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`, "animated-simpsons.gif", ); // ── Synthetic audio/video (A) ── - console.log("Synthetic audio/video:"); + // NOTE: media-30s.mp4 and media-30s.wav are no longer generated here. + // media-30s.mp4 is now a real Big Buck Bunny CC-BY hero clip (committed). + // media-30s.wav is a committed synthetic whose hash is locked in manifest.json. + // Regenerating either would clobber the committed content with different bytes. + console.log("Synthetic audio/video (metadata-tagged synthetics only):"); - ff( + const audioTagsOut = join(__dirname, "audio/valid/audio-with-tags.mp3"); + ffIfMissing( + audioTagsOut, `-f lavfi -i "sine=frequency=440:duration=1.2:sample_rate=8000" ` + `-c:a libmp3lame -b:a 64k -ar 8000 -ac 1 ` + `-metadata title="Test Song" -metadata artist="Test Artist" ` + `-metadata album="Test Album" -metadata date="2026" ` + `-metadata genre="Electronic" -metadata track="1/10" ` + - `-y "${join(AUDIO_VALID, "audio-with-tags.mp3")}"`, + `-y "${audioTagsOut}"`, "audio-with-tags.mp3", ); - ff( + const videoMetaOut = join(__dirname, "video/valid/video-with-meta.mp4"); + ffIfMissing( + videoMetaOut, `-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` + `-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` + - `-y "${join(VIDEO_VALID, "video-with-meta.mp4")}"`, + `-y "${videoMetaOut}"`, "video-with-meta.mp4", ); - ff( - `-f lavfi -i "testsrc2=duration=30:size=640x360:rate=24" ` + - `-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` + - `-c:v libx264 -pix_fmt yuv420p -preset ultrafast ` + - `-c:a aac -b:a 128k -ac 1 -movflags +faststart ` + - `-y "${join(VIDEO_VALID, "media-30s.mp4")}"`, - "media-30s.mp4", - ); - - ff( - `-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` + - `-ac 1 -c:a pcm_s16le ` + - `-y "${join(AUDIO_VALID, "media-30s.wav")}"`, - "media-30s.wav", - ); - - console.log("\nDone. Run gen-manifest.mjs to re-stamp hashes."); + console.log("\nDone. Run gen-manifest.mjs to re-stamp hashes if any files were regenerated."); } main().catch((e) => { diff --git a/tests/fixtures/manifest.json b/tests/fixtures/manifest.json index 47fab169..4abd19e6 100644 --- a/tests/fixtures/manifest.json +++ b/tests/fixtures/manifest.json @@ -72,6 +72,14 @@ "sha256": "be8a827f8c4afee32b2c5d4339f039e3c24887284f220c3bf849210095899e75", "bytes": 854 }, + { + "path": "document/valid/encrypted.pdf", + "sourceUrl": "generate-test-fixtures.mjs (qpdf AES-256 encryption of test-3page.pdf)", + "license": "CC0", + "toolsServed": [], + "sha256": "fd34ac2a0a5149eb0906dab62950d479e38e78e21aa161737b65c2c493e763bb", + "bytes": 3069 + }, { "path": "document/valid/multipage-6.pdf", "sourceUrl": "gen-synthetic-content.mjs", @@ -88,6 +96,14 @@ "sha256": "28d33f9f30a450943d59489345bf475b42ca0f73deffeb883df1d117d97e5d90", "bytes": 1049673 }, + { + "path": "document/valid/test-3page.pdf", + "sourceUrl": "Project-generated minimal 3-page PDF test fixture", + "license": "CC0", + "toolsServed": [], + "sha256": "d3caf5c109f742a3c7e1980f88c1bef3fc5c7f57d4cc08e3322ef471c19bcdb7", + "bytes": 2201 + }, { "path": "image/valid/animated-simpsons.gif", "sourceUrl": "gen-synthetic-content.mjs (ffmpeg testsrc, replaces copyrighted Simpsons clip)", @@ -96,6 +112,22 @@ "sha256": "738a6c04c1744c32bd9889372a4754dcb2d7af84586a33c9aa67e09ada8bdeb8", "bytes": 193823 }, + { + "path": "image/valid/animated.gif", + "sourceUrl": "Project-generated minimal animated GIF (2-frame, tiny)", + "license": "CC0", + "toolsServed": [], + "sha256": "96842e54f8aa6b34ef4923c6fe76c013aadcab4018da410008dc42eee3f2b545", + "bytes": 615 + }, + { + "path": "image/valid/animated.webp", + "sourceUrl": "Project-generated minimal animated WebP (tiny)", + "license": "CC0", + "toolsServed": [], + "sha256": "1cdbbc5423fa24eb0162beb90f454cfd75fec599563ecf9897a6fca05c8a9015", + "bytes": 392 + }, { "path": "image/valid/barcode.avif", "sourceUrl": "gen-synthetic-content.mjs (Code 128B via Sharp)", @@ -232,6 +264,14 @@ "sha256": "04a993def90ffb471cbb0b4bbe46bd2aa9a2d344a9ab7ad187dc22f5feca23ed", "bytes": 368937 }, + { + "path": "image/valid/sample-photo.jpg", + "sourceUrl": "Project-generated sample JPEG photo (used as hostile-fixture source)", + "license": "CC0", + "toolsServed": [], + "sha256": "4eda0de1008fd816d1412f7380a2eec96b3bfecec1bf787ea21771b33a74c6af", + "bytes": 129438 + }, { "path": "image/valid/stress-large.jpg", "sourceUrl": "Unknown; added in babca4cf (test coverage expansion). Samsung Galaxy S24 Ultra photo (EXIF 2026-04-11), Muay Thai event.", @@ -248,6 +288,78 @@ "sha256": "dae7668c0db8688e93cd7d6fedfd7f7121c3bfa563eee19e4492bc0dbfc73a0b", "bytes": 1426 }, + { + "path": "image/valid/test-100x100.jpg", + "sourceUrl": "Project-generated minimal 100x100 JPEG test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "4749f4af300d0bdcee870efdb9299125a132c4770dc2aaa885d194fe58c31758", + "bytes": 342 + }, + { + "path": "image/valid/test-100x100.svg", + "sourceUrl": "Project-generated minimal 100x100 SVG test fixture", + "license": "CC0", + "toolsServed": [], + "sha256": "fb5616fc72722429294f4f23c76c8bac56bb0b2e4a4dac163d78703cd71269da", + "bytes": 195 + }, + { + "path": "image/valid/test-200x150.heic", + "sourceUrl": "Project-generated minimal 200x150 HEIC test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "5566100fbb0da3892ed14cbacf59c66b67e7c1279bd068145bd0abda2e97e66d", + "bytes": 782 + }, + { + "path": "image/valid/test-200x150.png", + "sourceUrl": "Project-generated minimal 200x150 PNG test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "a645232af22f8ffa0685cda619d25eefa8baf2fd9efe09c88f5a7825901232f8", + "bytes": 544 + }, + { + "path": "image/valid/test-50x50.webp", + "sourceUrl": "Project-generated minimal 50x50 WebP test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "aa6de3f8a94a0715707fb6966a7c63b52ba1ae11faf2fc2e9be9fa3171647857", + "bytes": 128 + }, + { + "path": "image/valid/test-portrait.heic", + "sourceUrl": "Project-generated minimal portrait HEIC test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "5566100fbb0da3892ed14cbacf59c66b67e7c1279bd068145bd0abda2e97e66d", + "bytes": 782 + }, + { + "path": "image/valid/test-portrait.jpg", + "sourceUrl": "Project-generated minimal portrait JPEG test fixture (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "4749f4af300d0bdcee870efdb9299125a132c4770dc2aaa885d194fe58c31758", + "bytes": 342 + }, + { + "path": "image/valid/test-scene.png", + "sourceUrl": "Project-generated scene PNG test fixture (Sharp SVG render)", + "license": "CC0", + "toolsServed": [], + "sha256": "0ec225bdcf444a305c2a5d0394e65d9a7e4119e3904f003f21c86dd4390ba8e4", + "bytes": 5496 + }, + { + "path": "image/valid/test-with-exif.jpg", + "sourceUrl": "Project-generated JPEG with synthetic EXIF GPS/camera data (Sharp)", + "license": "CC0", + "toolsServed": [], + "sha256": "6074b156e1c1d281fcbf43f28785adcbaed9c84234b6c98c7df16198193613ab", + "bytes": 758 + }, { "path": "image/valid/watermark.jpg", "sourceUrl": "Unknown; added in babca4cf (test coverage expansion). Landscape photo with text watermark overlay.",