93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
findUnique: vi.fn(),
|
|
update: vi.fn(),
|
|
auditCreate: vi.fn(),
|
|
transaction: vi.fn(),
|
|
invalidateFlagCache: vi.fn(),
|
|
verifyUserHasPermission: vi.fn(),
|
|
validateCSRFToken: vi.fn(),
|
|
getUserInfo: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/locale/i18next.server", () => ({
|
|
getTranslations: vi.fn(async () => ({
|
|
t: (key: string, options?: Record<string, string | number>) => {
|
|
if (key === "featureFlags.engine.errors.invalidCompositeOperator") {
|
|
return `${options?.path}.op must be AND, OR, or NOT.`;
|
|
}
|
|
return key;
|
|
},
|
|
})),
|
|
}));
|
|
|
|
vi.mock("~/utils/helpers/.server/PermissionsService", () => ({
|
|
verifyUserHasPermission: mocks.verifyUserHasPermission,
|
|
}));
|
|
|
|
vi.mock("~/utils/session.server", () => ({
|
|
validateCSRFToken: mocks.validateCSRFToken,
|
|
getUserInfo: mocks.getUserInfo,
|
|
}));
|
|
|
|
vi.mock("~/utils/services/.server/featureFlagEvaluator.server", () => ({
|
|
invalidateFlagCache: mocks.invalidateFlagCache,
|
|
}));
|
|
|
|
vi.mock("~/utils/db.server", () => ({
|
|
db: {
|
|
featureFlag: {
|
|
findUnique: mocks.findUnique,
|
|
update: mocks.update,
|
|
},
|
|
featureFlagAudit: {
|
|
create: mocks.auditCreate,
|
|
},
|
|
$transaction: mocks.transaction,
|
|
},
|
|
}));
|
|
|
|
function requestForRules(rules: unknown) {
|
|
const body = new URLSearchParams({
|
|
intent: "save-rules",
|
|
id: "flag_1",
|
|
rules: JSON.stringify(rules),
|
|
});
|
|
return new Request("https://admin.example.com/feature-flags/engine", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body,
|
|
});
|
|
}
|
|
|
|
describe("admin feature flag engine rule validation", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.verifyUserHasPermission.mockResolvedValue(undefined);
|
|
mocks.validateCSRFToken.mockResolvedValue(undefined);
|
|
mocks.getUserInfo.mockResolvedValue({ userId: "user_1" });
|
|
mocks.findUnique.mockResolvedValue({
|
|
id: "flag_1",
|
|
name: "checkout.beta",
|
|
enabled: true,
|
|
rules: null,
|
|
});
|
|
});
|
|
|
|
it("returns a specific error for unsupported composite operators before persisting", async () => {
|
|
const { action } = await import("./engine");
|
|
const response = (await action({
|
|
request: requestForRules({ type: "composite", op: "XOR", children: [] }),
|
|
params: {},
|
|
context: {},
|
|
})) as Response;
|
|
|
|
expect(response.status).toBe(400);
|
|
await expect(response.json()).resolves.toEqual({ error: "rule.op must be AND, OR, or NOT." });
|
|
expect(mocks.transaction).not.toHaveBeenCalled();
|
|
expect(mocks.invalidateFlagCache).not.toHaveBeenCalled();
|
|
});
|
|
});
|