Gated behind NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED (opt-in, "true"). When on, submitter roles (TECHNICAL/MANNING) get read-only access to every PO: the History page + report export, any other user's PO detail page, and the per-PO Export PDF/XLSX buttons. No approval/payment/edit rights are added. - lib/feature-flags.ts: SUBMITTER_VIEW_ALL_ENABLED flag - lib/permissions.ts: isSubmitterRole / submitterCanViewAll / canViewAllPos - po/[id] page + export route: gate via canViewAllPos - history page + reports/export route: OR submitterCanViewAll into export_reports - sidebar: show History to submitters when flag on - tests: permission helpers, both flag states - docs: .env.example, CLAUDE.md (wiki updated separately) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
6.6 KiB
TypeScript
179 lines
6.6 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import {
|
|
hasPermission,
|
|
requirePermission,
|
|
isSubmitterRole,
|
|
submitterCanViewAll,
|
|
canViewAllPos,
|
|
} from "@/lib/permissions";
|
|
|
|
describe("Permissions", () => {
|
|
describe("hasPermission", () => {
|
|
// ── Original cases ─────────────────────────────────────────────────────
|
|
|
|
it("TECHNICAL can create POs", () => {
|
|
expect(hasPermission("TECHNICAL", "create_po")).toBe(true);
|
|
});
|
|
|
|
it("TECHNICAL cannot approve POs", () => {
|
|
expect(hasPermission("TECHNICAL", "approve_po")).toBe(false);
|
|
});
|
|
|
|
it("MANAGER can approve POs", () => {
|
|
expect(hasPermission("MANAGER", "approve_po")).toBe(true);
|
|
});
|
|
|
|
// MANAGER was intentionally granted process_payment in commit e1340b9
|
|
// ("chore(perm): manager permissions fix 2").
|
|
it("MANAGER can process payment", () => {
|
|
expect(hasPermission("MANAGER", "process_payment")).toBe(true);
|
|
});
|
|
|
|
it("ACCOUNTS can process payment", () => {
|
|
expect(hasPermission("ACCOUNTS", "process_payment")).toBe(true);
|
|
});
|
|
|
|
it("SUPERUSER has all operational permissions", () => {
|
|
expect(hasPermission("SUPERUSER", "create_po")).toBe(true);
|
|
expect(hasPermission("SUPERUSER", "approve_po")).toBe(true);
|
|
expect(hasPermission("SUPERUSER", "process_payment")).toBe(true);
|
|
expect(hasPermission("SUPERUSER", "confirm_receipt")).toBe(true);
|
|
});
|
|
|
|
it("ADMIN can manage users", () => {
|
|
expect(hasPermission("ADMIN", "manage_users")).toBe(true);
|
|
});
|
|
|
|
it("AUDITOR has read-only access", () => {
|
|
expect(hasPermission("AUDITOR", "view_all_pos")).toBe(true);
|
|
expect(hasPermission("AUDITOR", "approve_po")).toBe(false);
|
|
expect(hasPermission("AUDITOR", "create_po")).toBe(false);
|
|
});
|
|
|
|
// ── New permissions: MANAGER and ACCOUNTS expansions ──────────────────
|
|
|
|
it("MANAGER can create POs", () => {
|
|
expect(hasPermission("MANAGER", "create_po")).toBe(true);
|
|
});
|
|
|
|
it("MANAGER can submit POs", () => {
|
|
expect(hasPermission("MANAGER", "submit_po")).toBe(true);
|
|
});
|
|
|
|
it("MANAGER can manage vendors", () => {
|
|
expect(hasPermission("MANAGER", "manage_vendors")).toBe(true);
|
|
});
|
|
|
|
it("ACCOUNTS can manage vendors", () => {
|
|
expect(hasPermission("ACCOUNTS", "manage_vendors")).toBe(true);
|
|
});
|
|
|
|
it("ACCOUNTS cannot create POs", () => {
|
|
expect(hasPermission("ACCOUNTS", "create_po")).toBe(false);
|
|
});
|
|
|
|
it("ACCOUNTS cannot approve POs", () => {
|
|
expect(hasPermission("ACCOUNTS", "approve_po")).toBe(false);
|
|
});
|
|
|
|
it("TECHNICAL cannot manage vendors", () => {
|
|
expect(hasPermission("TECHNICAL", "manage_vendors")).toBe(false);
|
|
});
|
|
|
|
it("MANNING cannot manage vendors", () => {
|
|
expect(hasPermission("MANNING", "manage_vendors")).toBe(false);
|
|
});
|
|
|
|
it("AUDITOR cannot create, submit, or approve POs", () => {
|
|
expect(hasPermission("AUDITOR", "create_po")).toBe(false);
|
|
expect(hasPermission("AUDITOR", "submit_po")).toBe(false);
|
|
expect(hasPermission("AUDITOR", "approve_po")).toBe(false);
|
|
});
|
|
|
|
it("AUDITOR cannot manage vendors or products", () => {
|
|
expect(hasPermission("AUDITOR", "manage_vendors")).toBe(false);
|
|
expect(hasPermission("AUDITOR", "manage_products")).toBe(false);
|
|
});
|
|
|
|
it("ADMIN cannot approve or process payments", () => {
|
|
expect(hasPermission("ADMIN", "approve_po")).toBe(false);
|
|
expect(hasPermission("ADMIN", "process_payment")).toBe(false);
|
|
});
|
|
|
|
it("SUPERUSER does not have manage_vendors (admin-only permission)", () => {
|
|
expect(hasPermission("SUPERUSER", "manage_vendors")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── Submitter view-all (feature-flagged) ──────────────────────────────────
|
|
describe("isSubmitterRole", () => {
|
|
it("is true for the two submitter roles", () => {
|
|
expect(isSubmitterRole("TECHNICAL")).toBe(true);
|
|
expect(isSubmitterRole("MANNING")).toBe(true);
|
|
});
|
|
|
|
it("is false for every other role", () => {
|
|
for (const role of ["ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"] as const) {
|
|
expect(isSubmitterRole(role)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("canViewAllPos / submitterCanViewAll — flag OFF (default)", () => {
|
|
it("submitters cannot view all POs", () => {
|
|
expect(canViewAllPos("TECHNICAL")).toBe(false);
|
|
expect(canViewAllPos("MANNING")).toBe(false);
|
|
expect(submitterCanViewAll("TECHNICAL")).toBe(false);
|
|
});
|
|
|
|
it("view_all_pos holders can still view all POs", () => {
|
|
for (const role of ["ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"] as const) {
|
|
expect(canViewAllPos(role)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("canViewAllPos / submitterCanViewAll — flag ON", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.resetModules();
|
|
});
|
|
|
|
it("submitters gain view-all when NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED=true", async () => {
|
|
vi.resetModules();
|
|
vi.stubEnv("NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED", "true");
|
|
const perms = await import("@/lib/permissions");
|
|
|
|
expect(perms.submitterCanViewAll("TECHNICAL")).toBe(true);
|
|
expect(perms.submitterCanViewAll("MANNING")).toBe(true);
|
|
expect(perms.canViewAllPos("TECHNICAL")).toBe(true);
|
|
expect(perms.canViewAllPos("MANNING")).toBe(true);
|
|
});
|
|
|
|
it("does not widen non-submitter roles, and is read-only (no approve/edit)", async () => {
|
|
vi.resetModules();
|
|
vi.stubEnv("NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED", "true");
|
|
const perms = await import("@/lib/permissions");
|
|
|
|
expect(perms.submitterCanViewAll("MANAGER")).toBe(false);
|
|
expect(perms.canViewAllPos("ACCOUNTS")).toBe(true); // unchanged
|
|
// The flag grants read access only — no approval or edit rights.
|
|
expect(perms.hasPermission("TECHNICAL", "approve_po")).toBe(false);
|
|
expect(perms.hasPermission("TECHNICAL", "view_all_pos")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("requirePermission", () => {
|
|
it("does not throw when permission is granted", () => {
|
|
expect(() => requirePermission("MANAGER", "approve_po")).not.toThrow();
|
|
});
|
|
|
|
it("throws when permission is denied", () => {
|
|
expect(() => requirePermission("TECHNICAL", "approve_po")).toThrow();
|
|
});
|
|
|
|
it("throws with a message containing the role name", () => {
|
|
expect(() => requirePermission("ACCOUNTS", "approve_po")).toThrow(/ACCOUNTS/);
|
|
});
|
|
});
|
|
});
|