import { test } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import sharp from "sharp"; import Fastify from "fastify"; import cookie from "@fastify/cookie"; import { openDb } from "../src/db.js"; import { loadConfig, type AppConfig } from "../src/config.js"; import { registerRoutes } from "../src/routes.js"; import { createUser, login } from "../src/auth.js"; import { claimNextJob, getJob } from "../src/domain/jobs.js"; import { enqueueFabricImport, runFabricImport } from "../src/domain/fabricImport.js"; import { parseFabricCollectionPage, detectFabricVendor, FabricParseError } from "../src/import/fabricParser.js"; import { parseDavisCollectionPage } from "../src/import/fabricDavis.js"; import type { FetchedSource } from "../src/media/sourceFetch.js"; const FIXTURE = fs.readFileSync(path.join(import.meta.dirname, "fixtures", "davis-collection.html"), "utf8"); const DAVIS_URL = "https://www.davis.pl/kolekcja/tilia/"; const SIC_URL = "https://sic.com.pl/oferta/tkaniny/nova/"; const SIC_HTML = `

Kolekcja NOVA

karta `; // --- parser Davis: czyste HTML, zero sieci ----------------------------------- test("davis parser: nazwa, parametry, odcienie, karta PDF z PIM", () => { const parsed = parseDavisCollectionPage(FIXTURE, DAVIS_URL); assert.equal(parsed.code, "tilia"); assert.equal(parsed.name, "TILIA"); assert.equal(parsed.producer, "Davis"); // parametry techniczne → kanoniczne klucze params_json assert.deepEqual(parsed.params.grammage_g_m2, { value: 420, tolerance: "±5%" }); assert.equal(parsed.params.composition, "100% poliester"); assert.deepEqual(parsed.params.usable_width_cm, { min: 140 }); assert.deepEqual(parsed.params.martindale_cycles, { min: 90000, max: 100000 }); assert.deepEqual(parsed.params.seam_slippage_mm, { warp: { min: 1.9, max: 2.1 }, weft: { min: 1.5, max: 1.7 } }); assert.equal(parsed.params.pilling_grade, 4); assert.deepEqual(parsed.params.rubbing_fastness_dry, { light: "4-5", medium: "5", dark: "4-5" }); assert.deepEqual(parsed.params.rubbing_fastness_wet, { light: "5", medium: "5", dark: "5" }); assert.equal(parsed.params.pet_friendly, true); assert.equal(parsed.params.water_repellent, true); // odcienie: pełnowymiarowe próbki, czyste kody assert.deepEqual(parsed.shades.map((s) => s.code), ["01", "03", "83", "100"]); assert.equal(parsed.shades[0]!.imageUrl, "https://www.davis.pl/wp-content/uploads/2023/06/tilia-01_TILIA-01.jpg"); assert.ok(parsed.shades.every((s) => !s.imageUrl.includes("150x150")), "tylko pełnowymiarowe próbki, nie miniatury"); // karta PDF z PIM (protokół-względny URL → https) assert.equal(parsed.cardUrl, "https://pim.davis.pl/fabrics/generate-card/cat/207.pdf?lang=pl"); // błędy assert.throws(() => parseDavisCollectionPage("nic", DAVIS_URL), FabricParseError); assert.throws(() => parseDavisCollectionPage(FIXTURE, "https://example.com/x/"), /davis\.pl/); }); // --- router parserów: wybór strategii wg hosta -------------------------------- test("router: domena URL wybiera parser SIC lub Davis; inny host = błąd", () => { assert.equal(detectFabricVendor(SIC_URL), "sic"); assert.equal(detectFabricVendor(DAVIS_URL), "davis"); assert.throws(() => detectFabricVendor("https://bobochicparis.com/fr/x"), /Brak adaptera/); const sic = parseFabricCollectionPage(SIC_HTML, SIC_URL); assert.equal(sic.producer, "SIC"); assert.equal(sic.code, "nova"); assert.equal(sic.shades[0]!.imageUrl, "https://sic.com.pl/wp-content/uploads/2024/05/nova-1-Ivory.jpg"); const davis = parseFabricCollectionPage(FIXTURE, DAVIS_URL); assert.equal(davis.producer, "Davis"); assert.equal(davis.shades.length, 4); // błąd adaptera SIC przepakowywany do wspólnego FabricParseError assert.throws(() => parseFabricCollectionPage("nic", SIC_URL), FabricParseError); assert.throws(() => parseFabricCollectionPage(FIXTURE, "https://unknown.example/x"), FabricParseError); }); // --- job import_fabric z fixture Davis ----------------------------------------- const JPEG = (seed: number) => sharp({ create: { width: 32, height: 32, channels: 3, background: { r: 60 + (seed % 180), g: 90, b: 80 } } }).jpeg().toBuffer(); const PDF = Buffer.from("%PDF-1.4\n1 0 obj<>endobj\ntrailer<>\n%%EOF\n"); async function fixture(t: { after: (fn: () => Promise) => void }) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vilmal-davis-")); const cfg = loadConfig({ env: "test", dataDir: dir, dbPath: path.join(dir, "test.db"), mediaDir: path.join(dir, "media"), originalsDir: path.join(dir, "media/originals"), derivedDir: path.join(dir, "media/derived") }); const db = openDb(cfg.dbPath); const app = Fastify(); await app.register(cookie); registerRoutes(app, db, cfg); t.after(async () => { await app.close(); db.close(); fs.rmSync(dir, { recursive: true, force: true }); }); createUser(db, { login: "op", password: "test-only", role: "operator", displayName: "Operator" }); const operator = db.prepare("SELECT id FROM users WHERE login='op'").get() as { id: string }; const cookies = { vilmal_session: login(db, "op", "test-only")!.sessionId }; return { db, app, cfg, cookies, actor: operator.id }; } function davisFetcher(): (url: string, kind: string, signal?: AbortSignal) => Promise { return async (url, kind) => { if (kind === "fabric_page") return { buffer: Buffer.from(FIXTURE), mime: "text/html", finalUrl: url, redirects: [], fetchedAt: "2026-09-24T00:00:00.000Z" }; if (kind === "fabric_card") return { buffer: PDF, mime: "application/pdf", finalUrl: url, redirects: [], fetchedAt: "2026-09-24T00:00:00.000Z" }; return { buffer: await JPEG([...url].reduce((a, c) => a + c.charCodeAt(0), 0)), mime: "image/jpeg", finalUrl: url, redirects: [], fetchedAt: "2026-09-24T00:00:00.000Z" }; }; } test("fabric import: strona Davis → tkanina z parametrami + odcienie + próbki + karta PIM", async (t) => { const f = await fixture(t); // enqueue akceptuje davis.pl (cofnięty fast-fail non-SIC) const res = enqueueFabricImport(f.db, { url: DAVIS_URL, idempotencyKey: "dv1", actor: f.actor }); assert.equal(res.created, true); claimNextJob(f.db); await runFabricImport(f.db, f.cfg, res.import.job_id, davisFetcher()); const job = getJob(f.db, res.import.job_id)!; assert.equal(job.status, "done"); const result = JSON.parse(job.result_json!) as { code: string; shadesParsed: number; swatchesStored: number; cardStored: boolean; warnings: string[] }; assert.equal(result.code, "tilia"); assert.equal(result.shadesParsed, 4); assert.equal(result.swatchesStored, 4); assert.equal(result.cardStored, true); const fabric = f.db.prepare("SELECT * FROM fabrics WHERE code='tilia'").get() as { name: string; producer: string; params_json: string; card_path: string | null }; assert.equal(fabric.name, "TILIA"); assert.equal(fabric.producer, "Davis"); assert.ok(fabric.card_path, "karta PIM podpięta"); const params = JSON.parse(fabric.params_json) as Record; assert.deepEqual(params.grammage_g_m2, { value: 420, tolerance: "±5%" }); assert.equal(params.source, "davis.pl"); const shades = f.db.prepare("SELECT code, official_image_path FROM fabric_shades WHERE fabric_id=(SELECT id FROM fabrics WHERE code='tilia') ORDER BY code").all() as Array<{ code: string; official_image_path: string | null }>; assert.deepEqual(shades.map((s) => s.code), ["01", "03", "100", "83"]); assert.ok(shades.every((s) => s.official_image_path), "każdy odcień ma próbkę z source_assets"); // provenance: próbki z davis.pl + karta z pim.davis.pl jako source_assets const assets = f.db.prepare("SELECT role, source_url FROM source_assets").all() as Array<{ role: string; source_url: string | null }>; assert.equal(assets.filter((a) => a.role === "fabric_reference").length, 4); assert.equal(assets.filter((a) => a.role === "source_document").length, 1); assert.ok(assets.every((a) => a.source_url?.includes("davis.pl"))); });