import { looksLikeSkipExpression, parseSkipExpression, evaluateSkipExpression, } from "../skipExpression"; jest.mock("../logger", () => ({ log_debug: jest.fn(), log_error: jest.fn(), log_info: jest.fn(), })); const TOKEN = "quick-brick-login-flow.access_token"; const PROFILE = "user_account.profile"; /** * Stands in for `getKeyToSkipHook`: resolves a "namespace.key" store, so the * expression evaluator is exercised against real lookups rather than mocks. */ const storage = (store: Record) => jest.fn((key: string, namespace?: string) => Promise.resolve(store[`${namespace}.${key}`]) ); const evaluate = (expression: unknown, store: Record = {}) => evaluateSkipExpression(parseSkipExpression(expression), storage(store)); describe("looksLikeSkipExpression", () => { it("recognises a JSON object, whatever its content", () => { expect(looksLikeSkipExpression('{"exists":"ns.key"}')).toBe(true); expect(looksLikeSkipExpression(' {"exists":"ns.key"} ')).toBe(true); expect(looksLikeSkipExpression({ exists: "ns.key" })).toBe(true); }); it("recognises a malformed expression, so it is never read as a key list", () => { expect(looksLikeSkipExpression('{"any":')).toBe(true); }); it("rejects a legacy comma-separated key list", () => { expect(looksLikeSkipExpression("ns1.key1, ns2.key2")).toBe(false); }); it("rejects empty and missing input", () => { expect(looksLikeSkipExpression("")).toBe(false); expect(looksLikeSkipExpression(" ")).toBe(false); expect(looksLikeSkipExpression(undefined)).toBe(false); }); }); describe("parseSkipExpression", () => { it("parses a JSON string into an expression object", () => { expect(parseSkipExpression('{"exists":"ns.key"}')).toEqual({ exists: "ns.key", }); }); it("ignores leading and trailing whitespace around the JSON", () => { expect(parseSkipExpression(' {"exists":"ns.key"} ')).toEqual({ exists: "ns.key", }); }); it("accepts an expression that is already an object", () => { expect(parseSkipExpression({ exists: "ns.key" })).toEqual({ exists: "ns.key", }); }); it("returns null for a legacy comma-separated key list", () => { expect(parseSkipExpression("ns1.key1, ns2.key2")).toBeNull(); }); it("returns null for a malformed JSON object", () => { expect(parseSkipExpression('{"exists":')).toBeNull(); }); it("returns null for empty and missing input", () => { expect(parseSkipExpression("")).toBeNull(); expect(parseSkipExpression(undefined)).toBeNull(); expect(parseSkipExpression(null)).toBeNull(); }); it("returns null for a JSON array, which is not a valid expression", () => { expect(parseSkipExpression('["ns.key"]')).toBeNull(); }); }); describe("evaluateSkipExpression", () => { it("returns false when there is no expression", async () => { await expect(evaluateSkipExpression(null, storage({}))).resolves.toBe( false ); }); describe("exists", () => { it("is true when the key holds a value", async () => { await expect( evaluate({ exists: "ns.key" }, { "ns.key": "value" }) ).resolves.toBe(true); }); it("is false when the key is absent", async () => { await expect(evaluate({ exists: "ns.key" })).resolves.toBe(false); }); it("is false when the key holds an empty string", async () => { await expect( evaluate({ exists: "ns.key" }, { "ns.key": "" }) ).resolves.toBe(false); }); it("splits the namespace on the last dot, like the legacy format", async () => { const readKey = storage({ "com.applicaster.feature.someKey": "value" }); await evaluateSkipExpression( { exists: "com.applicaster.feature.someKey" }, readKey ); expect(readKey).toHaveBeenCalledWith( "someKey", "com.applicaster.feature" ); }); it("treats a bare string operand as a shorthand for exists", async () => { await expect( evaluateSkipExpression("ns.key", storage({ "ns.key": "value" })) ).resolves.toBe(true); }); }); describe("missing", () => { it("is true when the key is absent", async () => { await expect(evaluate({ missing: "ns.key" })).resolves.toBe(true); }); it("is false when the key holds a value", async () => { await expect( evaluate({ missing: "ns.key" }, { "ns.key": "value" }) ).resolves.toBe(false); }); }); describe("equals", () => { it("is true when the stored value matches", async () => { await expect( evaluate( { equals: { key: "ns.key", value: "kids" } }, { "ns.key": "kids" } ) ).resolves.toBe(true); }); it("is false when the stored value differs", async () => { await expect( evaluate( { equals: { key: "ns.key", value: "kids" } }, { "ns.key": "adults" } ) ).resolves.toBe(false); }); it("compares a non-string stored value by its string form", async () => { await expect( evaluate({ equals: { key: "ns.key", value: "42" } }, { "ns.key": 42 }) ).resolves.toBe(true); }); }); describe("all", () => { it("is true only when every operand is true", async () => { await expect( evaluate({ all: ["ns.a", "ns.b"] }, { "ns.a": "1", "ns.b": "2" }) ).resolves.toBe(true); }); it("is false when one operand is false", async () => { await expect( evaluate({ all: ["ns.a", "ns.b"] }, { "ns.a": "1" }) ).resolves.toBe(false); }); it("stops reading storage after the first false operand", async () => { const readKey = storage({ "ns.b": "2" }); await evaluateSkipExpression({ all: ["ns.a", "ns.b"] }, readKey); expect(readKey).toHaveBeenCalledTimes(1); }); it("is false for an empty operand list", async () => { await expect(evaluate({ all: [] })).resolves.toBe(false); }); }); describe("any", () => { it("is true when one operand is true", async () => { await expect( evaluate({ any: ["ns.a", "ns.b"] }, { "ns.b": "2" }) ).resolves.toBe(true); }); it("is false when every operand is false", async () => { await expect(evaluate({ any: ["ns.a", "ns.b"] })).resolves.toBe(false); }); it("stops reading storage after the first true operand", async () => { const readKey = storage({ "ns.a": "1", "ns.b": "2" }); await evaluateSkipExpression({ any: ["ns.a", "ns.b"] }, readKey); expect(readKey).toHaveBeenCalledTimes(1); }); it("is false for an empty operand list", async () => { await expect(evaluate({ any: [] })).resolves.toBe(false); }); }); describe("not", () => { it("inverts a true operand", async () => { await expect( evaluate({ not: "ns.key" }, { "ns.key": "value" }) ).resolves.toBe(false); }); it("inverts a false operand", async () => { await expect(evaluate({ not: "ns.key" })).resolves.toBe(true); }); }); describe("malformed expressions fail open", () => { it("is false for an unknown operator", async () => { await expect(evaluate({ matches: "ns.key" })).resolves.toBe(false); }); it("is false for an operator carrying the wrong operand type", async () => { await expect(evaluate({ exists: 42 })).resolves.toBe(false); await expect(evaluate({ all: "ns.key" })).resolves.toBe(false); }); it("is false for an operand list holding a malformed node", async () => { await expect( evaluate({ all: ["ns.a", { matches: "ns.b" }] }, { "ns.a": "1" }) ).resolves.toBe(false); }); it("is false when a storage read throws", async () => { const readKey = jest.fn(() => Promise.reject(new Error("storage error"))); await expect( evaluateSkipExpression({ exists: "ns.key" }, readKey) ).resolves.toBe(false); }); it("keeps evaluating the other operands when one storage read throws", async () => { const readKey = jest.fn((key: string) => key === "a" ? Promise.reject(new Error("storage error")) : Promise.resolve("value") ); await expect( evaluateSkipExpression({ any: ["ns.a", "ns.b"] }, readKey) ).resolves.toBe(true); }); }); describe("profile selector scenario", () => { const skipProfileSelector = { any: [{ missing: TOKEN }, { exists: PROFILE }], }; it("shows the hook to a logged-in user who has not picked a profile yet", async () => { await expect( evaluate(skipProfileSelector, { [TOKEN]: "a-token" }) ).resolves.toBe(false); }); it("skips the hook for a logged-out user", async () => { await expect(evaluate(skipProfileSelector)).resolves.toBe(true); }); it("skips the hook once a profile has been picked", async () => { await expect( evaluate(skipProfileSelector, { [TOKEN]: "a-token", [PROFILE]: "profile-id", }) ).resolves.toBe(true); }); }); });