feat(platform): harden access scoping and delivery baseline

This commit is contained in:
2026-03-30 00:27:31 +02:00
parent 00b936fa1f
commit 819345acfa
109 changed files with 26142 additions and 8081 deletions
@@ -1,6 +1,9 @@
import { SystemRole } from "@capakraken/shared";
import { VacationStatus, VacationType } from "@capakraken/db";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { createNotification } from "../lib/create-notification.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { vacationRouter } from "../router/vacation.js";
import { createCallerFactory } from "../trpc.js";
@@ -33,6 +36,15 @@ vi.mock("../lib/audit.js", () => ({
createAuditEntry: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../lib/logger.js", () => ({
logger: {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
},
}));
const createCaller = createCallerFactory(vacationRouter);
function createProtectedCaller(db: Record<string, unknown>) {
@@ -163,6 +175,9 @@ describe("vacation router", () => {
describe("list", () => {
it("returns vacations with default filters", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([sampleVacation]),
},
@@ -183,6 +198,9 @@ describe("vacation router", () => {
it("applies resourceId filter", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
@@ -198,8 +216,48 @@ describe("vacation router", () => {
);
});
it("scopes regular users to their own resource when no filter is provided", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const caller = createProtectedCaller(db);
await caller.list({});
expect(db.vacation.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ resourceId: "res_own" }),
}),
);
});
it("forbids regular users from listing another resource's vacations", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
},
vacation: {
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(caller.list({ resourceId: "res_other" })).rejects.toThrow(
"You can only view vacation data for your own resource",
);
expect(db.vacation.findMany).not.toHaveBeenCalled();
});
it("applies status and type filters", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
@@ -229,11 +287,110 @@ describe("vacation router", () => {
});
});
describe("background side effects", () => {
it("logs and swallows async notification failures during approval", async () => {
vi.mocked(createNotification).mockRejectedValueOnce(new Error("notification down"));
const db = createVacationDb({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
},
resource: {
findUnique: vi.fn().mockImplementation(async (args?: { select?: Record<string, unknown> }) => {
const select = args?.select ?? {};
return {
...(select.displayName ? { displayName: "Alice" } : {}),
...(select.user
? { user: { id: "user_1", email: "user@example.com", name: "User" } }
: {}),
};
}),
},
vacation: {
findUnique: vi.fn().mockResolvedValue({
...sampleVacation,
status: VacationStatus.PENDING,
}),
update: vi.fn().mockResolvedValue({
...sampleVacation,
status: VacationStatus.APPROVED,
}),
},
});
const caller = createManagerCaller(db);
const result = await caller.approve({ id: "vac_1" });
await Promise.resolve();
await Promise.resolve();
expect(result.status).toBe(VacationStatus.APPROVED);
expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
expect.objectContaining({
effectName: "notifyVacationStatus",
vacationId: "vac_1",
resourceId: "res_1",
newStatus: VacationStatus.APPROVED,
}),
"Vacation background side effect failed",
);
});
it("logs and swallows webhook failures during approval", async () => {
vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook down"));
const db = createVacationDb({
user: {
findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
},
resource: {
findUnique: vi.fn().mockImplementation(async (args?: { select?: Record<string, unknown> }) => {
const select = args?.select ?? {};
return {
...(select.displayName ? { displayName: "Alice" } : {}),
...(select.user
? { user: { id: "user_1", email: "user@example.com", name: "User" } }
: {}),
};
}),
},
vacation: {
findUnique: vi.fn().mockResolvedValue({
...sampleVacation,
status: VacationStatus.PENDING,
}),
update: vi.fn().mockResolvedValue({
...sampleVacation,
status: VacationStatus.APPROVED,
}),
},
});
const caller = createManagerCaller(db);
const result = await caller.approve({ id: "vac_1" });
await Promise.resolve();
await Promise.resolve();
expect(result.status).toBe(VacationStatus.APPROVED);
expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
expect.objectContaining({ effectName: "dispatchWebhooks", event: "vacation.approved" }),
"Vacation background side effect failed",
);
});
});
describe("getById", () => {
it("returns vacation by id", async () => {
const db = {
resource: {
findFirst: vi.fn(),
},
vacation: {
findUnique: vi.fn().mockResolvedValue(sampleVacation),
findUnique: vi.fn().mockResolvedValue({
...sampleVacation,
resource: { ...sampleVacation.resource, userId: "user_1" },
}),
},
};
@@ -248,6 +405,23 @@ describe("vacation router", () => {
);
});
it("forbids regular users from reading another user's vacation", async () => {
const db = {
vacation: {
findUnique: vi.fn().mockResolvedValue({
...sampleVacation,
requestedById: "someone_else",
resource: { ...sampleVacation.resource, userId: "someone_else" },
}),
},
};
const caller = createProtectedCaller(db);
await expect(caller.getById({ id: "vac_1" })).rejects.toThrow(
"You can only view your own vacation data",
);
});
it("throws NOT_FOUND for missing vacation", async () => {
const db = {
vacation: {
@@ -890,6 +1064,9 @@ describe("vacation router", () => {
describe("getForResource", () => {
it("returns approved vacations in date range", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([
{
@@ -920,6 +1097,27 @@ describe("vacation router", () => {
}),
);
});
it("forbids regular users from reading another resource's approved vacations", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
},
vacation: {
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.getForResource({
resourceId: "res_other",
startDate: new Date("2026-01-01"),
endDate: new Date("2026-12-31"),
}),
).rejects.toThrow("You can only view vacation data for your own resource");
expect(db.vacation.findMany).not.toHaveBeenCalled();
});
});
describe("getPendingApprovals", () => {
@@ -952,6 +1150,7 @@ describe("vacation router", () => {
it("returns overlapping vacations for the same chapter", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
findUnique: vi.fn().mockResolvedValue({ chapter: "Animation" }),
},
vacation: {
@@ -987,6 +1186,7 @@ describe("vacation router", () => {
it("returns empty array when resource has no chapter", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
findUnique: vi.fn().mockResolvedValue({ chapter: null }),
},
};
@@ -1000,6 +1200,76 @@ describe("vacation router", () => {
expect(result).toEqual([]);
});
it("forbids regular users from reading another resource's team overlap", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
findUnique: vi.fn(),
},
vacation: {
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.getTeamOverlap({
resourceId: "res_other",
startDate: new Date("2026-06-01"),
endDate: new Date("2026-06-05"),
}),
).rejects.toThrow("You can only view vacation data for your own resource");
expect(db.resource.findUnique).not.toHaveBeenCalled();
expect(db.vacation.findMany).not.toHaveBeenCalled();
});
});
describe("getTeamOverlapDetail", () => {
it("returns assistant-friendly overlap detail from the canonical overlap query", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_1" }),
findUnique: vi
.fn()
.mockResolvedValueOnce({ displayName: "Bruce Banner", chapter: "CGI" }),
},
vacation: {
findMany: vi.fn().mockResolvedValue([
{
...sampleVacation,
id: "vac_other",
resourceId: "res_2",
status: VacationStatus.APPROVED,
resource: { id: "res_2", displayName: "Clark Kent", eid: "E-002" },
},
]),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getTeamOverlapDetail({
resourceId: "res_1",
startDate: new Date("2026-08-10T00:00:00.000Z"),
endDate: new Date("2026-08-12T00:00:00.000Z"),
});
expect(result).toEqual({
resource: "Bruce Banner",
chapter: "CGI",
period: "2026-08-10 to 2026-08-12",
overlapCount: 1,
overlappingVacations: [
{
resource: "Clark Kent",
type: VacationType.ANNUAL,
status: VacationStatus.APPROVED,
start: "2026-06-01",
end: "2026-06-05",
},
],
});
});
});
describe("batchCreatePublicHolidays", () => {