From 7a955b3a70f2c204c290b5efaba27e4af8bf32c2 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Tue, 28 Apr 2026 20:54:47 +0200 Subject: [PATCH] Fix: Granting staff access to appointments --- project.inlang/messages/de.json | 2 +- src/lib/client/appointment-crypto.ts | 20 + .../grant-access-form.svelte | 2 +- src/routes/api/auth/register/[id]/+server.ts | 15 + .../appointments/[appointmentId]/+server.ts | 2 - .../staff-public-keys-by-staff/+server.ts | 123 ++++++ .../__tests__/staff-public-keys-api.test.ts | 385 ++++++++++++++++++ .../tunnels/add-staff-key-shares/+server.ts | 14 +- .../api/tenants/[id]/calendar/+server.ts | 3 + .../calendar/__tests__/calendar-api.test.ts | 16 +- 10 files changed, 568 insertions(+), 14 deletions(-) create mode 100644 src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/+server.ts create mode 100644 src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/__tests__/staff-public-keys-api.test.ts diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index d8d92a2..5361931 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -1036,7 +1036,7 @@ "reason": "Sie erhalten diese E-Mail, weil jemand für die PIN für Ihr Konto geändert hat." }, "userInvite": { - "subject": "E-Mail Adresse bestätigen für {tenant}", + "subject": "E-Mail Adresse bestätigen", "introduction": "willkommen beim Terminbuchungsportal von {tenant}. Bitte bestätige Deine E-Mail Adresse.", "action": "E-Mail Adresse bestätigen", "hint": "Dieser Link ist nur {expirationMinutes} Minuten gültig und kann nur einmal verwendet werden.", diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts index 75d8101..546e3b9 100644 --- a/src/lib/client/appointment-crypto.ts +++ b/src/lib/client/appointment-crypto.ts @@ -1247,6 +1247,26 @@ export class UnifiedAppointmentCrypto { return data.staffPublicKeys; } + async fetchStaffPublicKeysByStaff(tenantId: string): Promise { + const response = await fetch( + `/api/tenants/${tenantId}/appointments/staff-public-keys-by-staff`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.bookingAccessToken}`, + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch staff public keys for staff: ${response.statusText}`); + } + + const data = await response.json(); + return data.staffPublicKeys; + } + private async bootstrapNewClientAccess(tenantId: string): Promise { if (!this.tunnelId || !this.clientKeyPair) { throw new Error("Bootstrap requires generated client tunnel and key pair"); diff --git a/src/routes/(pages)/dashboard/staff/(components)/grant-access-form/grant-access-form.svelte b/src/routes/(pages)/dashboard/staff/(components)/grant-access-form/grant-access-form.svelte index 5d969ec..c2e26a5 100644 --- a/src/routes/(pages)/dashboard/staff/(components)/grant-access-form/grant-access-form.svelte +++ b/src/routes/(pages)/dashboard/staff/(components)/grant-access-form/grant-access-form.svelte @@ -35,7 +35,7 @@ step = "add-staff-key-shares"; // For each tunnel: decrypt currentStaffEncryptedTunnelKey with current user private key - const allPublicKeys = await cryptoClient.fetchStaffPublicKeys(tenantId); + const allPublicKeys = await cryptoClient.fetchStaffPublicKeysByStaff(tenantId); const newUserPublicKeys = allPublicKeys.filter((x) => x.userId === entity.id); if (newUserPublicKeys.length === 0) { diff --git a/src/routes/api/auth/register/[id]/+server.ts b/src/routes/api/auth/register/[id]/+server.ts index f740de3..e51fbda 100644 --- a/src/routes/api/auth/register/[id]/+server.ts +++ b/src/routes/api/auth/register/[id]/+server.ts @@ -5,6 +5,7 @@ import { BackendError, InternalError, logError, ValidationError } from "$lib/ser import type { RequestHandler } from "./$types"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import logger from "$lib/logger"; +import { AppointmentService } from "$lib/server/services/appointment-service"; // Register OpenAPI documentation registerOpenAPIRoute("/auth/register", "POST", { @@ -154,6 +155,20 @@ export const POST: RequestHandler = async ({ params, cookies, request, url }) => counter: verificationResult.counter, }); + // Set user to ACCESS_GRANTED, if no appointments exists + try { + const user = await UserService.getUserByEmail(body.email); + if (user.tenantId) { + const appointmentService = await AppointmentService.forTenant(user.tenantId); + const hasAppointments = await appointmentService.hasAppointments(); + if (!hasAppointments) { + await UserService.updateUser(user.id, { confirmationState: "ACCESS_GRANTED" }); + } + } + } catch { + // Silent fail for now + } + return json( { message: "User account now has a passkey.", diff --git a/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts b/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts index d71930f..15f2f9c 100644 --- a/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/[appointmentId]/+server.ts @@ -228,8 +228,6 @@ export const GET: RequestHandler = async ({ params, locals }) => { throw new ValidationError("Tenant ID and appointment ID are required"); } - // checkPermission(locals, tenantId, true); - log.debug("Getting appointment by ID", { tenantId, appointmentId, diff --git a/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/+server.ts b/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/+server.ts new file mode 100644 index 0000000..a9b47f2 --- /dev/null +++ b/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/+server.ts @@ -0,0 +1,123 @@ +import { logger } from "$lib/logger"; +import { registerOpenAPIRoute } from "$lib/server/openapi"; +import { StaffCryptoService } from "$lib/server/services/staff-crypto.service"; +import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors"; +import { checkPermission } from "$lib/server/utils/permissions"; +import { json, type RequestHandler } from "@sveltejs/kit"; + +// Register OpenAPI documentation for GET +registerOpenAPIRoute("/tenants/{id}/appointments/staff-public-keys-by-staff", "GET", { + summary: "Get staff public keys", + description: + "Returns public encryption keys for all staff members. Requires a valid access token.", + tags: ["GrantAccess"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID", + }, + ], + responses: { + "200": { + description: "Staff public keys retrieved successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + staffPublicKeys: { + type: "array", + items: { + type: "object", + properties: { + userId: { + type: "string", + format: "uuid", + description: "Staff member's user ID", + }, + publicKey: { + type: "string", + description: "ML-KEM-768 public key (hex encoded)", + example: "deadbeef123456789abcdef...", + }, + }, + required: ["userId", "publicKey"], + }, + description: "List of staff public keys for encryption", + }, + }, + required: ["staffPublicKeys"], + }, + }, + }, + }, + "400": { + description: "Invalid tenant ID", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "401": { + description: "Missing or invalid cookie access token", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + "500": { + description: "Internal server error or no staff keys found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + }, + }, + }, + }, +}); + +/** + * GET /api/tenants/[id]/appointments/staff-public-keys-by-staff + * + * Returns the public keys of all staff members for encryption. + * Requires booking access token from verify-challenge or bootstrap-verify endpoint. + */ +export const GET: RequestHandler = async ({ params, locals }) => { + try { + const tenantId = params.id; + if (!tenantId) { + throw new ValidationError("Tenant ID is required"); + } + + checkPermission(locals, tenantId, false); + + logger.info("Fetching staff public keys", { tenantId }); + + // Get staff public keys for encryption + const staffCryptoService = new StaffCryptoService(); + const staffPublicKeys = await staffCryptoService.getStaffPublicKeys(tenantId); + + if (staffPublicKeys.length === 0) { + logger.warn("No staff public keys found for tenant", { tenantId }); + throw new InternalError("No staff members with encryption keys found"); + } + + logger.info("Successfully retrieved staff public keys", { + tenantId, + staffCount: staffPublicKeys.length, + }); + + return json({ staffPublicKeys }); + } catch (error) { + logError(logger)("Failed to fetch staff public keys", error); + if (error instanceof BackendError) { + return error.toJson(); + } + return new InternalError().toJson(); + } +}; diff --git a/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/__tests__/staff-public-keys-api.test.ts b/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/__tests__/staff-public-keys-api.test.ts new file mode 100644 index 0000000..d75dc14 --- /dev/null +++ b/src/routes/api/tenants/[id]/appointments/staff-public-keys-by-staff/__tests__/staff-public-keys-api.test.ts @@ -0,0 +1,385 @@ +import { logger } from "$lib/logger"; +import { StaffCryptoService } from "$lib/server/services/staff-crypto.service"; +import { AuthenticationError, AuthorizationError } from "$lib/server/utils/errors"; +import { checkPermission } from "$lib/server/utils/permissions"; +import type { RequestEvent } from "@sveltejs/kit"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { GET } from "../+server"; + +// Mock dependencies +vi.mock("$lib/logger"); +vi.mock("$lib/server/services/staff-crypto.service"); +vi.mock("$lib/server/utils/permissions"); +vi.mock("$lib/server/openapi"); + +const mockCookies = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), +}; + +const createMockRequestEvent = (tenantId: string, locals: unknown = {}): RequestEvent => + ({ + params: { id: tenantId }, + request: new Request( + `http://localhost/api/tenants/${tenantId}/appointments/staff-public-keys-by-staff`, + ), + url: new URL( + `http://localhost/api/tenants/${tenantId}/appointments/staff-public-keys-by-staff`, + ), + route: { id: "/api/tenants/[id]/appointments/staff-public-keys-by-staff" }, + locals, + cookies: mockCookies, + fetch: global.fetch, + getClientAddress: () => "127.0.0.1", + isDataRequest: false, + platform: undefined, + setHeaders: vi.fn(), + depends: vi.fn(), + parent: vi.fn(), + }) as unknown as RequestEvent; + +describe("GET /api/tenants/[id]/appointments/staff-public-keys-by-staff", () => { + const mockTenantId = "550e8400-e29b-41d4-a716-446655440000"; + const mockStaffId1 = "f47ac10b-58cc-4372-a567-0e02b2c3d479"; + const mockStaffId2 = "f47ac10b-58cc-4372-a567-0e02b2c3d480"; + + const mockStaffPublicKeys = [ + { + userId: mockStaffId1, + publicKey: "deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678", + passkeyId: "key1", + }, + { + userId: mockStaffId2, + publicKey: "cafebabe1234567890abcdef1234567890abcdef1234567890abcdef12345678", + passkeyId: "key2", + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(logger.info).mockImplementation(() => {}); + vi.mocked(logger.warn).mockImplementation(() => {}); + }); + + describe("Success Cases", () => { + it("should return staff public keys for authenticated user with valid cookie", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + mockStaffPublicKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.staffPublicKeys).toEqual(mockStaffPublicKeys); + expect(data.staffPublicKeys).toHaveLength(2); + }); + + it("should return multiple staff public keys", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + + const manyStaffKeys = Array.from({ length: 5 }, (_, i) => ({ + userId: `staff-${i}`, + publicKey: `key-${i}${"a".repeat(60)}`, + passkeyId: `passkey-${i}`, + })); + + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + manyStaffKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.staffPublicKeys).toHaveLength(5); + expect(data.staffPublicKeys[0]).toHaveProperty("userId"); + expect(data.staffPublicKeys[0]).toHaveProperty("publicKey"); + }); + + it("should call StaffCryptoService with correct tenant ID", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + + const mockService = { + getStaffPublicKeys: vi.fn().mockResolvedValueOnce(mockStaffPublicKeys), + }; + + vi.mocked(StaffCryptoService).mockImplementationOnce( + () => mockService as unknown as StaffCryptoService, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + await GET(event); + + expect(mockService.getStaffPublicKeys).toHaveBeenCalledWith(mockTenantId); + expect(mockService.getStaffPublicKeys).toHaveBeenCalledOnce(); + }); + + it("should call checkPermission with tenant ID and false flag", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + mockStaffPublicKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + await GET(event); + + expect(vi.mocked(checkPermission)).toHaveBeenCalledWith(locals, mockTenantId, false); + expect(vi.mocked(checkPermission)).toHaveBeenCalledOnce(); + }); + }); + + describe("Validation & Error Cases", () => { + it("should return 422 when tenant ID is missing (ValidationError from SvelteKit)", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + const event = createMockRequestEvent("", locals); + const response = await GET(event); + + // ValidationError returns 422 in SvelteKit + expect(response.status).toBe(422); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + it("should return 401 when user is not authenticated", async () => { + vi.mocked(checkPermission).mockImplementationOnce(() => { + throw new AuthenticationError("Authentication required"); + }); + + const locals = { user: null }; + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + + expect(response.status).toBe(401); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + it("should return 401 when locals are missing user", async () => { + vi.mocked(checkPermission).mockImplementationOnce(() => { + throw new AuthenticationError("Authentication required"); + }); + + const event = createMockRequestEvent(mockTenantId, {}); + const response = await GET(event); + + expect(response.status).toBe(401); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + it("should return 403 when user lacks permission for tenant", async () => { + vi.mocked(checkPermission).mockImplementationOnce(() => { + throw new AuthorizationError("Insufficient permissions"); + }); + + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: "different-tenant-id", + }, + }; + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + + expect(response.status).toBe(403); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + it("should return 500 when no staff keys are found", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce([]); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + + expect(response.status).toBe(500); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + it("should return 500 when StaffCryptoService throws an error", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + + const serviceError = new Error("Database connection failed"); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockRejectedValueOnce( + serviceError, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + + expect(response.status).toBe(500); + }); + }); + + describe("Logging", () => { + it("should log info when successfully fetching staff keys", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + mockStaffPublicKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + await GET(event); + + expect(vi.mocked(logger.info)).toHaveBeenCalledWith("Fetching staff public keys", { + tenantId: mockTenantId, + }); + expect(vi.mocked(logger.info)).toHaveBeenCalledWith( + "Successfully retrieved staff public keys", + { + tenantId: mockTenantId, + staffCount: 2, + }, + ); + }); + + it("should log warning when no staff keys found", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce([]); + + const event = createMockRequestEvent(mockTenantId, locals); + await GET(event); + + expect(vi.mocked(logger.warn)).toHaveBeenCalledWith("No staff public keys found for tenant", { + tenantId: mockTenantId, + }); + }); + }); + + describe("Response Format", () => { + it("should return properly formatted JSON response", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + mockStaffPublicKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + + expect(response.headers.get("content-type")).toContain("application/json"); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toHaveProperty("staffPublicKeys"); + expect(Array.isArray(data.staffPublicKeys)).toBe(true); + }); + + it("should include complete staff key objects with userId and publicKey", async () => { + const locals = { + user: { + userId: "booking-user-id", + role: "GUEST", + tenantId: mockTenantId, + }, + }; + + vi.mocked(checkPermission).mockImplementationOnce(() => {}); + vi.mocked(StaffCryptoService.prototype.getStaffPublicKeys).mockResolvedValueOnce( + mockStaffPublicKeys, + ); + + const event = createMockRequestEvent(mockTenantId, locals); + const response = await GET(event); + const data = await response.json(); + + data.staffPublicKeys.forEach( + (key: { userId: string; publicKey: string; passkeyId: string }) => { + expect(key).toHaveProperty("userId"); + expect(key).toHaveProperty("publicKey"); + expect(key).toHaveProperty("passkeyId"); + expect(typeof key.userId).toBe("string"); + expect(typeof key.publicKey).toBe("string"); + }, + ); + }); + }); +}); diff --git a/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts b/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts index da17ac5..787530c 100644 --- a/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts @@ -140,14 +140,12 @@ registerOpenAPIRoute("/tenants/{id}/appointments/tunnels/add-staff-key-shares", const requestSchema = z.object({ staffUserId: z.string().uuid("Invalid staff user ID format"), - keyShares: z - .array( - z.object({ - tunnelId: z.string().uuid("Invalid tunnel ID format"), - encryptedTunnelKey: z.string().min(1, "Encrypted tunnel key cannot be empty"), - }), - ) - .min(1, "At least one key share is required"), + keyShares: z.array( + z.object({ + tunnelId: z.string().uuid("Invalid tunnel ID format"), + encryptedTunnelKey: z.string().min(1, "Encrypted tunnel key cannot be empty"), + }), + ), }); export const POST: RequestHandler = async ({ params, locals, request }) => { diff --git a/src/routes/api/tenants/[id]/calendar/+server.ts b/src/routes/api/tenants/[id]/calendar/+server.ts index b0c8402..6bb9169 100644 --- a/src/routes/api/tenants/[id]/calendar/+server.ts +++ b/src/routes/api/tenants/[id]/calendar/+server.ts @@ -4,6 +4,7 @@ import { BackendError, InternalError, logError, ValidationError } from "$lib/ser import type { RequestHandler } from "@sveltejs/kit"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import logger from "$lib/logger"; +import { checkPermission } from "$lib/server/utils/permissions"; // Register OpenAPI documentation for GET registerOpenAPIRoute("/tenants/{id}/calendar", "GET", { @@ -129,6 +130,8 @@ export const GET: RequestHandler = async ({ params, url, locals }) => { throw new ValidationError("Tenant ID is required"); } + checkPermission(locals, tenantId, false); + // Parse query parameters for date range const startDateParam = url.searchParams.get("startDate"); const endDateParam = url.searchParams.get("endDate"); diff --git a/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts b/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts index c39f778..8b7d48d 100644 --- a/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts +++ b/src/routes/api/tenants/[id]/calendar/__tests__/calendar-api.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { GET } from "../+server"; import { ScheduleService } from "$lib/server/services/schedule-service"; +import { checkPermission } from "$lib/server/utils/permissions"; // Mock the ScheduleService vi.mock("$lib/server/services/schedule-service", () => ({ @@ -9,6 +10,9 @@ vi.mock("$lib/server/services/schedule-service", () => ({ }, })); +// Mock checkPermission +vi.mock("$lib/server/utils/permissions"); + // Mock logger vi.mock("$lib/logger", () => ({ default: { @@ -35,6 +39,8 @@ describe("Calendar API", () => { mockGetSchedule.mockReset(); // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.mocked(ScheduleService.forTenant).mockResolvedValue(mockScheduleService as any); + // Mock checkPermission to succeed by default + vi.mocked(checkPermission).mockImplementation(() => {}); }); const createRequest = (tenantId: string, startDate?: string, endDate?: string) => { @@ -45,7 +51,13 @@ describe("Calendar API", () => { return { params: { id: tenantId }, url, - locals: {}, // Add locals object + locals: { + user: { + id: "user-123", + tenantId, + role: "GUEST", + }, + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; }; @@ -143,7 +155,7 @@ describe("Calendar API", () => { startDate: "2024-01-01T00:00:00.000Z", endDate: "2024-01-02T00:00:00.000Z", timeZone: "UTC", - staffUserId: undefined, + staffUserId: "user-123", }); });