Add turnover, project, retention, and year comparison pages from the supplied workbook, protected by named admin access. Include the local chart gallery, grouped monthly hover, smooth comparison toggle, and monthly income minus cost series. Require named PocketBase sign-in in every environment and remove the shared PIN bypass. Update access documentation and business-route test fixtures.
92 lines
4.9 KiB
TypeScript
92 lines
4.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
import { proxy } from "./proxy";
|
|
|
|
const request = (path: string, cookie = "mega_session=token") =>
|
|
new NextRequest(`http://app.test${path}`, { headers: cookie ? { cookie } : {} });
|
|
|
|
beforeEach(() => {
|
|
vi.stubEnv("AUTH_MODE", "users");
|
|
vi.stubEnv("NEXT_PUBLIC_POCKETBASE_URL", "http://pb.test");
|
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ record: {
|
|
id: "u1", email: "u@example.test", role: "site_user", sites: ["site1"], is_active: true,
|
|
} })));
|
|
});
|
|
afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
|
|
|
|
describe("login proxy", () => {
|
|
it("keeps requests protected when AUTH_MODE is absent or still set to pin", async () => {
|
|
for (const mode of ["", "pin"]) {
|
|
vi.stubEnv("AUTH_MODE", mode);
|
|
expect((await proxy(request("/api/sites", ""))).status).toBe(401);
|
|
}
|
|
});
|
|
|
|
it("blocks site managers from every Management page and API", async () => {
|
|
expect((await proxy(request("/dashboard/management/turnover"))).headers.get("location")).toBe("http://app.test/dashboard");
|
|
expect((await proxy(request("/api/management/turnover"))).status).toBe(403);
|
|
});
|
|
it("rejects an anonymous data request", async () => {
|
|
expect((await proxy(request("/api/sites", ""))).status).toBe(401);
|
|
});
|
|
|
|
it("keeps clock users on clock pages and scan/upload APIs", async () => {
|
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ record: {
|
|
id: "u1", email: "u@example.test", role: "clock_only", sites: ["site1"], is_active: true,
|
|
} })));
|
|
expect((await proxy(request("/api/sites"))).status).toBe(403);
|
|
expect((await proxy(request("/api/clock/scan"))).status).toBe(200);
|
|
expect((await proxy(request("/api/delivery-notes/upload"))).status).toBe(200);
|
|
expect((await proxy(request("/api/purchase-orders/upload"))).status).toBe(200);
|
|
for (const path of ["/dashboard", "/dashboard/workforce", "/dashboard/financial", "/dashboard/plant", "/dashboard/settings/users"]) {
|
|
expect((await proxy(request(path))).headers.get("location")).toBe("http://app.test/clock");
|
|
}
|
|
for (const path of ["/api/timesheets/import", "/api/vehicles/vehicle", "/api/dn-batches/batch/notes/note/post", "/api/clock/events/event/correct"]) {
|
|
expect((await proxy(request(path))).status).toBe(403);
|
|
}
|
|
});
|
|
|
|
it("limits a workforce office account to workforce pages and APIs", async () => {
|
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ record: {
|
|
id: "office", email: "office@example.test", role: "office_user", modules: ["workforce"], all_sites: true, sites: [], is_active: true,
|
|
} })));
|
|
for (const path of ["/dashboard/workforce", "/dashboard/workforce/workers", "/dashboard/workforce/new-feature", "/api/timesheets/import", "/api/employees/abcdefghijklmno/rates", "/api/employees/abcdefghijklmno/sick-notes", "/api/clock/events/event/correct"]) {
|
|
expect((await proxy(request(path))).status, path).toBe(200);
|
|
}
|
|
for (const path of ["/dashboard", "/dashboard/sites", "/dashboard/plant", "/dashboard/financial", "/dashboard/chat", "/dashboard/settings/users", "/clock"]) {
|
|
expect((await proxy(request(path))).headers.get("location"), path).toBe("http://app.test/dashboard/workforce");
|
|
}
|
|
for (const path of ["/api/vehicles/vehicle", "/api/delivery-notes/upload", "/api/suppliers", "/api/admin/users", "/api/unknown"]) {
|
|
expect((await proxy(request(path))).status, path).toBe(403);
|
|
}
|
|
});
|
|
|
|
it("blocks a site user from user administration and unknown APIs", async () => {
|
|
expect((await proxy(request("/api/admin/users/another-user"))).status).toBe(403);
|
|
expect((await proxy(request("/api/unknown"))).status).toBe(403);
|
|
});
|
|
|
|
it("allows site managers into user creation but blocks clock users", async () => {
|
|
expect((await proxy(request("/api/admin/users"))).status).toBe(200);
|
|
expect((await proxy(request("/dashboard/settings/users"))).status).toBe(200);
|
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ record: {
|
|
id: "u1", email: "u@example.test", role: "clock_only", sites: ["site1"], is_active: true,
|
|
} })));
|
|
expect((await proxy(request("/api/admin/users"))).status).toBe(403);
|
|
});
|
|
|
|
it("redirects a first-time user before rendering dashboard data", async () => {
|
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ record: {
|
|
id: "u1", email: "u@example.test", role: "site_user", sites: ["site1"],
|
|
is_active: true, must_change_password: true,
|
|
} })));
|
|
const response = await proxy(request("/dashboard"));
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe("http://app.test/change-password");
|
|
});
|
|
|
|
it("preserves machine routes without a user cookie", async () => {
|
|
expect((await proxy(request("/api/purchase-orders/ingest", ""))).status).toBe(200);
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
});
|