From 22dc3e71d629c86984ff9bf69bc76b365c38f346 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Mon, 8 Sep 2025 10:35:47 +0200 Subject: [PATCH] 71 implemented delete tenant route (#73) * 71 implemented delete tenant route * Fixed test --- .../__tests__/tenant-admin-service.test.ts | 290 ++++++++++++++++++ .../server/services/tenant-admin-service.ts | 192 +++++++++++- src/routes/api/tenants/[id]/+server.ts | 218 +++++++++++++ src/routes/api/tenants/tenant-routes.test.ts | 31 ++ 4 files changed, 730 insertions(+), 1 deletion(-) diff --git a/src/lib/server/services/__tests__/tenant-admin-service.test.ts b/src/lib/server/services/__tests__/tenant-admin-service.test.ts index 65cc89f..fe27a4a 100644 --- a/src/lib/server/services/__tests__/tenant-admin-service.test.ts +++ b/src/lib/server/services/__tests__/tenant-admin-service.test.ts @@ -23,6 +23,7 @@ vi.mock("../../db/tenant-config", () => ({ vi.mock("../tenant-migration-service", () => ({ TenantMigrationService: { createAndInitializeTenantDatabase: vi.fn(), + parseDatabaseUrl: vi.fn(), }, })); @@ -353,4 +354,293 @@ describe("TenantAdminService", () => { await expect(service.updateTenantConfig(configUpdates)).rejects.toThrow("Config error"); }); }); + + describe("deleteTenant", () => { + it("should delete tenant and all associated data successfully", async () => { + const tenantId = "tenant-123"; + const mockTenant = { + id: tenantId, + shortName: "test-clinic", + longName: "Test Clinic", + databaseUrl: "postgresql://user:pass@localhost:5432/test-clinic", + }; + + const mockDeletedUsers = [ + { id: "user-1", email: "admin@test.com", role: "TENANT_ADMIN" }, + { id: "user-2", email: "staff@test.com", role: "STAFF" }, + ]; + + const mockUpdatedGlobalAdmins = [ + { id: "global-1", email: "global@system.com", role: "GLOBAL_ADMIN" }, + ]; + + const mockDeletedConfigs = [ + { id: "config-1", name: "brandColor" }, + { id: "config-2", name: "maxChannels" }, + ]; + + const mockConfig = { setConfig: vi.fn() }; + + // Mock database query builders + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([mockTenant]), + }; + + const mockUpdateBuilder = { + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue(mockUpdatedGlobalAdmins), + }; + + const mockDeleteUserBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue(mockDeletedUsers), + }; + + const mockDeleteConfigBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue(mockDeletedConfigs), + }; + + const mockDeleteTenantBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([mockTenant]), + }; + + // Mock database connection ending + const mockAdminClient = { + unsafe: vi.fn().mockResolvedValue([]), + end: vi.fn().mockResolvedValue(undefined), + }; + + // Mock postgres import + vi.doMock("postgres", () => ({ + default: vi.fn(() => mockAdminClient), + })); + + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); + mockCentralDb.update.mockReturnValue(mockUpdateBuilder); + + // First delete call for users, second for configs, third for tenant + mockCentralDb.delete + .mockReturnValueOnce(mockDeleteUserBuilder) + .mockReturnValueOnce(mockDeleteConfigBuilder) + .mockReturnValueOnce(mockDeleteTenantBuilder); + + mockTenantMigrationService.parseDatabaseUrl.mockReturnValue({ + host: "localhost", + port: 5432, + database: "test-clinic", + username: "user", + password: "pass", + }); + + const service = await TenantAdminService.getTenantById(tenantId); + // + (service as any)["#tenant"] = mockTenant; // Set private field for testing + + const result = await service.deleteTenant(); + + // Verify user updates (global admins) + expect(mockCentralDb.update).toHaveBeenCalled(); + expect(mockUpdateBuilder.set).toHaveBeenCalledWith({ + tenantId: null, + updatedAt: expect.any(Date), + }); + + // Verify user deletions (non-global-admins) + expect(mockCentralDb.delete).toHaveBeenCalledTimes(3); + expect(mockDeleteUserBuilder.where).toHaveBeenCalled(); + expect(mockDeleteUserBuilder.returning).toHaveBeenCalled(); + + // Verify config deletions + expect(mockDeleteConfigBuilder.where).toHaveBeenCalled(); + expect(mockDeleteConfigBuilder.returning).toHaveBeenCalled(); + + // Verify database parsing and dropping + expect(mockTenantMigrationService.parseDatabaseUrl).toHaveBeenCalledWith( + mockTenant.databaseUrl, + ); + expect(mockAdminClient.unsafe).toHaveBeenCalledWith( + expect.stringContaining("pg_terminate_backend"), + ); + expect(mockAdminClient.unsafe).toHaveBeenCalledWith(`DROP DATABASE IF EXISTS "test-clinic"`); + expect(mockAdminClient.end).toHaveBeenCalled(); + + // Verify tenant deletion + expect(mockDeleteTenantBuilder.where).toHaveBeenCalled(); + expect(mockDeleteTenantBuilder.returning).toHaveBeenCalled(); + + // Verify result + expect(result).toEqual({ + tenantId, + shortName: "test-clinic", + deletedUsersCount: 2, + updatedGlobalAdminsCount: 1, + deletedConfigsCount: 2, + deletedUsers: mockDeletedUsers, + updatedGlobalAdmins: mockUpdatedGlobalAdmins, + }); + }); + + it("should throw NotFoundError when tenant does not exist", async () => { + const tenantId = "non-existent"; + + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), // Empty result + }; + + const mockConfig = { setConfig: vi.fn() }; + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); + + const service = await TenantAdminService.getTenantById(tenantId); + + await expect(service.deleteTenant()).rejects.toThrow("Tenant with ID non-existent not found"); + }); + + it("should continue with deletion even if database drop fails", async () => { + const tenantId = "tenant-123"; + const mockTenant = { + id: tenantId, + shortName: "test-clinic", + longName: "Test Clinic", + databaseUrl: "postgresql://user:pass@localhost:5432/test-clinic", + }; + + const mockConfig = { setConfig: vi.fn() }; + + // Mock successful database operations but failing database drop + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([mockTenant]), + }; + + const mockUpdateBuilder = { + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }; + + const mockDeleteBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }; + + const mockDeleteTenantBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([mockTenant]), + }; + + // Mock failing database drop + const mockAdminClient = { + unsafe: vi.fn().mockRejectedValue(new Error("Database drop failed")), + end: vi.fn().mockResolvedValue(undefined), + }; + + vi.doMock("postgres", () => ({ + default: vi.fn(() => mockAdminClient), + })); + + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); + mockCentralDb.update.mockReturnValue(mockUpdateBuilder); + mockCentralDb.delete + .mockReturnValueOnce(mockDeleteBuilder) // users + .mockReturnValueOnce(mockDeleteBuilder) // configs + .mockReturnValueOnce(mockDeleteTenantBuilder); // tenant + + mockTenantMigrationService.parseDatabaseUrl.mockReturnValue({ + host: "localhost", + port: 5432, + database: "test-clinic", + username: "user", + password: "pass", + }); + + const service = await TenantAdminService.getTenantById(tenantId); + (service as any)["#tenant"] = mockTenant; + + // Should not throw error even if database drop fails + const result = await service.deleteTenant(); + + expect(result.tenantId).toBe(tenantId); + expect(result.shortName).toBe("test-clinic"); + + // Verify tenant record was still deleted + expect(mockDeleteTenantBuilder.where).toHaveBeenCalled(); + expect(mockDeleteTenantBuilder.returning).toHaveBeenCalled(); + }); + + it("should throw error if tenant record deletion fails", async () => { + const tenantId = "tenant-123"; + const mockTenant = { + id: tenantId, + shortName: "test-clinic", + longName: "Test Clinic", + databaseUrl: "postgresql://user:pass@localhost:5432/test-clinic", + }; + + const mockConfig = { setConfig: vi.fn() }; + + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([mockTenant]), + }; + + const mockUpdateBuilder = { + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }; + + const mockDeleteBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }; + + const mockDeleteTenantBuilder = { + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), // Empty result = tenant not found + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); + mockCentralDb.update.mockReturnValue(mockUpdateBuilder); + mockCentralDb.delete + .mockReturnValueOnce(mockDeleteBuilder) // users + .mockReturnValueOnce(mockDeleteBuilder) // configs + .mockReturnValueOnce(mockDeleteTenantBuilder); // tenant + + mockTenantMigrationService.parseDatabaseUrl.mockReturnValue({ + host: "localhost", + port: 5432, + database: "test-clinic", + username: "user", + password: "pass", + }); + + // Mock successful database operations + const mockAdminClient = { + unsafe: vi.fn().mockResolvedValue([]), + end: vi.fn().mockResolvedValue(undefined), + }; + + vi.doMock("postgres", () => ({ + default: vi.fn(() => mockAdminClient), + })); + + const service = await TenantAdminService.getTenantById(tenantId); + (service as any)["#tenant"] = mockTenant; + + await expect(service.deleteTenant()).rejects.toThrow("Tenant with ID tenant-123 not found"); + }); + }); }); diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index 861442b..3bdb5f4 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -5,7 +5,7 @@ import { TenantConfig } from "../db/tenant-config"; import { TenantMigrationService } from "./tenant-migration-service"; import { env } from "$env/dynamic/private"; -import { eq } from "drizzle-orm"; +import { eq, and, not } from "drizzle-orm"; import logger from "$lib/logger"; import z from "zod/v4"; import { ValidationError, NotFoundError, ConflictError } from "../utils/errors"; @@ -373,4 +373,194 @@ export class TenantAdminService { return this.#db; } + + /** + * Delete a tenant and all associated data + * This operation: + * - Drops the tenant's isolated database + * - Deletes all non-global-admin users associated with the tenant + * - Removes tenant assignment from global admins (sets tenantId to null) + * - Removes the tenant record from the central database + * - Cleans up tenant configuration entries + * + * @throws {NotFoundError} If the tenant doesn't exist + * @throws {Error} If database operations fail + */ + async deleteTenant() { + const log = logger.setContext("TenantAdminService"); + log.info("Starting tenant deletion process", { tenantId: this.tenantId }); + + // First, get tenant data to ensure it exists + if (!this.#tenant) { + const tenantData = await centralDb + .select() + .from(centralSchema.tenant) + .where(eq(centralSchema.tenant.id, this.tenantId)) + .limit(1); + + if (!tenantData[0]) { + log.warn("Tenant deletion failed: Tenant not found", { tenantId: this.tenantId }); + throw new NotFoundError(`Tenant with ID ${this.tenantId} not found`); + } + + this.#tenant = tenantData[0]; + } + + const tenantDbUrl = this.#tenant.databaseUrl; + const tenantShortName = this.#tenant.shortName; + + try { + // Step 1: Handle users associated with this tenant + log.debug("Processing tenant users", { tenantId: this.tenantId }); + + // First, remove tenant assignment from global admins (they must not be deleted) + const updatedGlobalAdmins = await centralDb + .update(centralSchema.user) + .set({ + tenantId: null, + updatedAt: new Date(), + }) + .where( + and( + eq(centralSchema.user.tenantId, this.tenantId), + eq(centralSchema.user.role, "GLOBAL_ADMIN"), + ), + ) + .returning({ + id: centralSchema.user.id, + email: centralSchema.user.email, + role: centralSchema.user.role, + }); + + log.info("Removed tenant assignment from global admins", { + tenantId: this.tenantId, + updatedCount: updatedGlobalAdmins.length, + updatedAdmins: updatedGlobalAdmins.map((u) => ({ id: u.id, email: u.email, role: u.role })), + }); + + // Then, delete non-global-admin users (TENANT_ADMIN, STAFF) + const deletedUsers = await centralDb + .delete(centralSchema.user) + .where( + and( + eq(centralSchema.user.tenantId, this.tenantId), + not(eq(centralSchema.user.role, "GLOBAL_ADMIN")), + ), + ) + .returning({ + id: centralSchema.user.id, + email: centralSchema.user.email, + role: centralSchema.user.role, + }); + + log.info("Deleted non-global-admin tenant users", { + tenantId: this.tenantId, + deletedCount: deletedUsers.length, + deletedUsers: deletedUsers.map((u) => ({ id: u.id, email: u.email, role: u.role })), + }); + + // Step 2: Delete tenant configuration entries + log.debug("Deleting tenant configuration entries", { tenantId: this.tenantId }); + + const deletedConfigs = await centralDb + .delete(centralSchema.tenantConfig) + .where(eq(centralSchema.tenantConfig.tenantId, this.tenantId)) + .returning({ id: centralSchema.tenantConfig.id, name: centralSchema.tenantConfig.name }); + + log.info("Deleted tenant configuration entries", { + tenantId: this.tenantId, + deletedCount: deletedConfigs.length, + }); + + // Step 3: Drop the tenant database + log.debug("Dropping tenant database", { + tenantId: this.tenantId, + databaseUrl: tenantDbUrl, + shortName: tenantShortName, + }); + + try { + const dbConfig = TenantMigrationService.parseDatabaseUrl(tenantDbUrl); + const adminConnectionString = `postgres://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/postgres`; + + // Import postgres here to avoid top-level import issues + const { default: postgres } = await import("postgres"); + const adminClient = postgres(adminConnectionString); + + try { + // Terminate all connections to the database first + await adminClient.unsafe(` + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '${dbConfig.database}' + AND pid <> pg_backend_pid() + `); + + // Drop the database + await adminClient.unsafe(`DROP DATABASE IF EXISTS "${dbConfig.database}"`); + + log.info("Tenant database dropped successfully", { + tenantId: this.tenantId, + database: dbConfig.database, + }); + } finally { + await adminClient.end(); + } + } catch (dbError) { + log.error("Failed to drop tenant database", { + tenantId: this.tenantId, + databaseUrl: tenantDbUrl, + error: String(dbError), + }); + // Continue with tenant record deletion even if database drop fails + // This allows cleanup of orphaned tenant records + } + + // Step 4: Delete the tenant record from central database + log.debug("Deleting tenant record from central database", { tenantId: this.tenantId }); + + const deletedTenant = await centralDb + .delete(centralSchema.tenant) + .where(eq(centralSchema.tenant.id, this.tenantId)) + .returning(); + + if (!deletedTenant[0]) { + log.error("Failed to delete tenant record: Tenant not found", { tenantId: this.tenantId }); + throw new NotFoundError(`Tenant with ID ${this.tenantId} not found`); + } + + log.info("Tenant deletion completed successfully", { + tenantId: this.tenantId, + shortName: tenantShortName, + deletedUsersCount: deletedUsers.length, + updatedGlobalAdminsCount: updatedGlobalAdmins.length, + deletedConfigsCount: deletedConfigs.length, + }); + + // Clear cached data + this.#tenant = null; + this.#db = null; + + return { + tenantId: this.tenantId, + shortName: tenantShortName, + deletedUsersCount: deletedUsers.length, + updatedGlobalAdminsCount: updatedGlobalAdmins.length, + deletedConfigsCount: deletedConfigs.length, + deletedUsers: deletedUsers.map((u) => ({ id: u.id, email: u.email, role: u.role })), + updatedGlobalAdmins: updatedGlobalAdmins.map((u) => ({ + id: u.id, + email: u.email, + role: u.role, + })), + }; + } catch (error) { + log.error("Tenant deletion failed", { + tenantId: this.tenantId, + shortName: tenantShortName, + error: String(error), + }); + throw error; + } + } } diff --git a/src/routes/api/tenants/[id]/+server.ts b/src/routes/api/tenants/[id]/+server.ts index 8674620..509b75d 100644 --- a/src/routes/api/tenants/[id]/+server.ts +++ b/src/routes/api/tenants/[id]/+server.ts @@ -1,6 +1,7 @@ import { json } from "@sveltejs/kit"; import { TenantAdminService } from "$lib/server/services/tenant-admin-service"; import { ValidationError, NotFoundError } from "$lib/server/utils/errors"; +import { AuthorizationService } from "$lib/server/auth/authorization-service"; import type { RequestHandler } from "@sveltejs/kit"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import logger from "$lib/logger"; @@ -227,6 +228,158 @@ registerOpenAPIRoute("/tenants/{id}", "GET", { }, }); +// Register OpenAPI documentation for DELETE +registerOpenAPIRoute("/tenants/{id}", "DELETE", { + summary: "Delete tenant", + description: + "Permanently deletes a tenant and all associated data. This operation drops the tenant's database, removes tenant assignments from global admins, deletes non-global-admin users, and removes the tenant record. Only global admins can perform this operation.", + tags: ["Tenants"], + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "Tenant ID to delete", + }, + ], + responses: { + "200": { + description: "Tenant deleted successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { type: "string", description: "Success message" }, + result: { + type: "object", + properties: { + tenantId: { type: "string", format: "uuid", description: "Deleted tenant ID" }, + shortName: { type: "string", description: "Deleted tenant short name" }, + deletedUsersCount: { type: "number", description: "Number of users deleted" }, + updatedGlobalAdminsCount: { + type: "number", + description: "Number of global admins whose tenant assignment was removed", + }, + deletedConfigsCount: { + type: "number", + description: "Number of configuration entries deleted", + }, + deletedUsers: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string", format: "uuid" }, + email: { type: "string" }, + role: { type: "string" }, + }, + }, + description: "List of deleted users", + }, + updatedGlobalAdmins: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string", format: "uuid" }, + email: { type: "string" }, + role: { type: "string" }, + }, + }, + description: "List of global admins whose tenant assignment was removed", + }, + }, + required: [ + "tenantId", + "shortName", + "deletedUsersCount", + "updatedGlobalAdminsCount", + "deletedConfigsCount", + "deletedUsers", + "updatedGlobalAdmins", + ], + }, + }, + required: ["message", "result"], + }, + example: { + message: "Tenant deleted successfully", + result: { + tenantId: "01234567-89ab-cdef-0123-456789abcdef", + shortName: "acme-corp", + deletedUsersCount: 3, + updatedGlobalAdminsCount: 1, + deletedConfigsCount: 12, + deletedUsers: [ + { + id: "11111111-1111-1111-1111-111111111111", + email: "admin@acme.com", + role: "TENANT_ADMIN", + }, + { + id: "22222222-2222-2222-2222-222222222222", + email: "staff1@acme.com", + role: "STAFF", + }, + { + id: "33333333-3333-3333-3333-333333333333", + email: "staff2@acme.com", + role: "STAFF", + }, + ], + updatedGlobalAdmins: [ + { + id: "44444444-4444-4444-4444-444444444444", + email: "global@system.com", + role: "GLOBAL_ADMIN", + }, + ], + }, + }, + }, + }, + }, + "401": { + description: "Authentication required", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + example: { error: "Authentication required" }, + }, + }, + }, + "403": { + description: "Insufficient permissions (only global admins can delete tenants)", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + example: { error: "Insufficient permissions" }, + }, + }, + }, + "404": { + description: "Tenant not found", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + example: { error: "Tenant not found" }, + }, + }, + }, + "500": { + description: "Internal server error", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Error" }, + example: { error: "Internal server error" }, + }, + }, + }, + }, +}); + export const PUT: RequestHandler = async ({ params, request }) => { const log = logger.setContext("API"); @@ -329,3 +482,68 @@ export const GET: RequestHandler = async ({ params, locals }) => { return json({ error: "Internal server error" }, { status: 500 }); } }; + +export const DELETE: RequestHandler = async ({ params, locals }) => { + const log = logger.setContext("API"); + + try { + const tenantId = params.id; + + // Check if user is authenticated + if (!locals.user) { + return json({ error: "Authentication required" }, { status: 401 }); + } + + if (!tenantId) { + return json({ error: "No tenant id given" }, { status: 400 }); + } + + log.info("Attempting tenant deletion", { + tenantId, + requestedBy: locals.user.userId, + userRole: locals.user.role, + }); + + // Authorization: Only global admins can delete tenants + try { + AuthorizationService.requireGlobalAdmin(locals.user); + } catch { + log.warn("Tenant deletion denied: insufficient permissions", { + tenantId, + requestedBy: locals.user.userId, + userRole: locals.user.role, + }); + return json({ error: "Insufficient permissions" }, { status: 403 }); + } + + // Get tenant service and perform deletion + const tenantService = await TenantAdminService.getTenantById(tenantId); + const result = await tenantService.deleteTenant(); + + log.info("Tenant deletion completed successfully", { + tenantId, + shortName: result.shortName, + deletedUsersCount: result.deletedUsersCount, + updatedGlobalAdminsCount: result.updatedGlobalAdminsCount, + deletedConfigsCount: result.deletedConfigsCount, + requestedBy: locals.user.userId, + }); + + return json({ + message: "Tenant deleted successfully", + result, + }); + } catch (error) { + log.error("Error deleting tenant", { + tenantId: params.id, + requestedBy: locals.user?.userId, + error: JSON.stringify(error || "?"), + }); + + if (error instanceof NotFoundError) { + return json({ error: "Tenant not found" }, { status: 404 }); + } + + return json({ error: "Internal server error" }, { status: 500 }); + } +}; diff --git a/src/routes/api/tenants/tenant-routes.test.ts b/src/routes/api/tenants/tenant-routes.test.ts index 3441227..7fd912a 100644 --- a/src/routes/api/tenants/tenant-routes.test.ts +++ b/src/routes/api/tenants/tenant-routes.test.ts @@ -29,6 +29,18 @@ vi.mock("$lib/logger", () => ({ error: vi.fn(), })), }, + UniversalLogger: vi.fn().mockImplementation(() => ({ + setContext: vi.fn(() => ({ + debug: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + })), + debug: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + })), })); vi.mock("$lib/server/utils/errors", () => ({ @@ -46,6 +58,25 @@ vi.mock("$lib/server/utils/errors", () => ({ }, })); +vi.mock("$lib/server/auth/authorization-service", () => ({ + AuthorizationService: { + requireGlobalAdmin: vi.fn(), + requireRole: vi.fn(), + requireAnyRole: vi.fn(), + requireTenantAccess: vi.fn(), + requireTenantAdmin: vi.fn(), + requireStaffOrAbove: vi.fn(), + canAccessTenant: vi.fn(), + isGlobalAdmin: vi.fn(), + isTenantAdmin: vi.fn(), + isStaff: vi.fn(), + hasRole: vi.fn(), + hasAnyRole: vi.fn(), + getUserTenantId: vi.fn(), + }, + withAuthorization: vi.fn(), +})); + describe("Tenant API Routes", () => { beforeEach(() => { vi.clearAllMocks();