Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/featureFlags/db/featureFlags.db.server.test.ts
T

168 lines
6.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server";
const mocks = vi.hoisted(() => ({
featureFlagFindMany: vi.fn(),
featureFlagUpdate: vi.fn(),
featureFlagDelete: vi.fn(),
featureFlagDeleteMany: vi.fn(),
tenantFindMany: vi.fn(),
analyticsEventGroupBy: vi.fn(),
clearCacheKeyPrefix: vi.fn(),
}));
vi.mock("~/utils/db.server", () => ({
db: {
featureFlag: {
findMany: mocks.featureFlagFindMany,
update: mocks.featureFlagUpdate,
delete: mocks.featureFlagDelete,
deleteMany: mocks.featureFlagDeleteMany,
},
tenant: { findMany: mocks.tenantFindMany },
analyticsEvent: { groupBy: mocks.analyticsEventGroupBy },
},
}));
vi.mock("~/utils/cache.server", () => ({ clearCacheKeyPrefix: mocks.clearCacheKeyPrefix }));
import {
deleteAllFeatureFlags,
deleteFeatureFlag,
getAllFeatureFlags,
getFeatureFlagEvaluationNames,
getFeatureFlagsWithEventCounts,
getLegacyFeatureFlagsWithEventCounts,
updateFeatureFlag,
} from "./featureFlags.db.server";
describe("feature flag list projections", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.featureFlagFindMany.mockResolvedValue([]);
mocks.featureFlagUpdate.mockResolvedValue({ id: "flag_1" });
mocks.featureFlagDelete.mockResolvedValue({ id: "flag_1" });
mocks.featureFlagDeleteMany.mockResolvedValue({ count: 2 });
mocks.tenantFindMany.mockResolvedValue([]);
mocks.analyticsEventGroupBy.mockResolvedValue([]);
});
it("aggregates event counts across tenant scopes and isolated platform analytics", async () => {
mocks.featureFlagFindMany.mockResolvedValue([
{ id: "flag_1", filters: [] },
{ id: "flag_2", filters: [] },
]);
mocks.tenantFindMany.mockImplementation(async () => {
expect(getRlsExecutionContext()).toEqual({ userId: "operator_1" });
return [{ id: "tenant_1" }, { id: "tenant_2" }];
});
mocks.analyticsEventGroupBy.mockImplementation(async () => {
const context = getRlsExecutionContext();
if (context?.tenantId === "tenant_1") return [{ featureFlagId: "flag_1", _count: 2 }];
if (context?.tenantId === "tenant_2") {
return [
{ featureFlagId: "flag_1", _count: 3 },
{ featureFlagId: "flag_2", _count: 1 },
];
}
if (context?.platformAnalytics) {
expect(context).toEqual({ platformAnalytics: true });
return [
{ featureFlagId: "flag_1", _count: 4 },
{ featureFlagId: "flag_2", _count: 5 },
];
}
throw new Error(`Unexpected RLS context: ${JSON.stringify(context)}`);
});
const result = await runWithRlsContext({ userId: "operator_1" }, () => getAllFeatureFlags());
expect(result).toEqual([
expect.objectContaining({ id: "flag_1", _count: { events: 9 } }),
expect.objectContaining({ id: "flag_2", _count: { events: 6 } }),
]);
expect(mocks.featureFlagFindMany).toHaveBeenCalledWith({
where: { AND: [{ archivedAt: null }] },
include: { filters: true },
orderBy: { createdAt: "desc" },
});
expect(mocks.tenantFindMany).toHaveBeenCalledWith({ select: { id: true }, orderBy: { id: "asc" } });
expect(mocks.analyticsEventGroupBy).toHaveBeenCalledTimes(3);
expect(mocks.analyticsEventGroupBy).toHaveBeenCalledWith({
by: ["featureFlagId"],
where: { featureFlagId: { in: ["flag_1", "flag_2"] } },
_count: true,
});
expect(getRlsExecutionContext()).toBeNull();
});
it("applies the enabled filter before aggregating matching flag events", async () => {
mocks.featureFlagFindMany.mockResolvedValue([{ id: "flag_1", filters: [] }]);
await runWithRlsContext({ userId: "operator_1" }, () => getFeatureFlagsWithEventCounts({ enabled: true }));
expect(mocks.featureFlagFindMany).toHaveBeenCalledWith({
where: { AND: [{ archivedAt: null }, { enabled: true }] },
include: { filters: true },
orderBy: { createdAt: "desc" },
});
expect(mocks.analyticsEventGroupBy).toHaveBeenCalledOnce();
expect(getRlsExecutionContext()).toBeNull();
});
it("lists request-time evaluation names without cross-tenant analytics aggregation", async () => {
mocks.featureFlagFindMany.mockResolvedValue([
{ name: "flag-one", key: null },
{ name: "flag-two", key: "legacy-two" },
{ name: "flag-one", key: null },
]);
const names = await runWithRlsContext({ userId: "user_1", tenantId: "tenant_1" }, async () => {
expect(getRlsExecutionContext()).toEqual({ userId: "user_1", tenantId: "tenant_1" });
return getFeatureFlagEvaluationNames();
});
expect(names).toEqual(["flag-one", "flag-two"]);
expect(mocks.featureFlagFindMany).toHaveBeenCalledWith({
where: { archivedAt: null },
select: { name: true, key: true },
orderBy: { createdAt: "desc" },
});
expect(mocks.tenantFindMany).not.toHaveBeenCalled();
expect(mocks.analyticsEventGroupBy).not.toHaveBeenCalled();
expect(getRlsExecutionContext()).toBeNull();
});
it("deletes flags without deleting their analytics history", async () => {
await expect(deleteAllFeatureFlags()).resolves.toEqual({ count: 2 });
expect(mocks.featureFlagDeleteMany).toHaveBeenCalledOnce();
expect(mocks.featureFlagDeleteMany).toHaveBeenCalledWith();
expect(mocks.clearCacheKeyPrefix).toHaveBeenCalledWith("featureFlags:enabled");
});
it("hides archived flags from the legacy editor", async () => {
await getLegacyFeatureFlagsWithEventCounts({ enabled: undefined });
expect(mocks.featureFlagFindMany).toHaveBeenCalledWith({
where: {
AND: [{ archivedAt: null }, { enabled: undefined }],
},
include: { filters: true },
orderBy: { createdAt: "desc" },
});
});
it("updates active generic rollout flags", async () => {
await expect(updateFeatureFlag("flag_1", { enabled: false })).resolves.toEqual({ id: "flag_1" });
expect(mocks.featureFlagUpdate).toHaveBeenCalledWith(
expect.objectContaining({ where: { id: "flag_1" }, data: expect.objectContaining({ enabled: false }) })
);
});
it("deletes active generic rollout flags", async () => {
await expect(deleteFeatureFlag("flag_1")).resolves.toEqual({ id: "flag_1" });
expect(mocks.featureFlagDelete).toHaveBeenCalledWith({ where: { id: "flag_1" } });
});
});