/** * End-to-end smoke test for the Management section schema. * * Exercises the same write paths the UI does (direct PocketBase admin * API), so a green run here proves the new field shapes are accepted * by the live PB and the pb-helpers read paths still return what * they used to. * * Flow: * 1. Create a Company with geoPoint coordinates. * 2. Read it back and verify the geoPoint round-trips. * 3. Create a Customer linked to that Company (relation), with a * sales rep (relation to users), and geoPoint coordinates. * 4. Read it back with `expand=company,rep` and verify both * relations resolve to real names. * 5. Create a Supplier (text coordinates — unchanged). * 6. Read each collection via the pb-helpers getters to confirm * the client-side reads still return the expected shape. * 7. Clean up — delete the three test rows. * * Run with: * npx tsx scripts/smoke/management-flow.ts * * Why direct PB admin and not the Next.js helpers: * The Next.js pb-helpers are unauth (they reuse the browser * pocketbase instance and need the user's session). The smoke * script is server-side with admin creds, so it goes via * `getAdminPocketBase` — the same path the migrations use. */ import PocketBase from "pocketbase"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; function loadEnvFile(name: string) { try { const raw = readFileSync(resolve(process.cwd(), name), "utf8"); for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i); if (m && !process.env[m[1]]) { process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim(); } } } catch { /* missing file — fine */ } } loadEnvFile(".env"); loadEnvFile(".env.local"); const RUN_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; // Distinct prefixes so the rows are obvious in the PB admin and easy // to bulk-delete if a run leaves orphans behind. const COMPANY_NAME = `SMOKE-CO-${RUN_ID}`; const CUSTOMER_NAME = `SMOKE-CU-${RUN_ID}`; const SUPPLIER_NAME = `SMOKE-SU-${RUN_ID}`; const JOHANNESBURG = { lon: 28.0473, lat: -26.2041 }; const CAPE_TOWN = { lon: 18.4241, lat: -33.9249 }; async function main() { const url = process.env.NEXT_PUBLIC_POCKETBASE_URL; const email = process.env.PB_ADMIN_EMAIL; const password = process.env.PB_ADMIN_PASSWORD; if (!url || !email || !password) { throw new Error("Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD."); } const pb = new PocketBase(url); await pb.collection("_superusers").authWithPassword(email, password); console.log(`[mgmt-smoke] Authenticated against ${url}`); console.log(`[mgmt-smoke] Run id: ${RUN_ID}`); // Look up a real user to use as the customer's rep — relations // must point to a valid record id. const users = await pb.collection("users").getList(1, 1, { fields: "id,name,email" }); if (users.items.length === 0) { throw new Error("No users in the auth collection — can't back the rep relation."); } const rep = users.items[0]; console.log(`[mgmt-smoke] Using rep: ${rep.name || rep.email} (${rep.id})`); const createdIds = { company: "", customer: "", supplier: "" }; try { // ── 1. Create Company (geoPoint) ───────────────────────────── const company = await pb.collection("companies").create({ company: COMPANY_NAME, address: "123 Smoke Test Lane", vendor: `SMOKE-V-${RUN_ID}`, coordinates: JOHANNESBURG, }); createdIds.company = company.id; console.log(`[mgmt-smoke] Company created: ${company.company} (${company.id})`); // ── 2. Verify company round-trip ───────────────────────────── const companyBack = await pb.collection("companies").getOne(company.id); assertEqual(companyBack.company, COMPANY_NAME, "company.company round-trip"); if ( !companyBack.coordinates || companyBack.coordinates.lon !== JOHANNESBURG.lon || companyBack.coordinates.lat !== JOHANNESBURG.lat ) { throw new Error( `FAIL — company.coordinates round-trip: expected ${JSON.stringify(JOHANNESBURG)}, got ${JSON.stringify(companyBack.coordinates)}` ); } console.log(`[mgmt-smoke] Company coordinates round-trip OK: ${JSON.stringify(companyBack.coordinates)}`); // ── 3. Create Customer (relations + geoPoint) ──────────────── const customer = await pb.collection("customers").create({ customer: CUSTOMER_NAME, email: `smoke-cu-${RUN_ID}@example.com`, number: "0123456789", company: company.id, rep: rep.id, coordinates: CAPE_TOWN, description: "Auto-created by mgmt-flow smoke test.", dateadded: new Date().toISOString().slice(0, 10), lastvisiteddate: new Date().toISOString(), }); createdIds.customer = customer.id; console.log( `[mgmt-smoke] Customer created: ${customer.customer} (${customer.id}) → company=${company.id}, rep=${rep.id}` ); // ── 4. Verify customer with expand chain ───────────────────── const customerBack = await pb.collection("customers").getOne(customer.id, { expand: "company,rep", }); if (customerBack.company !== company.id) { throw new Error(`FAIL — customer.company should be ${company.id}, got ${customerBack.company}`); } if (customerBack.rep !== rep.id) { throw new Error(`FAIL — customer.rep should be ${rep.id}, got ${customerBack.rep}`); } if (!customerBack.expand?.company || customerBack.expand.company.id !== company.id) { throw new Error( `FAIL — customer.expand.company should resolve to ${company.id}, got ${JSON.stringify(customerBack.expand?.company)}` ); } if (!customerBack.expand?.rep || customerBack.expand.rep.id !== rep.id) { throw new Error( `FAIL — customer.expand.rep should resolve to ${rep.id}, got ${JSON.stringify(customerBack.expand?.rep)}` ); } if ( !customerBack.coordinates || customerBack.coordinates.lon !== CAPE_TOWN.lon || customerBack.coordinates.lat !== CAPE_TOWN.lat ) { throw new Error( `FAIL — customer.coordinates round-trip: expected ${JSON.stringify(CAPE_TOWN)}, got ${JSON.stringify(customerBack.coordinates)}` ); } console.log( `[mgmt-smoke] Customer expand OK: company="${customerBack.expand.company.company}", rep="${customerBack.expand.rep.name || customerBack.expand.rep.email}", coords=${JSON.stringify(customerBack.coordinates)}` ); // ── 5. Create Supplier (text coordinates — unchanged) ─────── const supplier = await pb.collection("suppliers").create({ suppliername: SUPPLIER_NAME, email: `smoke-su-${RUN_ID}@example.com`, suppliernumber: `SMOKE-S-${RUN_ID}`, supplieraddress: "1 Test Road", supply_tags: "smoke,test", coordinates: `${CAPE_TOWN.lat}, ${CAPE_TOWN.lon}`, closingtime: "17:00", }); createdIds.supplier = supplier.id; console.log(`[mgmt-smoke] Supplier created: ${supplier.suppliername} (${supplier.id})`); const supplierBack = await pb.collection("suppliers").getOne(supplier.id); assertEqual(supplierBack.suppliername, SUPPLIER_NAME, "supplier.suppliername round-trip"); if (supplierBack.coordinates !== `${CAPE_TOWN.lat}, ${CAPE_TOWN.lon}`) { throw new Error(`FAIL — supplier.coordinates text round-trip: got "${supplierBack.coordinates}"`); } console.log(`[mgmt-smoke] Supplier coordinates text round-trip OK: "${supplierBack.coordinates}"`); // ── 6. pb-helpers read paths ───────────────────────────────── // These run on the live PB and use the same shape the UI // expects. We can't import the helpers here (they pull in the // client-side pb instance), so we mirror their queries below. const allCompanies = await pb.collection("companies").getList(1, 200, { filter: `company = "${COMPANY_NAME}"`, }); if (allCompanies.items.length !== 1) { throw new Error(`FAIL — expected 1 company matching ${COMPANY_NAME}, got ${allCompanies.items.length}`); } const allCustomers = await pb.collection("customers").getList(1, 200, { filter: `customer = "${CUSTOMER_NAME}"`, expand: "company,rep", }); if (allCustomers.items.length !== 1) { throw new Error(`FAIL — expected 1 customer matching ${CUSTOMER_NAME}, got ${allCustomers.items.length}`); } const expanded = allCustomers.items[0]; if (!expanded.expand?.company?.company) { throw new Error("FAIL — expanded customer missing expand.company.company"); } console.log( `[mgmt-smoke] pb-helpers shape: customers.getList with expand returned ${expanded.expand.company.company} / ${expanded.expand.rep?.name || expanded.expand.rep?.email}` ); console.log("\n[mgmt-smoke] All checks passed."); } finally { // ── 7. Cleanup ──────────────────────────────────────────────── // Always attempt cleanup so a mid-run failure doesn't leave // smoke rows in production. Each delete is best-effort. console.log("\n[mgmt-smoke] Cleanup..."); for (const [coll, id] of [ ["customers", createdIds.customer], ["companies", createdIds.company], ["suppliers", createdIds.supplier], ] as const) { if (!id) continue; try { await pb.collection(coll).delete(id); console.log(`[mgmt-smoke] deleted ${coll}/${id}`); } catch (err) { console.warn(`[mgmt-smoke] failed to delete ${coll}/${id}: ${err}`); } } } } function assertEqual(actual: unknown, expected: unknown, label: string) { if (actual !== expected) { throw new Error(`FAIL — ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); } } main().catch((err) => { console.error("[mgmt-smoke] FAILED:", err); process.exit(1); });