import { test } from "node:test"; import assert from "node:assert/strict"; import Fastify from "fastify"; import cookie from "@fastify/cookie"; import { openDb } from "../src/db.js"; import type { AppConfig } from "../src/config.js"; import { upsertFabric, upsertShade, listShades, setAllShadesVerification } from "../src/domain/fabric.js"; import { getAppSetting, setAppSetting } from "../src/domain/settings.js"; import { createHttpBaseAdapter, ExportBlocked, type BaseAdapter } from "../src/integration/baseAdapter.js"; import { registerRoutes } from "../src/routes.js"; import { createUser, login } from "../src/auth.js"; function fixture() { const db = openDb(":memory:"); const cfg = { env: "test", baseApiToken: null, baseApiUrl: "https://example.invalid/connector.php", } as AppConfig; const fabric = upsertFabric(db, { code: "tst", name: "TST", producer: "TestTex" }); for (const code of ["01", "03", "08"]) upsertShade(db, fabric.id, { code, name: `odcień ${code}` }); const adminLogin = `adm-${Math.random().toString(36).slice(2, 8)}`; const approverLogin = `app-${Math.random().toString(36).slice(2, 8)}`; const operatorLogin = `ops-${Math.random().toString(36).slice(2, 8)}`; createUser(db, { login: adminLogin, password: "test-isolated-only", displayName: "admin", role: "admin" }); createUser(db, { login: approverLogin, password: "test-isolated-only", displayName: "approver", role: "approver" }); createUser(db, { login: operatorLogin, password: "test-isolated-only", displayName: "operator", role: "operator" }); const cookies = { admin: { vilmal_session: login(db, adminLogin, "test-isolated-only")!.sessionId }, approver: { vilmal_session: login(db, approverLogin, "test-isolated-only")!.sessionId }, operator: { vilmal_session: login(db, operatorLogin, "test-isolated-only")!.sessionId }, }; const cleanup = () => db.close(); return { db, cfg, fabric, cookies, cleanup }; } async function appWith(adapter?: BaseAdapter) { const app = Fastify(); await app.register(cookie); return { app, adapterArg: adapter ? { baseAdapter: adapter } : {} }; } test("verify-all: jedna decyzja zatwierdza wszystkie odcienie, audyt per odcień, idempotentna", async (t) => { const f = fixture(); t.after(f.cleanup); const first = setAllShadesVerification(f.db, "tst", "confirmed", "approver-1"); assert.deepEqual(first, { total: 3, changed: 3 }); assert.ok(listShades(f.db, f.fabric.id).every((s) => s.verification === "confirmed")); const audits = f.db.prepare( "SELECT COUNT(*) n FROM audit_log WHERE action = 'fabric_shade.verification'" ).get() as { n: number }; assert.equal(audits.n, 3); const second = setAllShadesVerification(f.db, "tst", "confirmed", "approver-1"); assert.deepEqual(second, { total: 3, changed: 0 }); assert.equal(setAllShadesVerification(f.db, "nie-ma", "confirmed", "approver-1"), null); }); test("verify-all endpoint: 401 bez sesji, 403 dla operatora, 200 dla approvera, 404 dla złej tkaniny", async (t) => { const f = fixture(); t.after(f.cleanup); const { app, adapterArg } = await appWith(); registerRoutes(app, f.db, f.cfg, adapterArg); const url = "/api/fabrics/tst/shades/verify-all"; assert.equal((await app.inject({ method: "POST", url })).statusCode, 401); assert.equal((await app.inject({ method: "POST", url, cookies: f.cookies.operator })).statusCode, 403); assert.equal((await app.inject({ method: "POST", url: "/api/fabrics/nie-ma/shades/verify-all", cookies: f.cookies.approver })).statusCode, 404); const res = await app.inject({ method: "POST", url, cookies: f.cookies.approver }); assert.equal(res.statusCode, 200); assert.deepEqual(res.json(), { total: 3, changed: 3 }); assert.ok(listShades(f.db, f.fabric.id).every((s) => s.verification === "confirmed")); await app.close(); }); test("settings: GET nie zwraca tokena; PUT tylko dla admina; token z panelu od razu aktywuje adapter", async (t) => { const f = fixture(); t.after(f.cleanup); const adapter = createHttpBaseAdapter(f.cfg, () => getAppSetting(f.db, "baselinker.api_token")); const { app } = await appWith(); registerRoutes(app, f.db, f.cfg, { baseAdapter: adapter }); assert.equal(adapter.live, false); const before = (await app.inject({ url: "/api/settings", cookies: f.cookies.operator })).json() as { baselinker: { tokenConfigured: boolean; tokenSource: string | null }; }; assert.equal(before.baselinker.tokenConfigured, false); assert.equal(before.baselinker.tokenSource, null); assert.equal( (await app.inject({ method: "PUT", url: "/api/settings/baselinker", cookies: f.cookies.approver, payload: { apiToken: "X".repeat(80) } })).statusCode, 403 ); const saved = await app.inject({ method: "PUT", url: "/api/settings/baselinker", cookies: f.cookies.admin, payload: { apiToken: "X".repeat(80), inventoryId: 62360, priceGroupId: 54601, warehouseId: "bl_91183" }, }); assert.equal(saved.statusCode, 200); const after = saved.json() as { baselinker: { tokenConfigured: boolean; tokenSource: string; inventoryId: number; priceGroupId: number; warehouseId: string } }; assert.equal(after.baselinker.tokenConfigured, true); assert.equal(after.baselinker.tokenSource, "panel"); assert.equal(after.baselinker.inventoryId, 62360); assert.equal(after.baselinker.priceGroupId, 54601); assert.equal(after.baselinker.warehouseId, "bl_91183"); assert.ok(!JSON.stringify(after).includes("X".repeat(80)), "odpowiedź nie może zawierać tokena"); // adapter widzi token z app_settings bez restartu assert.equal(adapter.live, true); // audyt zapisuje zmianę bez wartości sekretu const auditRow = f.db.prepare( "SELECT detail_json FROM audit_log WHERE action = 'settings.set' AND entity_id = 'baselinker.api_token'" ).get() as { detail_json: string }; assert.ok(!auditRow.detail_json.includes("X".repeat(80))); await app.close(); }); test("adapter: autoryzuje żądania tokenem w body, getInventories parsuje odpowiedź, brak tokenu → ExportBlocked", async (t) => { const bodies: string[] = []; const orig = globalThis.fetch; globalThis.fetch = (async (_url: unknown, init?: { body?: unknown }) => { bodies.push(String(init?.body ?? "")); return new Response(JSON.stringify({ status: "SUCCESS", inventories: { "62360": { name: "Magazyn główny" } } }), { status: 200, headers: { "Content-Type": "application/json" }, }); }) as typeof fetch; t.after(() => { globalThis.fetch = orig; }); const f = fixture(); t.after(f.cleanup); const adapter = createHttpBaseAdapter(f.cfg, () => getAppSetting(f.db, "baselinker.api_token")); await assert.rejects(() => adapter.getInventories(), ExportBlocked); setAppSetting(f.db, "baselinker.api_token", "Y".repeat(80), "admin-1"); const inventories = await adapter.getInventories(); assert.deepEqual(inventories, [{ inventoryId: 62360, name: "Magazyn główny" }]); assert.equal(bodies.length, 1); const params = new URLSearchParams(bodies[0]!); assert.equal(params.get("token"), "Y".repeat(80)); assert.equal(params.get("method"), "getInventories"); });