MEGAINFRA-WEBAPP/src/proxy.test.ts
DEls 0857e56cb3 fix(workforce): let site managers add and rename employers and brokers
The employer and broker API routes were never named in the path
classifier, so they fell through to its admin-only default and the
proxy rejected a site manager with a bare {"error":"Not allowed"}
before the route handler ran. The handler's own site scoping was
always correct — it just never got the chance to apply.

- classify /api/employers as dashboard and map it to the workforce
  module, mirroring /api/employees
- let the site dropdown be chosen when adding an employer; only a
  rename freezes the site, which preserves assignment history
- cover the rename path, which had no route test, and the proxy
  gate for the site-manager role
2026-10-19 16:33:41 +02:00

108 lines
5.7 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);
});
// Employers and brokers are site-scoped Workforce master data. The proxy
// must let a site manager both create one and rename one; the handler
// scopes both writes to the caller's own sites.
it("lets a site manager add and rename employers and brokers", async () => {
expect((await proxy(request("/api/employers"))).status).toBe(200);
expect((await proxy(request("/api/employers/broker000000001"))).status).toBe(200);
});
it("keeps employer and broker routes away from clock accounts", 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/employers"))).status).toBe(403);
expect((await proxy(request("/api/employers/broker000000001"))).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();
});
});