Files
dashfront/test/collect.mjs
2026-10-08 04:45:35 +02:00

274 lines
9.8 KiB
JavaScript

/**
* Collects a deterministic fixture snapshot from the live testing backend.
*
* Usage:
* npm run rig:reference (runs collect first, then captures)
*
* 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) {
try {
const buf = await get(token, `/api/thumb/${hash}`, { binary: true });
fs.writeFileSync(path.join(IMAGE_DIR, `${hash}.avif`), 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);
});