import { beforeEach, describe, expect, it, vi } from "vitest"; import { getAvailableTenantSlug, isReservedTenantChildRouteSlug, isReservedTenantSlug, normalizeTenantSlug, validateDesiredTenantSlug, } from "./tenantService"; import { db } from "../db.server"; vi.mock("../db.server", () => ({ db: { tenant: { count: vi.fn() } } })); const countMock = vi.mocked(db.tenant.count); vi.mock("../stripe.server", () => ({ deleteStripeCustomer: vi.fn() })); vi.mock("../db/tenants.db.server", () => ({ deleteTenant: vi.fn() })); vi.mock("../db/tenantSubscriptions.db.server", () => ({ getTenantSubscription: vi.fn() })); vi.mock("./.server/subscriptionService", () => ({ cancelTenantSubscription: vi.fn() })); describe("tenant route slug reservations", () => { it("normalizes slugs before checking top-level route reservations", () => { expect(normalizeTenantSlug(" Settings ")).toBe("settings"); expect(isReservedTenantSlug("Admin")).toBe(true); expect(isReservedTenantSlug("Acme")).toBe(false); }); it("reserves tenant child routes used by canonical workspace pages", () => { expect(isReservedTenantChildRouteSlug("settings")).toBe(true); expect(isReservedTenantChildRouteSlug("CRM")).toBe(true); expect(isReservedTenantChildRouteSlug("g")).toBe(true); expect(isReservedTenantChildRouteSlug("customers")).toBe(false); }); }); describe("validateDesiredTenantSlug", () => { it("accepts a normal workspace name and returns the normalized slug", () => { expect(validateDesiredTenantSlug("Acme Corp")).toEqual({ ok: true, slug: "acme-corp" }); expect(validateDesiredTenantSlug(" Beta Works ")).toEqual({ ok: true, slug: "beta-works" }); }); it("rejects reserved slugs with a typed reason", () => { expect(validateDesiredTenantSlug("admin")).toEqual({ ok: false, reason: "reserved" }); expect(validateDesiredTenantSlug("API")).toEqual({ ok: false, reason: "reserved" }); expect(validateDesiredTenantSlug("settings")).toEqual({ ok: false, reason: "reserved" }); }); it("rejects empty and symbol-only input", () => { expect(validateDesiredTenantSlug("")).toEqual({ ok: false, reason: "empty" }); expect(validateDesiredTenantSlug("!!!")).toEqual({ ok: false, reason: "empty" }); }); it("rejects single-character slugs as too short", () => { expect(validateDesiredTenantSlug("x")).toEqual({ ok: false, reason: "too-short" }); }); }); describe("getAvailableTenantSlug", () => { beforeEach(() => { countMock.mockReset(); }); it("returns the name-derived slug when free", async () => { countMock.mockResolvedValue(0); await expect(getAvailableTenantSlug({ name: "Acme" })).resolves.toBe("acme"); }); it("appends a counter when the slug is already taken", async () => { countMock.mockResolvedValueOnce(1).mockResolvedValueOnce(0); await expect(getAvailableTenantSlug({ name: "Acme" })).resolves.toBe("acme1"); }); it("skips reserved desired slugs without a DB lookup and falls back to the name base", async () => { countMock.mockResolvedValue(0); // "admin" ist reserviert -> getExistingSlug kurzschliesst (kein count) -> Namens-Basis + Zaehler. await expect(getAvailableTenantSlug({ name: "Acme", slug: "admin" })).resolves.toBe("acme1"); }); it("never returns an empty or numeric slug for symbol-only names", async () => { countMock.mockResolvedValue(0); await expect(getAvailableTenantSlug({ name: "!!!" })).resolves.toBe("workspace"); }); });