Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/services/admin/subscription-assembly.server.test.ts
T

119 lines
4.9 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { ProductBlueprintFeatureDefinition } from "@hivegrid/entitlements";
import {
AdminConnectorError,
buildConnectorInputRows,
getTenantConnectorRows,
setTenantFeatureConnector,
type ConnectorServiceDeps,
} from "./subscription-assembly.server";
import { AdminActionInputError } from "./admin-action-audit.server";
import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
const FEATURES: readonly ProductBlueprintFeatureDefinition[] = [
{ featureKey: "crm-basic", defaultEnabled: true, tenantToggleable: false },
{ featureKey: "files", defaultEnabled: true, tenantToggleable: true },
{ featureKey: "api", defaultEnabled: false, tenantToggleable: true },
];
function makeDeps(overrides: Array<{ featureKey: string; override: string }> = []): {
deps: ConnectorServiceDeps;
upsert: ReturnType<typeof vi.fn>;
deleteMany: ReturnType<typeof vi.fn>;
invalidateCache: ReturnType<typeof vi.fn>;
recordAction: ReturnType<typeof vi.fn>;
} {
const upsert = vi.fn(async () => ({}));
const deleteMany = vi.fn(async () => ({ count: 1 }));
const invalidateCache = vi.fn();
const recordAction = vi.fn(async () => {});
const deps = {
db: {
tenantProductConfiguration: { findUnique: vi.fn(async () => ({ blueprintId: "crm-saas" })) },
tenantFeatureOverride: { findMany: vi.fn(async () => overrides), upsert, deleteMany },
},
invalidateCache,
readBlueprint: () => FEATURES,
recordAction,
} as unknown as ConnectorServiceDeps;
return { deps, upsert, deleteMany, invalidateCache, recordAction };
}
describe("buildConnectorInputRows", () => {
it("joins blueprint features with stored overrides", () => {
const rows = buildConnectorInputRows(FEATURES, new Map([["files", "disabled"]]));
expect(rows).toEqual([
{ featureKey: "crm-basic", toggleable: false, baseEnabled: true, overrideState: null },
{ featureKey: "files", toggleable: true, baseEnabled: true, overrideState: "disabled" },
{ featureKey: "api", toggleable: true, baseEnabled: false, overrideState: null },
]);
});
});
describe("getTenantConnectorRows", () => {
it("reads overrides and maps them onto the blueprint", async () => {
const { deps } = makeDeps([{ featureKey: "api", override: "enabled" }]);
vi.mocked(deps.db.tenantProductConfiguration.findUnique).mockImplementationOnce(async () => {
expect(getRlsExecutionContext()?.tenantId).toBe("t1");
return { blueprintId: "crm-saas" };
});
const rows = await getTenantConnectorRows("t1", deps);
expect(rows.find((r) => r.featureKey === "api")?.overrideState).toBe("enabled");
});
});
describe("setTenantFeatureConnector", () => {
it("rejects a missing reason before touching the DB", async () => {
const { deps, upsert } = makeDeps();
await expect(
setTenantFeatureConnector({ tenantId: "t1", featureKey: "files", state: "disabled", reason: "", actorUserId: "a1" }, deps)
).rejects.toBeInstanceOf(AdminActionInputError);
expect(upsert).not.toHaveBeenCalled();
});
it("rejects an unknown feature key", async () => {
const { deps } = makeDeps();
await expect(
setTenantFeatureConnector({ tenantId: "t1", featureKey: "nope", state: "enabled", reason: "valid reason", actorUserId: "a1" }, deps)
).rejects.toMatchObject({ code: "FEATURE_NOT_FOUND" });
});
it("refuses to toggle a non-toggleable core feature", async () => {
const { deps } = makeDeps();
await expect(
setTenantFeatureConnector({ tenantId: "t1", featureKey: "crm-basic", state: "disabled", reason: "valid reason", actorUserId: "a1" }, deps)
).rejects.toBeInstanceOf(AdminConnectorError);
});
it("upserts the override, invalidates the cache and audits", async () => {
const { deps, upsert, invalidateCache, recordAction } = makeDeps();
upsert.mockImplementationOnce(async () => {
expect(getRlsExecutionContext()?.tenantId).toBe("t1");
return {};
});
await setTenantFeatureConnector(
{ tenantId: "t1", featureKey: "api", state: "enabled", reason: "Pilot freischalten", actorUserId: "a1" },
deps
);
expect(upsert).toHaveBeenCalledOnce();
expect(invalidateCache).toHaveBeenCalledWith("t1");
expect(recordAction).toHaveBeenCalledOnce();
expect(recordAction.mock.calls[0][0]).toMatchObject({
action: "subscription.featureToggled",
targetId: "t1",
reason: "Pilot freischalten",
after: { featureKey: "api", state: "enabled" },
});
});
it("clears the override (deleteMany) when set back to inherit", async () => {
const { deps, deleteMany, upsert } = makeDeps();
await setTenantFeatureConnector(
{ tenantId: "t1", featureKey: "files", state: "inherit", reason: "Zurueck auf Plan-Default", actorUserId: "a1" },
deps
);
expect(deleteMany).toHaveBeenCalledOnce();
expect(upsert).not.toHaveBeenCalled();
});
});