2026-10-06 02:02:08 +00:00
|
|
|
// Run against a running local dev or production server. Does not modify PocketBase.
|
|
|
|
|
import { test } from "node:test";
|
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
|
|
|
|
|
const base = process.env.MEGA_AUTH_BASE_URL || "http://localhost:6007";
|
|
|
|
|
test("anonymous dashboard and app API requests are denied, including IDs ending in .json", async () => {
|
Split the invoice register into Internal and External Invoices
Renames the existing /income/invoices page to "Internal Invoices" and adds a
sibling /income/external-invoices page, splitting ONE `invoices` collection by
the debtor printed on the invoice: a Mega group company is internal, anything
else is external.
Never two collections. An invoice must not be able to exist on neither page, and
the halves must stay reconcilable against /income. Both routes render through the
single components/InvoiceRegisterPage.tsx so they cannot drift apart.
lib/invoice-party-policy.json holds the group entity list and lib/invoice-party.ts
does the matching. Matching is on debtor name OR debtor_no, never name alone:
all three external invoices (PHI 178/186/192) carry an empty debtor name and only
a third-party number, so a name-only match would misfile every one of them. An
invoice naming neither is never internal - it lands on External and is reported
as unclassified rather than silently filed.
The split is 16 internal / 3 external across the 19 invoices in the register.
Also fixes three things the split would otherwise break:
- Booking a line, importing a PDF and retrying an intake can all land on either
side depending on the debtor, so the five write paths only revalidated
/income/invoices and would have left the other page stale. They now call
revalidateInvoicePages() from lib/invoice-revalidate.ts. Route paths live in
lib/invoice-routes.ts, which imports no next/* so a client component may import
it; the revalidate helper imports next/cache and is server-only.
- A site's "Go and assign them" link hard-coded the internal path, so a site whose
outstanding lines sit on external invoices would dead-end. SiteSummary now
carries the outstanding lines split by party.
- verify-auth-routes.mjs now covers the new route.
The two routes are siblings by design: isGroupActive matches a child by path
prefix, so a nested /income/invoices/external would highlight both leaves at once.
Verified: typecheck clean, 41 tests pass (7 new), production build green,
anonymous access to both routes 307s to /login, and the mobile checklist passes at
375x812 and 768x1024 - no horizontal page scroll, no tap target under 44px, no
text under 12px. The nested line-item table still scrolls inside its own container
without stretching the outer table.
Note: lib/rows.ts, lib/server-data.ts and the running/insurance feature are
someone else's in-flight work and were deliberately left uncommitted here.
2026-10-08 09:49:48 +00:00
|
|
|
for (const path of [
|
2026-10-22 09:39:18 +00:00
|
|
|
"/", "/settings", "/plant", "/income/invoices", "/income/external-invoices", "/sites", "/clock", "/employees", "/attendance",
|
Split the invoice register into Internal and External Invoices
Renames the existing /income/invoices page to "Internal Invoices" and adds a
sibling /income/external-invoices page, splitting ONE `invoices` collection by
the debtor printed on the invoice: a Mega group company is internal, anything
else is external.
Never two collections. An invoice must not be able to exist on neither page, and
the halves must stay reconcilable against /income. Both routes render through the
single components/InvoiceRegisterPage.tsx so they cannot drift apart.
lib/invoice-party-policy.json holds the group entity list and lib/invoice-party.ts
does the matching. Matching is on debtor name OR debtor_no, never name alone:
all three external invoices (PHI 178/186/192) carry an empty debtor name and only
a third-party number, so a name-only match would misfile every one of them. An
invoice naming neither is never internal - it lands on External and is reported
as unclassified rather than silently filed.
The split is 16 internal / 3 external across the 19 invoices in the register.
Also fixes three things the split would otherwise break:
- Booking a line, importing a PDF and retrying an intake can all land on either
side depending on the debtor, so the five write paths only revalidated
/income/invoices and would have left the other page stale. They now call
revalidateInvoicePages() from lib/invoice-revalidate.ts. Route paths live in
lib/invoice-routes.ts, which imports no next/* so a client component may import
it; the revalidate helper imports next/cache and is server-only.
- A site's "Go and assign them" link hard-coded the internal path, so a site whose
outstanding lines sit on external invoices would dead-end. SiteSummary now
carries the outstanding lines split by party.
- verify-auth-routes.mjs now covers the new route.
The two routes are siblings by design: isGroupActive matches a child by path
prefix, so a nested /income/invoices/external would highlight both leaves at once.
Verified: typecheck clean, 41 tests pass (7 new), production build green,
anonymous access to both routes 307s to /login, and the mobile checklist passes at
375x812 and 768x1024 - no horizontal page scroll, no tap target under 44px, no
text under 12px. The nested line-item table still scrolls inside its own container
without stretching the outer table.
Note: lib/rows.ts, lib/server-data.ts and the running/insurance feature are
someone else's in-flight work and were deliberately left uncommitted here.
2026-10-08 09:49:48 +00:00
|
|
|
]) {
|
2026-10-06 02:02:08 +00:00
|
|
|
const response = await fetch(`${base}${path}`, { redirect: "manual" });
|
|
|
|
|
assert.equal(response.status, 307);
|
|
|
|
|
assert.ok(response.headers.get("location")?.endsWith("/login"));
|
|
|
|
|
}
|
2026-10-22 09:39:18 +00:00
|
|
|
for (const path of ["/api/assets/missing", "/api/assets/missing.json", "/api/invoices/intake", "/api/clock/roster", "/api/clock/scan"]) {
|
2026-10-06 02:02:08 +00:00
|
|
|
assert.equal((await fetch(`${base}${path}`)).status, 401);
|
|
|
|
|
}
|
|
|
|
|
assert.equal((await fetch(`${base}/api/health`)).status, 200);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-21 04:39:15 +00:00
|
|
|
test("standalone Clock manifest and sign-in are public while scanner remains protected", async () => {
|
|
|
|
|
const response=await fetch(`${base}/clock.webmanifest`);
|
|
|
|
|
assert.equal(response.status,200);assert.equal((await response.json()).id,"/clock");
|
|
|
|
|
const login=await fetch(`${base}/clock/login`);assert.equal(login.status,200);
|
|
|
|
|
const html=await login.text();assert.match(html,/clock\.webmanifest/);
|
|
|
|
|
assert.match(html,/Sign in to start scanning/);
|
|
|
|
|
const scanner=await fetch(`${base}/clock`,{redirect:"manual"});
|
|
|
|
|
assert.equal(scanner.status,307);assert.ok(scanner.headers.get("location")?.endsWith("/clock/login"));
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-06 02:02:08 +00:00
|
|
|
test("logout deletes local session without a valid backend user and rejects other origins", async () => {
|
|
|
|
|
const response = await fetch(`${base}/api/auth/logout`, {
|
|
|
|
|
method: "POST", headers: { Origin: base, Cookie: "mega_session=not-a-real-token" },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(response.status, 200);
|
|
|
|
|
assert.match(response.headers.get("set-cookie") || "", /mega_session=;/);
|
|
|
|
|
assert.equal((await fetch(`${base}/api/auth/logout`, { method: "POST", headers: { Origin: "https://unrelated.example" } })).status, 403);
|
|
|
|
|
});
|