82 lines
3.4 KiB
TypeScript
82 lines
3.4 KiB
TypeScript
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");
|
|
});
|
|
});
|