/// import { ApiResponseSchema, cryptoAssetsApi, useGetTokensDataInfiniteQuery, useFindTokenByIdQuery, useFindTokenByAddressInCurrencyQuery, useGetTokensSyncHashQuery, transformTokensResponse, transformApiTokenToTokenCurrency, validateAndTransformSingleTokenResponse, } from "./api"; import type { ApiTokenResponse } from "../entities"; import { getEnv } from "@ledgerhq/live-env"; import type { TokenCurrency } from "@ledgerhq/types-cryptoassets"; import type { FetchBaseQueryMeta } from "@reduxjs/toolkit/query/react"; // Mock live-env jest.mock("@ledgerhq/live-env", () => ({ getEnv: jest.fn(), })); // Mock api-token-converter jest.mock("../../api-token-converter", () => ({ convertApiToken: jest.fn(), legacyIdToApiId: jest.fn((id: string) => id), })); import { convertApiToken } from "../../api-token-converter"; const mockGetEnv = getEnv as jest.MockedFunction; describe("api.ts", () => { beforeEach(() => { jest.clearAllMocks(); mockGetEnv.mockImplementation((key: string) => { if (key === "CAL_SERVICE_URL") return "https://cal.api.live.ledger.com"; if (key === "CAL_SERVICE_URL_STAGING") return "https://cal.api.staging.ledger.com"; if (key === "LEDGER_CLIENT_VERSION") return "1.0.0"; return ""; }); }); describe("ApiResponseSchema", () => { const mockApiTokenResponse: ApiTokenResponse = { id: "ethereum/erc20/usd_coin", contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], standard: "erc20", decimals: 6, delisted: false, live_signature: "3045022100...", }; it("should validate an array of tokens", () => { const result = ApiResponseSchema.parse([mockApiTokenResponse]); expect(result).toHaveLength(1); expect(result[0]).toEqual(mockApiTokenResponse); }); it("should validate an empty array", () => { const result = ApiResponseSchema.parse([]); expect(result).toHaveLength(0); }); it("should validate multiple tokens", () => { const result = ApiResponseSchema.parse([mockApiTokenResponse, mockApiTokenResponse]); expect(result).toHaveLength(2); }); it("should throw on invalid data", () => { expect(() => ApiResponseSchema.parse("not an array")).toThrow(); }); it("should throw on invalid token structure", () => { expect(() => ApiResponseSchema.parse([{ invalid: "data" }])).toThrow(); }); it("should throw on missing required fields", () => { const invalidToken = { ...mockApiTokenResponse }; // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (invalidToken as any).id; expect(() => ApiResponseSchema.parse([invalidToken])).toThrow(); }); it("should validate token with optional fields", () => { const tokenWithOptionals = { ...mockApiTokenResponse, token_identifier: "some-identifier", }; const result = ApiResponseSchema.parse([tokenWithOptionals]); expect(result[0].token_identifier).toBe("some-identifier"); }); it("should validate token without optional fields", () => { const tokenWithoutOptionals = { ...mockApiTokenResponse }; delete tokenWithoutOptionals.token_identifier; delete tokenWithoutOptionals.live_signature; const result = ApiResponseSchema.parse([tokenWithoutOptionals]); expect(result[0]).toBeDefined(); expect(result[0].token_identifier).toBeUndefined(); expect(result[0].live_signature).toBeUndefined(); }); }); describe("cryptoAssetsApi configuration", () => { it("should have the correct reducer path", () => { expect(cryptoAssetsApi.reducerPath).toBe("cryptoAssetsApi"); }); it("should have findTokenById endpoint", () => { expect(cryptoAssetsApi.endpoints.findTokenById).toBeDefined(); }); it("should have findTokenByAddressInCurrency endpoint", () => { expect(cryptoAssetsApi.endpoints.findTokenByAddressInCurrency).toBeDefined(); }); it("should have getTokensSyncHash endpoint", () => { expect(cryptoAssetsApi.endpoints.getTokensSyncHash).toBeDefined(); }); it("should have getTokensData endpoint", () => { expect(cryptoAssetsApi.endpoints.getTokensData).toBeDefined(); }); }); describe("hook exports", () => { it("should export hooks from the API", () => { // Imported at the top of file already validates these exist expect(useGetTokensDataInfiniteQuery).toBeDefined(); expect(useFindTokenByIdQuery).toBeDefined(); expect(useFindTokenByAddressInCurrencyQuery).toBeDefined(); expect(useGetTokensSyncHashQuery).toBeDefined(); }); }); describe("API integration tests", () => { it("should have correct interface types for TokenByIdParams", () => { const params: import("./api").TokenByIdParams = { id: "ethereum/erc20/usdc", }; expect(params.id).toBe("ethereum/erc20/usdc"); }); it("should have correct interface types for TokenByAddressInCurrencyParams", () => { const params: import("./api").TokenByAddressInCurrencyParams = { contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", network: "ethereum", }; expect(params.contract_address).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); expect(params.network).toBe("ethereum"); }); }); describe("baseQuery configuration", () => { it("should be properly configured", () => { // The prepareHeaders function is used internally // We can verify it's called correctly by checking getEnv calls expect(cryptoAssetsApi).toBeDefined(); expect(cryptoAssetsApi.reducerPath).toBe("cryptoAssetsApi"); }); }); describe("transformTokensResponse", () => { const mockApiTokenResponse: ApiTokenResponse = { id: "ethereum/erc20/usd_coin", contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], standard: "erc20", decimals: 6, delisted: false, live_signature: "3045022100...", }; const mockTokenCurrency: TokenCurrency = { type: "TokenCurrency", id: "ethereum/erc20/usd_coin", contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], tokenType: "erc20", delisted: false, disableCountervalue: false, parentCurrency: {} as any, }; beforeEach(() => { (convertApiToken as jest.MockedFunction).mockReturnValue( mockTokenCurrency, ); }); it("should transform an array of API tokens to TokenCurrency array", () => { const result = transformTokensResponse([mockApiTokenResponse]); expect(result.tokens).toHaveLength(1); expect(result.tokens[0]).toEqual(mockTokenCurrency); }); it("should extract nextCursor from response headers", () => { const meta: Partial = { response: { headers: { get: jest.fn((header: string) => { if (header === "x-ledger-next") return "next-cursor-value"; return null; }), } as any, } as any, }; const result = transformTokensResponse([mockApiTokenResponse], meta as FetchBaseQueryMeta); expect(result.pagination.nextCursor).toBe("next-cursor-value"); }); it("should set nextCursor to undefined when no header is present", () => { const meta: Partial = { response: { headers: { get: jest.fn(() => null), } as any, } as any, }; const result = transformTokensResponse([mockApiTokenResponse], meta as FetchBaseQueryMeta); expect(result.pagination.nextCursor).toBeUndefined(); }); it("should set nextCursor to undefined when meta is not provided", () => { const result = transformTokensResponse([mockApiTokenResponse]); expect(result.pagination.nextCursor).toBeUndefined(); }); it("should filter out tokens that fail conversion", () => { (convertApiToken as jest.MockedFunction) .mockReturnValueOnce(mockTokenCurrency) .mockReturnValueOnce(undefined) .mockReturnValueOnce(mockTokenCurrency); const result = transformTokensResponse([ mockApiTokenResponse, mockApiTokenResponse, mockApiTokenResponse, ]); expect(result.tokens).toHaveLength(2); }); it("should handle empty token array", () => { const result = transformTokensResponse([]); expect(result.tokens).toHaveLength(0); expect(result.pagination.nextCursor).toBeUndefined(); }); }); describe("transformApiTokenToTokenCurrency", () => { const mockApiTokenResponse: ApiTokenResponse = { id: "ethereum/erc20/usd_coin", contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], standard: "erc20", decimals: 6, delisted: false, live_signature: "3045022100...", }; const mockTokenCurrency: TokenCurrency = { type: "TokenCurrency", id: "ethereum/erc20/usd_coin", contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], tokenType: "erc20", delisted: false, disableCountervalue: false, parentCurrency: {} as any, }; beforeEach(() => { (convertApiToken as jest.MockedFunction).mockReturnValue( mockTokenCurrency, ); }); it("should convert ApiTokenResponse to TokenCurrency", () => { const result = transformApiTokenToTokenCurrency(mockApiTokenResponse); expect(result).toEqual(mockTokenCurrency); }); it("should call convertApiToken with correct parameters", () => { transformApiTokenToTokenCurrency(mockApiTokenResponse); expect(convertApiToken).toHaveBeenCalledWith({ id: mockApiTokenResponse.id, contractAddress: mockApiTokenResponse.contract_address, name: mockApiTokenResponse.name, ticker: mockApiTokenResponse.ticker, units: mockApiTokenResponse.units, standard: mockApiTokenResponse.standard, tokenIdentifier: mockApiTokenResponse.token_identifier, delisted: mockApiTokenResponse.delisted, ledgerSignature: mockApiTokenResponse.live_signature, }); }); it("should return undefined when convertApiToken returns undefined", () => { (convertApiToken as jest.MockedFunction).mockReturnValue(undefined); const result = transformApiTokenToTokenCurrency(mockApiTokenResponse); expect(result).toBeUndefined(); }); it("should handle tokens without optional fields", () => { const tokenWithoutOptionals: ApiTokenResponse = { ...mockApiTokenResponse, token_identifier: undefined, live_signature: undefined, }; transformApiTokenToTokenCurrency(tokenWithoutOptionals); expect(convertApiToken).toHaveBeenCalledWith( expect.objectContaining({ tokenIdentifier: undefined, ledgerSignature: undefined, }), ); }); it("should handle tokens with token_identifier", () => { const tokenWithIdentifier: ApiTokenResponse = { ...mockApiTokenResponse, token_identifier: "some-identifier", }; transformApiTokenToTokenCurrency(tokenWithIdentifier); expect(convertApiToken).toHaveBeenCalledWith( expect.objectContaining({ tokenIdentifier: "some-identifier", }), ); }); }); describe("validateAndTransformSingleTokenResponse", () => { const mockApiTokenResponse: ApiTokenResponse = { id: "ethereum/erc20/usd_coin", contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], standard: "erc20", decimals: 6, delisted: false, live_signature: "3045022100...", }; const mockTokenCurrency: TokenCurrency = { type: "TokenCurrency", id: "ethereum/erc20/usd_coin", contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", ticker: "USDC", units: [ { code: "USDC", name: "USD Coin", magnitude: 6, }, ], tokenType: "erc20", delisted: false, disableCountervalue: false, parentCurrency: {} as any, }; beforeEach(() => { (convertApiToken as jest.MockedFunction).mockReturnValue( mockTokenCurrency, ); }); it("should validate and transform a single token response", () => { const result = validateAndTransformSingleTokenResponse([mockApiTokenResponse]); expect(result).toEqual(mockTokenCurrency); }); it("should return undefined when array is empty", () => { const result = validateAndTransformSingleTokenResponse([]); expect(result).toBeUndefined(); }); it("should throw when response is not an array", () => { expect(() => validateAndTransformSingleTokenResponse("not an array")).toThrow(); }); it("should throw when token structure is invalid", () => { expect(() => validateAndTransformSingleTokenResponse([{ invalid: "data" }])).toThrow(); }); it("should call convertApiToken with correct parameters", () => { validateAndTransformSingleTokenResponse([mockApiTokenResponse]); expect(convertApiToken).toHaveBeenCalledWith({ id: mockApiTokenResponse.id, contractAddress: mockApiTokenResponse.contract_address, name: mockApiTokenResponse.name, ticker: mockApiTokenResponse.ticker, units: mockApiTokenResponse.units, standard: mockApiTokenResponse.standard, tokenIdentifier: mockApiTokenResponse.token_identifier, delisted: mockApiTokenResponse.delisted, ledgerSignature: mockApiTokenResponse.live_signature, }); }); it("should return undefined when convertApiToken returns undefined", () => { (convertApiToken as jest.MockedFunction).mockReturnValue(undefined); const result = validateAndTransformSingleTokenResponse([mockApiTokenResponse]); expect(result).toBeUndefined(); }); it("should handle tokens with optional fields", () => { const tokenWithOptionals: ApiTokenResponse = { ...mockApiTokenResponse, token_identifier: "some-identifier", live_signature: "signature", }; validateAndTransformSingleTokenResponse([tokenWithOptionals]); expect(convertApiToken).toHaveBeenCalledWith( expect.objectContaining({ tokenIdentifier: "some-identifier", ledgerSignature: "signature", }), ); }); it("should validate token schema before transformation", () => { const invalidToken = { ...mockApiTokenResponse, units: [], // units must have at least 1 item }; expect(() => validateAndTransformSingleTokenResponse([invalidToken])).toThrow(); }); }); });