@frak-labs/core-sdk
Version:
Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.
513 lines (450 loc) • 17 kB
text/typescript
import { vi } from "vitest";
import {
afterEach,
beforeEach,
describe,
expect,
test,
} from "../../tests/vitest-fixtures";
vi.mock("../config/clientId", () => ({
getClientId: vi.fn().mockReturnValue("test-client-id"),
}));
vi.mock("../config/sdkConfigStore", () => ({
sdkConfigStore: {
resolveMerchantId: vi.fn().mockResolvedValue(undefined),
},
}));
import { getClientId } from "../config/clientId";
import { sdkConfigStore } from "../config/sdkConfigStore";
import { trackPurchaseStatus } from "./trackPurchaseStatus";
describe.sequential("trackPurchaseStatus", () => {
const TRACK_PURCHASE_URL = "https://backend.frak.id/user/track/purchase";
let mockSessionStorage: {
getItem: ReturnType<typeof vi.fn>;
setItem: ReturnType<typeof vi.fn>;
removeItem: ReturnType<typeof vi.fn>;
};
let mockLocalStorage: {
getItem: ReturnType<typeof vi.fn>;
setItem: ReturnType<typeof vi.fn>;
removeItem: ReturnType<typeof vi.fn>;
};
let fetchSpy: ReturnType<typeof vi.fn>;
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
function setupStorage(values: {
interactionToken?: string | null;
merchantId?: string | null;
clientId?: string | null;
}) {
mockSessionStorage.getItem.mockImplementation((key: string) => {
if (key === "frak-wallet-interaction-token") {
return values.interactionToken ?? null;
}
if (key === "frak-merchant-id") {
return values.merchantId ?? null;
}
return null;
});
mockLocalStorage.getItem.mockImplementation((key: string) => {
if (key === "frak-client-id") {
return values.clientId ?? null;
}
return null;
});
}
function getTrackingRequests() {
return fetchSpy.mock.calls.filter(
([url]) => url === TRACK_PURCHASE_URL
);
}
function getLastTrackingRequest() {
return getTrackingRequests().at(-1);
}
beforeEach(() => {
mockSessionStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
};
mockLocalStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
};
Object.defineProperty(window, "sessionStorage", {
value: mockSessionStorage,
writable: true,
configurable: true,
});
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage,
writable: true,
configurable: true,
});
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
vi.mocked(getClientId).mockReturnValue("test-client-id");
vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
undefined
);
fetchSpy = vi.fn().mockResolvedValue({
ok: true,
status: 200,
});
global.fetch = fetchSpy as typeof fetch;
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
consoleWarnSpy.mockRestore();
vi.clearAllMocks();
});
describe("successful tracking", () => {
test("should send POST request with correct parameters including merchantId", async () => {
const callCountBefore = getTrackingRequests().length;
await trackPurchaseStatus({
customerId: "cust-456",
orderId: "order-789",
token: "purchase-token",
merchantId: "merchant-explicit",
});
expect(getTrackingRequests().length).toBe(callCountBefore + 1);
expect(getLastTrackingRequest()).toEqual([
TRACK_PURCHASE_URL,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"x-wallet-sdk-auth": "token-123",
"x-frak-client-id": "test-client-id",
},
body: JSON.stringify({
customerId: "cust-456",
orderId: "order-789",
token: "purchase-token",
merchantId: "merchant-explicit",
}),
},
]);
});
test("should include x-frak-client-id header", async () => {
setupStorage({
interactionToken: null,
merchantId: null,
clientId: "test-client-id",
});
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
headers: Record<string, string>;
};
expect(requestInit.headers).toEqual({
Accept: "application/json",
"Content-Type": "application/json",
"x-frak-client-id": "test-client-id",
});
});
test("should include x-wallet-sdk-auth header when interaction token exists", async () => {
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
headers: Record<string, string>;
};
expect(requestInit.headers).toEqual({
Accept: "application/json",
"Content-Type": "application/json",
"x-wallet-sdk-auth": "token-123",
"x-frak-client-id": "test-client-id",
});
});
test("should handle numeric customerId and orderId", async () => {
await trackPurchaseStatus({
customerId: 12345,
orderId: 67890,
token: "purchase-token",
merchantId: "merchant-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
body: string;
};
expect(requestInit.body).toBe(
JSON.stringify({
customerId: 12345,
orderId: 67890,
token: "purchase-token",
merchantId: "merchant-1",
})
);
});
test("should use new endpoint URL /user/track/purchase", async () => {
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
expect(getLastTrackingRequest()?.[0]).toBe(TRACK_PURCHASE_URL);
});
});
describe("merchantId resolution", () => {
test("should resolve merchantId from explicit param first", async () => {
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
"fetched-merchant-id"
);
const merchantLookupCallsBefore = vi.mocked(
sdkConfigStore.resolveMerchantId
).mock.calls.length;
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "explicit-merchant-id",
});
const requestInit = getLastTrackingRequest()?.[1] as {
body: string;
};
expect(requestInit.body).toBe(
JSON.stringify({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "explicit-merchant-id",
})
);
expect(
vi.mocked(sdkConfigStore.resolveMerchantId).mock.calls.length
).toBe(merchantLookupCallsBefore);
});
test("should fall back to sessionStorage for merchantId", async () => {
vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
"session-merchant-id"
);
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
body: string;
};
expect(requestInit.body).toBe(
JSON.stringify({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "session-merchant-id",
})
);
expect(
vi.mocked(sdkConfigStore.resolveMerchantId)
).toHaveBeenCalled();
});
test("should fall back to resolveMerchantId when no explicit merchantId", async () => {
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
"fetched-merchant-id"
);
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
body: string;
};
expect(requestInit.body).toBe(
JSON.stringify({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "fetched-merchant-id",
})
);
});
test("should warn and skip when no merchantId available", async () => {
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
undefined
);
const callCountBefore = getTrackingRequests().length;
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
});
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[Frak] No merchant id found, skipping purchase check"
);
expect(getTrackingRequests().length).toBe(callCountBefore);
});
});
describe("anonymous user support", () => {
test("should send request with only x-frak-client-id when no interaction token", async () => {
setupStorage({
interactionToken: null,
merchantId: null,
clientId: "test-client-id",
});
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
headers: Record<string, string>;
};
expect(requestInit.headers).toEqual({
Accept: "application/json",
"Content-Type": "application/json",
"x-frak-client-id": "test-client-id",
});
});
test("should send request with both headers when both available", async () => {
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
const requestInit = getLastTrackingRequest()?.[1] as {
headers: Record<string, string>;
};
expect(requestInit.headers).toEqual({
Accept: "application/json",
"Content-Type": "application/json",
"x-wallet-sdk-auth": "token-123",
"x-frak-client-id": "test-client-id",
});
});
test("should skip when no identity available", async () => {
setupStorage({
interactionToken: null,
merchantId: "merchant-1",
clientId: null,
});
vi.mocked(getClientId).mockReturnValue("");
const callCountBefore = getTrackingRequests().length;
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
});
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[Frak] No identity found, skipping purchase check"
);
expect(getTrackingRequests().length).toBe(callCountBefore);
});
});
describe("missing identity", () => {
test("should warn when no identity sources available", async () => {
setupStorage({
interactionToken: null,
merchantId: "merchant-1",
clientId: null,
});
vi.mocked(getClientId).mockReturnValue("");
const callCountBefore = getTrackingRequests().length;
await trackPurchaseStatus({
customerId: "cust-456",
orderId: "order-789",
token: "purchase-token",
});
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[Frak] No identity found, skipping purchase check"
);
expect(getTrackingRequests().length).toBe(callCountBefore);
});
});
describe("non-browser environment", () => {
test("should warn and skip when window is undefined", async () => {
const savedWindow = globalThis.window;
Reflect.deleteProperty(globalThis, "window");
const callCountBefore = getTrackingRequests().length;
try {
await trackPurchaseStatus({
customerId: "cust-1",
orderId: "order-1",
token: "token-1",
merchantId: "merchant-1",
});
} finally {
Object.defineProperty(globalThis, "window", {
value: savedWindow,
writable: true,
configurable: true,
});
}
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[Frak] No window found, can't track purchase"
);
expect(getTrackingRequests().length).toBe(callCountBefore);
});
});
describe("network errors", () => {
test("should handle fetch rejection", async () => {
vi.mocked(getClientId).mockReturnValue("test-client-id");
setupStorage({
interactionToken: "token-123",
merchantId: null,
clientId: "test-client-id",
});
fetchSpy.mockRejectedValueOnce(new Error("Network error"));
await expect(
trackPurchaseStatus({
customerId: "cust-456",
orderId: "order-789",
token: "purchase-token",
merchantId: "merchant-1",
})
).rejects.toThrow("Network error");
});
test("should handle fetch with error response", async () => {
fetchSpy.mockResolvedValue({
ok: false,
status: 500,
});
await trackPurchaseStatus({
customerId: "cust-456",
orderId: "order-789",
token: "purchase-token",
merchantId: "merchant-1",
});
expect(fetchSpy).toHaveBeenCalled();
});
});
});