164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
import { PermissionKey, SSE_EVENT_TYPES, SystemRole } from "@capakraken/shared";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
cancelPendingEvents,
|
|
eventBus,
|
|
permissionAudience,
|
|
resourceAudience,
|
|
type SseEvent,
|
|
userAudience,
|
|
} from "../sse/event-bus.js";
|
|
import { deriveUserSseSubscription } from "../sse/subscription-policy.js";
|
|
|
|
vi.mock("ioredis", () => {
|
|
const RedisMock = vi.fn().mockImplementation(() => ({
|
|
on: vi.fn(),
|
|
subscribe: vi.fn().mockResolvedValue(undefined),
|
|
publish: vi.fn().mockImplementation(() => {
|
|
throw new Error("Redis unavailable (test)");
|
|
}),
|
|
}));
|
|
return { Redis: RedisMock };
|
|
});
|
|
|
|
describe("sse subscription policy", () => {
|
|
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cancelPendingEvents();
|
|
consoleWarnSpy.mockRestore();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("derives canonical user, resource, and permission audiences server-side", () => {
|
|
const subscription = deriveUserSseSubscription({
|
|
userId: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
resourceId: "res_1",
|
|
permissionOverrides: {
|
|
granted: [PermissionKey.VIEW_PLANNING, PermissionKey.MANAGE_ALLOCATIONS],
|
|
},
|
|
});
|
|
|
|
expect(subscription.includeUnscoped).toBe(false);
|
|
expect(subscription.permissions).toEqual(new Set([
|
|
PermissionKey.VIEW_PLANNING,
|
|
PermissionKey.MANAGE_ALLOCATIONS,
|
|
]));
|
|
expect(subscription.audiences).toEqual([
|
|
permissionAudience(PermissionKey.MANAGE_ALLOCATIONS),
|
|
permissionAudience(PermissionKey.VIEW_PLANNING),
|
|
resourceAudience("res_1"),
|
|
userAudience("user_1"),
|
|
]);
|
|
});
|
|
|
|
it("does not deliver planning events to a standard user without the matching audience", () => {
|
|
const received: SseEvent[] = [];
|
|
const unsubscribe = eventBus.subscribe(
|
|
(event) => {
|
|
received.push(event);
|
|
},
|
|
deriveUserSseSubscription({
|
|
userId: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
resourceId: "res_self",
|
|
}),
|
|
);
|
|
|
|
eventBus.emit(
|
|
SSE_EVENT_TYPES.ALLOCATION_UPDATED,
|
|
{ id: "allocation_1", resourceId: "res_other" },
|
|
[permissionAudience(PermissionKey.MANAGE_ALLOCATIONS), resourceAudience("res_other")],
|
|
);
|
|
|
|
vi.advanceTimersByTime(50);
|
|
|
|
expect(received).toHaveLength(0);
|
|
unsubscribe();
|
|
});
|
|
|
|
it("does not leak personal notifications to a manager subscribed with elevated permissions", () => {
|
|
const received: SseEvent[] = [];
|
|
const unsubscribe = eventBus.subscribe(
|
|
(event) => {
|
|
received.push(event);
|
|
},
|
|
deriveUserSseSubscription({
|
|
userId: "manager_1",
|
|
systemRole: SystemRole.MANAGER,
|
|
}),
|
|
);
|
|
|
|
eventBus.emit(
|
|
SSE_EVENT_TYPES.NOTIFICATION_CREATED,
|
|
{ notificationId: "notification_1", userId: "user_2" },
|
|
[userAudience("user_2")],
|
|
);
|
|
|
|
vi.advanceTimersByTime(50);
|
|
|
|
expect(received).toHaveLength(0);
|
|
unsubscribe();
|
|
});
|
|
|
|
it("delivers a multi-audience planning event only to matching manager and affected resource subscribers", () => {
|
|
const managerReceived: SseEvent[] = [];
|
|
const affectedUserReceived: SseEvent[] = [];
|
|
const unrelatedUserReceived: SseEvent[] = [];
|
|
|
|
const unsubscribeManager = eventBus.subscribe(
|
|
(event) => {
|
|
managerReceived.push(event);
|
|
},
|
|
deriveUserSseSubscription({
|
|
userId: "manager_1",
|
|
systemRole: SystemRole.MANAGER,
|
|
}),
|
|
);
|
|
|
|
const unsubscribeAffectedUser = eventBus.subscribe(
|
|
(event) => {
|
|
affectedUserReceived.push(event);
|
|
},
|
|
deriveUserSseSubscription({
|
|
userId: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
resourceId: "res_1",
|
|
}),
|
|
);
|
|
|
|
const unsubscribeUnrelatedUser = eventBus.subscribe(
|
|
(event) => {
|
|
unrelatedUserReceived.push(event);
|
|
},
|
|
deriveUserSseSubscription({
|
|
userId: "user_2",
|
|
systemRole: SystemRole.USER,
|
|
resourceId: "res_2",
|
|
}),
|
|
);
|
|
|
|
eventBus.emit(
|
|
SSE_EVENT_TYPES.ALLOCATION_UPDATED,
|
|
{ id: "allocation_1", resourceId: "res_1" },
|
|
[permissionAudience(PermissionKey.MANAGE_ALLOCATIONS), resourceAudience("res_1")],
|
|
);
|
|
|
|
vi.advanceTimersByTime(50);
|
|
|
|
expect(managerReceived).toHaveLength(1);
|
|
expect(affectedUserReceived).toHaveLength(1);
|
|
expect(unrelatedUserReceived).toHaveLength(0);
|
|
|
|
unsubscribeManager();
|
|
unsubscribeAffectedUser();
|
|
unsubscribeUnrelatedUser();
|
|
});
|
|
});
|