/** * Collects a deterministic fixture snapshot from the live testing backend. * * Usage: * npm run rig:collect (collects snapshot fixtures from live backend) * npm run rig:reference (captures reference screenshots) * * Reads ../dashback/.env for GOOGLE_CLIENT_ID (stored as googleClientId in the * snapshot) and mints a JWT for user id 1 with the backend's own env + signing * library, then GETs every endpoint the dummy backend needs to serve. * Writes test/fixtures/snapshot.json (plus images/ for binary thumbs). * * The live backend must run at http://localhost:4208 (npm run dev in dashback). */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FRONT_ROOT = path.resolve(__dirname, '..'); const BACK_ROOT = path.resolve(FRONT_ROOT, '../dashback'); const FIXTURE_DIR = path.join(FRONT_ROOT, 'test', 'fixtures'); const IMAGE_DIR = path.join(FIXTURE_DIR, 'images'); const BACKEND = process.env.RIG_BACKEND ?? 'http://localhost:4208'; function parseDotEnv(text) { const out = {}; for (const line of text.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); } return out; } async function mintToken() { const env = await import(path.join(BACK_ROOT, 'src/config/env.js')).then( (m) => m.default, ); const { default: jwt } = await import( path.join(BACK_ROOT, 'node_modules/jsonwebtoken/index.js') ); return { token: jwt.sign({ id: 1 }, env.jwtSecret, { expiresIn: '1d' }), googleClientId: env.googleClientId ?? null, }; } async function get(token, p, { binary = false } = {}) { const res = await fetch(`${BACKEND}${p}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`GET ${p} → ${res.status} ${text.slice(0, 200)}`); } if (binary) return Buffer.from(await res.arrayBuffer()); return res.json(); } async function getOptional(token, p) { try { return await get(token, p); } catch (err) { console.warn(`[rig:collect] skip ${p}: ${err.message}`); return null; } } const KNOWN_SKU = 'ART-000001'; const KNOWN_DELIVERY = 6; const KNOWN_ENTITY = 1; async function main() { fs.mkdirSync(FIXTURE_DIR, { recursive: true }); fs.mkdirSync(IMAGE_DIR, { recursive: true }); const snapshotFile = path.join(FIXTURE_DIR, 'snapshot.json'); try { await fetch(`${BACKEND}/api/auth/me`, { signal: AbortSignal.timeout(1500) }); } catch { if (fs.existsSync(snapshotFile)) { console.log(`[rig:collect] Live backend (${BACKEND}) not running; using existing ${snapshotFile}`); return; } throw new Error(`Live backend (${BACKEND}) not running and ${snapshotFile} does not exist.`); } // Load existing snapshot if available for fallbacks let existingSnapshot = null; try { if (fs.existsSync(snapshotFile)) { existingSnapshot = JSON.parse(fs.readFileSync(snapshotFile, 'utf8')); } } catch { // Ignore } // GOOGLE_CLIENT_ID comes from the testing backend env (../dashback/.env). let dotenvClientId = null; try { const raw = fs.readFileSync(path.join(BACK_ROOT, '.env'), 'utf8'); dotenvClientId = parseDotEnv(raw).GOOGLE_CLIENT_ID ?? null; } catch { console.warn('[rig:collect] ../dashback/.env not readable, using backend env module'); } const { token, googleClientId } = await mintToken(); const me = await get(token, '/api/auth/me'); const user = me.user; const companyId = user.selectedCompanyId; const snapshot = { meta: { collectedAt: new Date().toISOString(), backend: BACKEND, companyId, googleClientId: dotenvClientId ?? googleClientId, }, auth: { me: { user, googleClientId: dotenvClientId ?? googleClientId }, companies: (await get(token, '/api/company')).companies ?? [], }, company: {}, catalog: {}, parties: {}, documents: {}, misc: {}, images: {}, }; // Company detail is fetched per-id via GET /api/company (list) — the // frontend's companyApi.getById() filters the list client-side. snapshot.company.detail = snapshot.auth.companies.find((c) => c.id === companyId) ?? null; snapshot.company.settings = (await getOptional(token, '/api/company/setting'))?.settings ?? []; // Catalog const tree = await get(token, '/api/category/tree'); snapshot.catalog.tree = tree; const categoryIds = (tree.tree ?? []).map((t) => t.id); snapshot.catalog.articlesByCategory = {}; for (const id of categoryIds) { const data = await getOptional(token, `/api/category/${id}/articles`); snapshot.catalog.articlesByCategory[String(id)] = data?.articles ?? []; } const allArticles = Object.values(snapshot.catalog.articlesByCategory).flat(); const targetSku = allArticles.find((a) => a.sku === KNOWN_SKU)?.sku || allArticles[0]?.sku || KNOWN_SKU; snapshot.catalog.articleDetail = (await getOptional(token, `/api/article/${encodeURIComponent(targetSku)}`))?.article ?? existingSnapshot?.catalog?.articleDetail ?? null; snapshot.catalog.articleHistory = (await getOptional(token, `/api/article/${encodeURIComponent(targetSku)}/history?limit=50`))?.history ?? existingSnapshot?.catalog?.articleHistory ?? []; snapshot.catalog.articleSuppliers = (await getOptional(token, `/api/article/${encodeURIComponent(targetSku)}/supplier`))?.suppliers ?? existingSnapshot?.catalog?.articleSuppliers ?? []; snapshot.catalog.brands = (await getOptional(token, '/api/brand'))?.brands ?? []; snapshot.catalog.units = (await getOptional(token, '/api/unit'))?.units ?? []; snapshot.catalog.warehouses = (await getOptional(token, '/api/warehouse'))?.warehouses ?? []; snapshot.catalog.attributes = (await getOptional(token, '/api/attributename'))?.attributenames ?? []; snapshot.catalog.properties = (await getOptional(token, '/api/propertyname'))?.propertynames ?? []; // Parties snapshot.parties.suppliers = (await getOptional(token, '/api/supplier'))?.suppliers ?? []; snapshot.parties.entities = (await getOptional(token, '/api/entity'))?.entities ?? []; snapshot.parties.entityDetail = (await getOptional(token, `/api/entity/${KNOWN_ENTITY}`))?.entity ?? null; snapshot.parties.entityAddresses = (await getOptional(token, `/api/entity/${KNOWN_ENTITY}/address`))?.addresses ?? []; snapshot.parties.entityContacts = (await getOptional(token, `/api/entity/${KNOWN_ENTITY}/contact`))?.contacts ?? []; snapshot.parties.customergroups = (await getOptional(token, '/api/customergroup'))?.customergroups ?? []; // Documents snapshot.documents.deliveries = (await getOptional(token, '/api/delivery'))?.deliveries ?? []; snapshot.documents.deliveryDetail = (await getOptional(token, `/api/delivery/${KNOWN_DELIVERY}`))?.delivery ?? null; snapshot.documents.quotes = (await getOptional(token, '/api/quote'))?.quotes ?? []; snapshot.documents.orders = (await getOptional(token, '/api/order'))?.orders ?? []; snapshot.documents.numbers = (await getOptional(token, '/api/number'))?.numbers ?? []; // Misc snapshot.misc.posterminals = (await getOptional(token, '/api/posterminal'))?.posterminals ?? []; snapshot.misc.printers = (await getOptional(token, '/api/printer'))?.printers ?? []; snapshot.misc.labeltemplates = (await getOptional(token, '/api/labeltemplate'))?.labeltemplates ?? []; snapshot.misc.vfs = (await getOptional(token, '/api/vfs'))?.files ?? []; snapshot.misc.globalHistory = (await getOptional(token, '/api/history?limit=50'))?.history ?? (await getOptional(token, '/api/history?limit=50'))?.rows ?? []; snapshot.misc.articleHistoryGrid = (await getOptional(token, '/api/article/history?limit=50'))?.history ?? (await getOptional(token, '/api/article/history?limit=50'))?.rows ?? []; // Binary thumbs: collect every fullHash referenced by articles/categories/brands // so /api/thumb/:hash and /api/image/:hash serve byte-identical images. // Backend fullHash is an 8-char alphanumeric string (see dashback pic.js). const hashes = new Set(); const collectHash = (h) => { if (typeof h === 'string' && /^[A-Za-z0-9]{8}$/.test(h)) hashes.add(h); }; for (const list of Object.values(snapshot.catalog.articlesByCategory)) { for (const a of list) collectHash(a.fullHash); } for (const t of tree.tree ?? []) collectHash(t.fullHash); for (const b of snapshot.catalog.brands) { collectHash(b.fullHash); for (const p of b.pics ?? []) collectHash(p.fullHash); } for (const p of snapshot.catalog.articleDetail?.photos ?? []) collectHash(p.fullHash); let imageBytes = 0; for (const hash of hashes) { const dest = path.join(IMAGE_DIR, `${hash}.avif`); if (fs.existsSync(dest) && fs.statSync(dest).size > 0) { snapshot.images[hash] = `${hash}.avif`; continue; } try { const buf = await get(token, `/api/thumb/${hash}`, { binary: true }); fs.writeFileSync(dest, buf); snapshot.images[hash] = `${hash}.avif`; imageBytes += buf.length; } catch (err) { console.warn(`[rig:collect] thumb ${hash.slice(0, 12)}…: ${err.message}`); } } fs.writeFileSync( path.join(FIXTURE_DIR, 'snapshot.json'), `${JSON.stringify(snapshot, null, 2)}\n`, ); const counts = { categories: categoryIds.length, categoryArticles: Object.values(snapshot.catalog.articlesByCategory).reduce( (n, l) => n + l.length, 0, ), brands: snapshot.catalog.brands.length, images: Object.keys(snapshot.images).length, deliveries: snapshot.documents.deliveries.length, entities: snapshot.parties.entities.length, }; console.log( `[rig:collect] snapshot.json + ${counts.images} images ` + `(${(imageBytes / 1024).toFixed(0)} KiB): ${JSON.stringify(counts)}`, ); } main().catch((err) => { console.error(`[rig:collect] FAILED: ${err.message}`); process.exit(1); });