import { parseKeyEntries, getKeyToSkipHook, shouldSkipHook, } from "../validationHelper"; const mockSessionGetItem = jest.fn(); const mockLocalGetItem = jest.fn(); jest.mock( "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage", () => ({ sessionStorage: { getItem: (...args) => mockSessionGetItem(...args) }, }) ); jest.mock( "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage", () => ({ localStorage: { getItem: (...args) => mockLocalGetItem(...args) }, }) ); jest.mock("../logger", () => ({ log_debug: jest.fn(), log_error: jest.fn(), log_info: jest.fn(), })); describe("parseKeyEntries", () => { it("parses a namespaced key as namespace.key", () => { expect(parseKeyEntries("myNamespace.myKey")).toEqual([ { namespace: "myNamespace", key: "myKey" }, ]); }); it("treats everything before the last dot as the namespace", () => { expect(parseKeyEntries("com.applicaster.feature.someKey")).toEqual([ { namespace: "com.applicaster.feature", key: "someKey" }, ]); }); it("falls back to the default namespace when there is no dot", () => { expect(parseKeyEntries("plainKey")).toEqual([ { namespace: "applicaster.v2", key: "plainKey" }, ]); }); it("splits comma-separated entries, trimming whitespace and empty items", () => { expect(parseKeyEntries(" ns1.key1 , , ns2.key2 ,")).toEqual([ { namespace: "ns1", key: "key1" }, { namespace: "ns2", key: "key2" }, ]); }); }); describe("getKeyToSkipHook", () => { beforeEach(() => { jest.clearAllMocks(); }); it("returns the session storage value when present, without hitting local storage", async () => { mockSessionGetItem.mockResolvedValue("session-value"); const value = await getKeyToSkipHook("myKey", "myNamespace"); expect(value).toBe("session-value"); expect(mockSessionGetItem).toHaveBeenCalledWith("myKey", "myNamespace"); expect(mockLocalGetItem).not.toHaveBeenCalled(); }); it("falls back to local storage when session storage is empty", async () => { mockSessionGetItem.mockResolvedValue(null); mockLocalGetItem.mockResolvedValue("local-value"); const value = await getKeyToSkipHook("myKey", "myNamespace"); expect(value).toBe("local-value"); expect(mockLocalGetItem).toHaveBeenCalledWith("myKey", "myNamespace"); }); }); describe("shouldSkipHook", () => { beforeEach(() => { jest.clearAllMocks(); mockSessionGetItem.mockResolvedValue(null); mockLocalGetItem.mockResolvedValue(null); }); it("returns false when no condition is provided", async () => { await expect(shouldSkipHook(undefined)).resolves.toBe(false); await expect(shouldSkipHook("")).resolves.toBe(false); await expect(shouldSkipHook(" ")).resolves.toBe(false); }); it("returns true when a key is found in storage, querying with parsed namespace and key", async () => { mockSessionGetItem.mockResolvedValue("value"); await expect(shouldSkipHook("myNamespace.myKey")).resolves.toBe(true); expect(mockSessionGetItem).toHaveBeenCalledWith("myKey", "myNamespace"); }); it("returns true when any of the comma-separated keys is found", async () => { mockLocalGetItem.mockImplementation((key) => Promise.resolve(key === "key2" ? "value" : null) ); await expect(shouldSkipHook("ns1.key1, ns2.key2")).resolves.toBe(true); }); it("returns false when none of the keys are found", async () => { await expect(shouldSkipHook("ns1.key1, ns2.key2")).resolves.toBe(false); expect(mockSessionGetItem).toHaveBeenCalledTimes(2); expect(mockLocalGetItem).toHaveBeenCalledTimes(2); }); it("continues to the next key when a storage read throws", async () => { mockSessionGetItem .mockRejectedValueOnce(new Error("storage error")) .mockResolvedValueOnce("value"); await expect(shouldSkipHook("ns1.key1, ns2.key2")).resolves.toBe(true); }); }); describe("shouldSkipHook with a packed expression", () => { const SKIP_UNLESS_LOGGED_IN_WITHOUT_PROFILE = JSON.stringify({ any: [ { missing: "quick-brick-login-flow.access_token" }, { exists: "user_account.profile" }, ], }); const storedKeys = (store: Record) => { mockSessionGetItem.mockImplementation((key, namespace) => Promise.resolve(store[`${namespace}.${key}`] ?? null) ); mockLocalGetItem.mockResolvedValue(null); }; beforeEach(() => { jest.clearAllMocks(); storedKeys({}); }); it("does not skip for a logged-in user who has not picked a profile yet", async () => { storedKeys({ "quick-brick-login-flow.access_token": "a-token" }); await expect( shouldSkipHook(SKIP_UNLESS_LOGGED_IN_WITHOUT_PROFILE) ).resolves.toBe(false); }); it("skips for a logged-out user", async () => { await expect( shouldSkipHook(SKIP_UNLESS_LOGGED_IN_WITHOUT_PROFILE) ).resolves.toBe(true); }); it("skips once a profile has been picked", async () => { storedKeys({ "quick-brick-login-flow.access_token": "a-token", "user_account.profile": "profile-id", }); await expect( shouldSkipHook(SKIP_UNLESS_LOGGED_IN_WITHOUT_PROFILE) ).resolves.toBe(true); }); it("falls back to local storage for a key the session does not hold", async () => { mockLocalGetItem.mockResolvedValue("a-token"); await expect( shouldSkipHook(JSON.stringify({ exists: "ns.key" })) ).resolves.toBe(true); expect(mockSessionGetItem).toHaveBeenCalledWith("key", "ns"); expect(mockLocalGetItem).toHaveBeenCalledWith("key", "ns"); }); it("returns false for a malformed expression instead of reading storage", async () => { await expect(shouldSkipHook('{"any":')).resolves.toBe(false); expect(mockSessionGetItem).not.toHaveBeenCalled(); expect(mockLocalGetItem).not.toHaveBeenCalled(); }); });