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 335c8a5..65cc89f 100644 --- a/src/lib/server/services/__tests__/tenant-admin-service.test.ts +++ b/src/lib/server/services/__tests__/tenant-admin-service.test.ts @@ -84,11 +84,17 @@ describe("TenantAdminService", () => { returning: vi.fn().mockResolvedValue([mockCreatedTenant]), }; + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue([]), + }; + const mockDeleteBuilder = { where: vi.fn().mockResolvedValue({ count: 1 }), }; mockCentralDb.insert.mockReturnValue(mockInsertBuilder); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); mockCentralDb.delete.mockReturnValue(mockDeleteBuilder); mockTenantConfig.create.mockResolvedValue(mockConfig); mockTenantMigrationService.createAndInitializeTenantDatabase.mockResolvedValue(); @@ -126,11 +132,17 @@ describe("TenantAdminService", () => { returning: vi.fn().mockResolvedValue([mockCreatedTenant]), }; + const mockSelectBuilder = { + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue([]), + }; + const mockDeleteBuilder = { where: vi.fn().mockResolvedValue({ count: 1 }), }; mockCentralDb.insert.mockReturnValue(mockInsertBuilder); + mockCentralDb.select.mockReturnValue(mockSelectBuilder); mockCentralDb.delete.mockReturnValue(mockDeleteBuilder); mockTenantMigrationService.createAndInitializeTenantDatabase.mockRejectedValue( new Error("Database initialization failed"), diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index bc7877a..861442b 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -8,7 +8,7 @@ import { env } from "$env/dynamic/private"; import { eq } from "drizzle-orm"; import logger from "$lib/logger"; import z from "zod/v4"; -import { ValidationError, NotFoundError } from "../utils/errors"; +import { ValidationError, NotFoundError, ConflictError } from "../utils/errors"; import { sendTenantAdminInviteEmail } from "../email/email-service"; if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); @@ -68,6 +68,26 @@ export class TenantAdminService { shortName: request.shortName, }); + // Check if tenant can be created without any duplications. Do not create tenant if: + // - short name already exists + // - invited tenant admin email already exists + const tenantExists = await centralDb + .select() + .from(centralSchema.tenant) + .where(eq(centralSchema.tenant.shortName, request.shortName)); + if (tenantExists.length > 0) { + throw new ConflictError("Tenant with shortname already exists"); + } + if (request.inviteAdmin) { + const adminExists = await centralDb + .select() + .from(centralSchema.user) + .where(eq(centralSchema.user.email, request.inviteAdmin)); + if (adminExists.length > 0) { + throw new ConflictError("Tenant Admin E-Mail Address already exists"); + } + } + const configuration = TenantAdminService.getConfigDefaults(); const urlParts = env.DATABASE_URL?.split("/") ?? []; diff --git a/src/lib/server/utils/errors.ts b/src/lib/server/utils/errors.ts index 2e9b24d..fc5d064 100644 --- a/src/lib/server/utils/errors.ts +++ b/src/lib/server/utils/errors.ts @@ -1,19 +1,51 @@ -export class AuthenticationError extends Error { - constructor(message: string) { +import { json } from "@sveltejs/kit"; + +export class BackendError extends Error { + constructor( + message: string, + public code: number, + ) { super(message); + } + + public toJson = () => json({ message: this.message }, { status: this.code }); +} + +export class AuthenticationError extends BackendError { + constructor( + message: string, + public code = 403, + ) { + super(message, code); this.name = "AuthenticationError"; } } -export class ValidationError extends Error { - constructor(message: string) { - super(message); +export class ValidationError extends BackendError { + constructor( + message: string, + public code = 422, + ) { + super(message, code); this.name = "ValidationError"; } } -export class NotFoundError extends Error { - constructor(message: string) { - super(message); +export class NotFoundError extends BackendError { + constructor( + message: string, + public code = 404, + ) { + super(message, code); this.name = "ValidationError"; } } + +export class ConflictError extends BackendError { + constructor( + message: string, + public code = 409, + ) { + super(message, code); + this.name = "ConflictError"; + } +} diff --git a/src/routes/api/tenants/+server.ts b/src/routes/api/tenants/+server.ts index 38a3d5d..22ff3e9 100644 --- a/src/routes/api/tenants/+server.ts +++ b/src/routes/api/tenants/+server.ts @@ -1,6 +1,6 @@ import { json } from "@sveltejs/kit"; import { TenantAdminService } from "$lib/server/services/tenant-admin-service"; -import { ValidationError } from "$lib/server/utils/errors"; +import { BackendError } from "$lib/server/utils/errors"; import type { RequestHandler } from "@sveltejs/kit"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import { db } from "$lib/server/db"; @@ -178,8 +178,8 @@ export const POST: RequestHandler = async ({ request }) => { } catch (error) { log.error("Tenant creation error:", JSON.stringify(error || "?")); - if (error instanceof ValidationError) { - return json({ error: error.message }, { status: 400 }); + if (error instanceof BackendError) { + return error.toJson(); } // Handle unique constraint violation (shortName already exists) diff --git a/src/routes/api/tenants/tenants.test.ts b/src/routes/api/tenants/tenants.test.ts index f52dcc7..8f1fbe9 100644 --- a/src/routes/api/tenants/tenants.test.ts +++ b/src/routes/api/tenants/tenants.test.ts @@ -35,15 +35,8 @@ vi.mock("$lib/logger", () => ({ }, })); -// Mock ValidationError -vi.mock("$lib/server/utils/errors", () => ({ - ValidationError: class ValidationError extends Error { - constructor(message: string) { - super(message); - this.name = "ValidationError"; - } - }, -})); +// Don't mock errors - use real ones +// vi.mock("$lib/server/utils/errors"); describe("/api/tenants", () => { beforeEach(() => { @@ -173,9 +166,9 @@ describe("/api/tenants", () => { const data = await response.json(); expect(data).toEqual({ - error: "Invalid tenant creation request", + message: "Invalid tenant creation request", }); - expect(response.status).toBe(400); + expect(response.status).toBe(422); }); it("should handle unique constraint violations", async () => {