From 80cce7c57a8b4b8e2c9747f15e63d3615178159f Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 20 May 2024 09:13:31 -0500 Subject: [PATCH 1/9] Remove axios-mock-adapter and update client access in Models --- package.json | 1 - src/coinbase/address.ts | 16 ++-- src/coinbase/coinbase.ts | 18 ++-- src/coinbase/tests/address_test.ts | 127 ++++++++++------------------ src/coinbase/tests/coinbase_test.ts | 43 ++++------ src/coinbase/tests/user_test.ts | 11 +-- src/coinbase/tests/utils.ts | 79 ++++++++++++++++- src/coinbase/tests/wallet_test.ts | 32 ++----- src/coinbase/user.ts | 12 +-- src/coinbase/wallet.ts | 6 +- yarn.lock | 13 --- 11 files changed, 167 insertions(+), 191 deletions(-) diff --git a/package.json b/package.json index 639e4b58..376ac1ca 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@types/secp256k1": "^4.0.6", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", - "axios-mock-adapter": "^1.22.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^48.2.5", diff --git a/src/coinbase/address.ts b/src/coinbase/address.ts index 7c6a9e5f..3375179d 100644 --- a/src/coinbase/address.ts +++ b/src/coinbase/address.ts @@ -1,9 +1,9 @@ import { Address as AddressModel } from "../client"; import { Balance } from "./balance"; import { BalanceMap } from "./balance_map"; +import { Coinbase } from "./coinbase"; import { InternalError } from "./errors"; import { FaucetTransaction } from "./faucet_transaction"; -import { AddressAPIClient } from "./types"; import { Decimal } from "decimal.js"; /** @@ -11,24 +11,18 @@ import { Decimal } from "decimal.js"; */ export class Address { private model: AddressModel; - private client: AddressAPIClient; /** * Initializes a new Address instance. * * @param {AddressModel} model - The address model data. - * @param {AddressAPIClient} client - The API client to interact with address-related endpoints. * @throws {InternalError} If the model or client is empty. */ - constructor(model: AddressModel, client: AddressAPIClient) { + constructor(model: AddressModel) { if (!model) { throw new InternalError("Address model cannot be empty"); } - if (!client) { - throw new InternalError("Address client cannot be empty"); - } this.model = model; - this.client = client; } /** @@ -40,7 +34,7 @@ export class Address { * @throws {Error} If the request fails. */ async faucet(): Promise { - const response = await this.client.requestFaucetFunds( + const response = await Coinbase.apiClients.address!.requestFaucetFunds( this.model.wallet_id, this.model.address_id, ); @@ -71,7 +65,7 @@ export class Address { * @returns {BalanceMap} - The map from asset ID to balance. */ async listBalances(): Promise { - const response = await this.client.listAddressBalances( + const response = await Coinbase.apiClients.address!.listAddressBalances( this.model.wallet_id, this.model.address_id, ); @@ -86,7 +80,7 @@ export class Address { * @returns {Decimal} The balance of the asset. */ async getBalance(assetId: string): Promise { - const response = await this.client.getAddressBalance( + const response = await Coinbase.apiClients.address!.getAddressBalance( this.model.wallet_id, this.model.address_id, assetId, diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index acb42396..1c68a0ae 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -42,7 +42,7 @@ export class Coinbase { Weth: "weth", }; - apiClients: ApiClients = {}; + static apiClients: ApiClients = {}; /** * Represents the number of Wei per Ether. @@ -85,11 +85,13 @@ export class Coinbase { response => logApiResponse(response, debugging), ); - this.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); - this.apiClients.wallet = WalletsApiFactory(config, BASE_PATH, axiosInstance); - this.apiClients.address = AddressesApiFactory(config, BASE_PATH, axiosInstance); - this.apiClients.transfer = TransfersApiFactory(config, BASE_PATH, axiosInstance); - this.apiClients.baseSepoliaProvider = new ethers.JsonRpcProvider("https://sepolia.base.org"); + Coinbase.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); + Coinbase.apiClients.wallet = WalletsApiFactory(config, BASE_PATH, axiosInstance); + Coinbase.apiClients.address = AddressesApiFactory(config, BASE_PATH, axiosInstance); + Coinbase.apiClients.transfer = TransfersApiFactory(config, BASE_PATH, axiosInstance); + Coinbase.apiClients.baseSepoliaProvider = new ethers.JsonRpcProvider( + "https://sepolia.base.org", + ); } /** @@ -137,7 +139,7 @@ export class Coinbase { * @throws {APIError} If the request fails. */ async getDefaultUser(): Promise { - const userResponse = await this.apiClients.user!.getCurrentUser(); - return new User(userResponse.data as UserModel, this.apiClients); + const userResponse = await Coinbase.apiClients.user!.getCurrentUser(); + return new User(userResponse.data as UserModel); } } diff --git a/src/coinbase/tests/address_test.ts b/src/coinbase/tests/address_test.ts index 764659ee..600957e0 100644 --- a/src/coinbase/tests/address_test.ts +++ b/src/coinbase/tests/address_test.ts @@ -1,20 +1,14 @@ import { ethers } from "ethers"; -import { - AddressBalanceList, - AddressesApiFactory, - Address as AddressModel, - Balance as BalanceModel, -} from "../../client"; +import { Address as AddressModel } from "../../client"; import { Address } from "./../address"; import { FaucetTransaction } from "./../faucet_transaction"; -import MockAdapter from "axios-mock-adapter"; import { randomUUID } from "crypto"; +import Decimal from "decimal.js"; import { APIError, FaucetLimitReachedError } from "../api_error"; import { Coinbase } from "../coinbase"; import { InternalError } from "../errors"; -import { createAxiosMock } from "./utils"; -import Decimal from "decimal.js"; +import { VALID_BALANCE_MODEL, VALID_ADDRESS_BALANCE_LIST, addressesApiMock } from "./utils"; const newEthAddress = ethers.Wallet.createRandom(); @@ -25,62 +19,12 @@ const VALID_ADDRESS_MODEL: AddressModel = { wallet_id: randomUUID(), }; -const VALID_BALANCE_MODEL: BalanceModel = { - amount: "1000000000000000000", - asset: { - asset_id: Coinbase.assetList.Eth, - network_id: Coinbase.networkList.BaseSepolia, - }, -}; - -const VALID_ADDRESS_BALANCE_LIST: AddressBalanceList = { - data: [ - { - amount: "1000000000000000000", - asset: { - asset_id: Coinbase.assetList.Eth, - network_id: Coinbase.networkList.BaseSepolia, - decimals: 18, - }, - }, - { - amount: "5000000000", - asset: { - asset_id: "usdc", - network_id: Coinbase.networkList.BaseSepolia, - decimals: 6, - }, - }, - { - amount: "3000000000000000000", - asset: { - asset_id: "weth", - network_id: Coinbase.networkList.BaseSepolia, - decimals: 6, - }, - }, - ], - has_more: false, - next_page: "", - total_count: 3, -}; - // Test suite for Address class describe("Address", () => { - const [axiosInstance, configuration, BASE_PATH] = createAxiosMock(); - const client = AddressesApiFactory(configuration, BASE_PATH, axiosInstance); - let address: Address, axiosMock; - - beforeAll(() => { - axiosMock = new MockAdapter(axiosInstance); - }); + let address: Address; beforeEach(() => { - address = new Address(VALID_ADDRESS_MODEL, client); - }); - - afterEach(() => { - axiosMock.reset(); + address = new Address(VALID_ADDRESS_MODEL); }); it("should initialize a new Address", () => { @@ -96,7 +40,10 @@ describe("Address", () => { }); it("should return the correct list of balances", async () => { - axiosMock.onGet().reply(200, VALID_ADDRESS_BALANCE_LIST); + Coinbase.apiClients.address = { + ...addressesApiMock, + listAddressBalances: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), + }; const balances = await address.listBalances(); expect(balances.get(Coinbase.assetList.Eth)).toEqual(new Decimal(1)); expect(balances.get("usdc")).toEqual(new Decimal(5000)); @@ -104,28 +51,40 @@ describe("Address", () => { }); it("should return the correct ETH balance", async () => { - axiosMock.onGet().reply(200, VALID_BALANCE_MODEL); + Coinbase.apiClients.address = { + ...addressesApiMock, + getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + }; const ethBalance = await address.getBalance(Coinbase.assetList.Eth); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1)); }); it("should return the correct Gwei balance", async () => { - axiosMock.onGet().reply(200, VALID_BALANCE_MODEL); + Coinbase.apiClients.address = { + ...addressesApiMock, + getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + }; const ethBalance = await address.getBalance("gwei"); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000)); }); it("should return the correct Wei balance", async () => { - axiosMock.onGet().reply(200, VALID_BALANCE_MODEL); + Coinbase.apiClients.address = { + ...addressesApiMock, + getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + }; const ethBalance = await address.getBalance("wei"); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000000000000)); }); it("should return an error for an unsupported asset", async () => { - axiosMock.onGet().reply(404, null); + Coinbase.apiClients.address = { + ...addressesApiMock, + getAddressBalance: jest.fn().mockRejectedValue(new APIError("")), + }; try { await address.getBalance("unsupported-asset"); fail("Expect 404 to be thrown"); @@ -139,41 +98,43 @@ describe("Address", () => { }); it("should throw an InternalError when model is not provided", () => { - expect(() => new Address(null!, client)).toThrow(`Address model cannot be empty`); - }); - - it("should throw an InternalError when client is not provided", () => { - expect(() => new Address(VALID_ADDRESS_MODEL, null!)).toThrow(`Address client cannot be empty`); + expect(() => new Address(null!)).toThrow(`Address model cannot be empty`); }); it("should request funds from the faucet and returns the faucet transaction", async () => { const transactionHash = "0xdeadbeef"; - axiosMock.onPost().reply(200, { - transaction_hash: transactionHash, - }); + Coinbase.apiClients.address = { + ...addressesApiMock, + requestFaucetFunds: jest + .fn() + .mockResolvedValue({ data: { transaction_hash: transactionHash } }), + }; const faucetTransaction = await address.faucet(); expect(faucetTransaction).toBeInstanceOf(FaucetTransaction); expect(faucetTransaction.getTransactionHash()).toBe(transactionHash); }); it("should throw an APIError when the request is unsuccesful", async () => { - axiosMock.onPost().reply(400); + Coinbase.apiClients.address = { + ...addressesApiMock, + requestFaucetFunds: jest.fn().mockRejectedValue(new APIError("")), + }; await expect(address.faucet()).rejects.toThrow(APIError); }); it("should throw a FaucetLimitReachedError when the faucet limit is reached", async () => { - axiosMock.onPost().reply(429, { - code: "faucet_limit_reached", - message: "Faucet limit reached", - }); + Coinbase.apiClients.address = { + ...addressesApiMock, + requestFaucetFunds: jest.fn().mockRejectedValue(new FaucetLimitReachedError("")), + }; await expect(address.faucet()).rejects.toThrow(FaucetLimitReachedError); }); it("should throw an InternalError when the request fails unexpectedly", async () => { - axiosMock.onPost().reply(500, { - code: "internal", - message: "unexpected error occurred while requesting faucet funds", - }); + Coinbase.apiClients.address = { + ...addressesApiMock, + requestFaucetFunds: jest.fn().mockRejectedValue(new InternalError("")), + }; await expect(address.faucet()).rejects.toThrow(InternalError); }); diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 6a887b12..a9e66298 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -1,17 +1,10 @@ -import { Coinbase } from "../coinbase"; -import MockAdapter from "axios-mock-adapter"; -import axios from "axios"; import { APIError } from "../api_error"; -import { VALID_WALLET_MODEL } from "./wallet_test"; +import { Coinbase } from "../coinbase"; +import { VALID_WALLET_MODEL, addressesApiMock, usersApiMock, walletsApiMock } from "./utils"; -const axiosMock = new MockAdapter(axios); const PATH_PREFIX = "./src/coinbase/tests/config"; describe("Coinbase tests", () => { - beforeEach(() => { - axiosMock.reset(); - }); - it("should throw an error if the API key name or private key is empty", () => { expect(() => new Coinbase("", "test")).toThrow("Invalid configuration: apiKeyName is empty"); expect(() => new Coinbase("test", "")).toThrow("Invalid configuration: privateKey is empty"); @@ -41,28 +34,20 @@ describe("Coinbase tests", () => { }); describe("should able to interact with the API", () => { + let user; const cbInstance = Coinbase.configureFromJson( `${PATH_PREFIX}/coinbase_cloud_api_key.json`, true, ); - let user; - beforeEach(async () => { - axiosMock.reset(); - axiosMock - .onPost(/\/v1\/wallets\/.*\/addresses\/.*\/faucet/) - .reply(200, { transaction_hash: "0xdeadbeef" }) - .onGet(/\/me/) - .reply(200, { - id: 123, - }) - .onPost(/\/v1\/wallets/) - .reply(200, VALID_WALLET_MODEL) - .onGet(/\/v1\/wallets\/.*/) - .reply(200, VALID_WALLET_MODEL); + + it("should return the correct user ID", async () => { + Coinbase.apiClients = { + user: usersApiMock, + wallet: walletsApiMock, + address: addressesApiMock, + }; user = await cbInstance.getDefaultUser(); - }); - it("should return the correct user ID", () => { expect(user.getId()).toBe(123); expect(user.toString()).toBe("User{ userId: 123 }"); }); @@ -72,7 +57,7 @@ describe("Coinbase tests", () => { expect(wallet.getId()).toBe(VALID_WALLET_MODEL.id); const defaultAddress = wallet.defaultAddress(); - expect(defaultAddress?.getId()).toBe(VALID_WALLET_MODEL.default_address.address_id); + expect(defaultAddress?.getId()).toBe(VALID_WALLET_MODEL.default_address?.address_id); const faucetTransaction = await wallet?.faucet(); expect(faucetTransaction.getTransactionHash()).toBe("0xdeadbeef"); @@ -80,7 +65,11 @@ describe("Coinbase tests", () => { }); it("should raise an error if the user is not found", async () => { - axiosMock.onGet().reply(404); + jest.mock("../../client/api", () => ({ + UsersApiFactory: jest.fn().mockReturnValue({ + me: jest.fn().mockRejectedValue(new APIError("")), + }), + })); const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); await expect(cbInstance.getDefaultUser()).rejects.toThrow(APIError); }); diff --git a/src/coinbase/tests/user_test.ts b/src/coinbase/tests/user_test.ts index fab4292d..10443497 100644 --- a/src/coinbase/tests/user_test.ts +++ b/src/coinbase/tests/user_test.ts @@ -1,27 +1,22 @@ -import { User } from "./../user"; -import { ApiClients } from "./../types"; import { User as UserModel } from "./../../client/api"; +import { User } from "./../user"; describe("User Class", () => { let mockUserModel: UserModel; - let mockApiClients: ApiClients; - beforeEach(() => { mockUserModel = { id: "12345", } as UserModel; - - mockApiClients = {} as ApiClients; }); it("should initialize User instance with a valid user model and API clients, and set the user ID correctly", () => { - const user = new User(mockUserModel, mockApiClients); + const user = new User(mockUserModel); expect(user).toBeInstanceOf(User); expect(user.getId()).toBe(mockUserModel.id); }); it("should return a correctly formatted string representation of the User instance", () => { - const user = new User(mockUserModel, mockApiClients); + const user = new User(mockUserModel); expect(user.toString()).toBe(`User{ userId: ${mockUserModel.id} }`); }); }); diff --git a/src/coinbase/tests/utils.ts b/src/coinbase/tests/utils.ts index d9c961b7..66a7b153 100644 --- a/src/coinbase/tests/utils.ts +++ b/src/coinbase/tests/utils.ts @@ -1,8 +1,67 @@ import axios, { AxiosInstance } from "axios"; -import { Configuration } from "../../client"; +import { randomUUID } from "crypto"; +import { + Configuration, + Wallet as WalletModel, + Balance as BalanceModel, + AddressBalanceList, +} from "../../client"; import { BASE_PATH } from "../../client/base"; +import { Coinbase } from "../coinbase"; import { registerAxiosInterceptors } from "../utils"; +export const walletId = randomUUID(); +export const VALID_WALLET_MODEL: WalletModel = { + id: randomUUID(), + network_id: Coinbase.networkList.BaseSepolia, + default_address: { + wallet_id: walletId, + address_id: "0xdeadbeef", + public_key: "0x1234567890", + network_id: Coinbase.networkList.BaseSepolia, + }, +}; + +export const VALID_BALANCE_MODEL: BalanceModel = { + amount: "1000000000000000000", + asset: { + asset_id: Coinbase.assetList.Eth, + network_id: Coinbase.networkList.BaseSepolia, + }, +}; + +export const VALID_ADDRESS_BALANCE_LIST: AddressBalanceList = { + data: [ + { + amount: "1000000000000000000", + asset: { + asset_id: Coinbase.assetList.Eth, + network_id: Coinbase.networkList.BaseSepolia, + decimals: 18, + }, + }, + { + amount: "5000000000", + asset: { + asset_id: "usdc", + network_id: Coinbase.networkList.BaseSepolia, + decimals: 6, + }, + }, + { + amount: "3000000000000000000", + asset: { + asset_id: "weth", + network_id: Coinbase.networkList.BaseSepolia, + decimals: 6, + }, + }, + ], + has_more: false, + next_page: "", + total_count: 3, +}; + /** * AxiosMockReturn type. Represents the Axios instance, configuration, and base path. */ @@ -10,6 +69,7 @@ type AxiosMockType = [AxiosInstance, Configuration, string]; /** * Returns an Axios instance with interceptors and configuration for testing. + * * @returns {AxiosMockType} - The Axios instance, configuration, and base path. */ export const createAxiosMock = (): AxiosMockType => { @@ -22,3 +82,20 @@ export const createAxiosMock = (): AxiosMockType => { const configuration = new Configuration(); return [axiosInstance, configuration, BASE_PATH]; }; + +export const usersApiMock = { + getCurrentUser: jest.fn().mockResolvedValue({ data: { id: 123 } }), +}; + +export const walletsApiMock = { + getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), + createWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), +}; + +export const addressesApiMock = { + requestFaucetFunds: jest.fn().mockResolvedValue({ data: { transaction_hash: "0xdeadbeef" } }), + getAddress: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), + getAddressBalance: jest.fn().mockResolvedValue({ data: { VALID_BALANCE_MODEL } }), + listAddressBalances: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), + createAddress: jest.fn().mockResolvedValue({ data: VALID_WALLET_MODEL.default_address }), +}; diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index b82240fc..1369e6ed 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -1,44 +1,22 @@ -import MockAdapter from "axios-mock-adapter"; import * as bip39 from "bip39"; -import { randomUUID } from "crypto"; -import { AddressesApiFactory, WalletsApiFactory } from "../../client"; import { Coinbase } from "../coinbase"; import { ArgumentError } from "../errors"; import { Wallet } from "../wallet"; -import { createAxiosMock } from "./utils"; - -const walletId = randomUUID(); -export const VALID_WALLET_MODEL = { - id: randomUUID(), - network_id: Coinbase.networkList.BaseSepolia, - default_address: { - wallet_id: walletId, - address_id: "0xdeadbeef", - public_key: "0x1234567890", - network_id: Coinbase.networkList.BaseSepolia, - }, -}; +import { VALID_WALLET_MODEL, addressesApiMock, walletsApiMock } from "./utils"; describe("Wallet Class", () => { - let wallet, axiosMock; + let wallet; const seed = bip39.generateMnemonic(); - const [axiosInstance, configuration, BASE_PATH] = createAxiosMock(); const client = { - wallet: WalletsApiFactory(configuration, BASE_PATH, axiosInstance), - address: AddressesApiFactory(configuration, BASE_PATH, axiosInstance), + wallet: walletsApiMock, + address: addressesApiMock, }; beforeAll(async () => { - axiosMock = new MockAdapter(axiosInstance); - axiosMock.onPost().reply(200, VALID_WALLET_MODEL).onGet().reply(200, VALID_WALLET_MODEL); wallet = await Wallet.init(VALID_WALLET_MODEL, client, seed, 2); }); - afterEach(() => { - axiosMock.reset(); - }); - describe("should initializes a new Wallet", () => { it("should return a Wallet instance", async () => { expect(wallet).toBeInstanceOf(Wallet); @@ -50,7 +28,7 @@ describe("Wallet Class", () => { expect(wallet.getNetworkId()).toBe(Coinbase.networkList.BaseSepolia); }); it("should return the correct default address", async () => { - expect(wallet.defaultAddress()?.getId()).toBe(VALID_WALLET_MODEL.default_address.address_id); + expect(wallet.defaultAddress()?.getId()).toBe(VALID_WALLET_MODEL.default_address?.address_id); }); it("should derive the correct number of addresses", async () => { diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index cf18f862..823d75b5 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -1,4 +1,3 @@ -import { ApiClients } from "./types"; import { User as UserModel } from "./../client/api"; import { Coinbase } from "./coinbase"; import { Wallet } from "./wallet"; @@ -10,16 +9,13 @@ import { Wallet } from "./wallet"; */ export class User { private model: UserModel; - private client: ApiClients; /** * Initializes a new User instance. * * @param user - The user model. - * @param client - The API clients. */ - constructor(user: UserModel, client: ApiClients) { - this.client = client; + constructor(user: UserModel) { this.model = user; } @@ -37,10 +33,10 @@ export class User { network_id: Coinbase.networkList.BaseSepolia, }, }; - const walletData = await this.client.wallet!.createWallet(payload); + const walletData = await Coinbase.apiClients.wallet!.createWallet(payload); return Wallet.init(walletData.data!, { - wallet: this.client.wallet!, - address: this.client.address!, + wallet: Coinbase.apiClients.wallet!, + address: Coinbase.apiClients.address!, }); } diff --git a/src/coinbase/wallet.ts b/src/coinbase/wallet.ts index f7cda42b..a8d621d5 100644 --- a/src/coinbase/wallet.ts +++ b/src/coinbase/wallet.ts @@ -184,7 +184,7 @@ export class Wallet { * @returns {void} */ private cacheAddress(address: AddressModel): void { - this.addresses.push(new Address(address, this.client.address!)); + this.addresses.push(new Address(address)); this.addressIndex++; } @@ -212,9 +212,7 @@ export class Wallet { * @returns The default address */ public defaultAddress(): Address | undefined { - return this.model.default_address - ? new Address(this.model.default_address, this.client.address!) - : undefined; + return this.model.default_address ? new Address(this.model.default_address) : undefined; } /** diff --git a/yarn.lock b/yarn.lock index 5b545c79..edd237bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1059,14 +1059,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios-mock-adapter@^1.22.0: - version "1.22.0" - resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d" - integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw== - dependencies: - fast-deep-equal "^3.1.3" - is-buffer "^2.0.5" - axios@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" @@ -1955,11 +1947,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-buffer@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - is-builtin-module@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" From 65880bbbb7a4ece6958791cdc0f2a47f1519da21 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 20 May 2024 13:47:55 -0500 Subject: [PATCH 2/9] Updating Address tests --- src/coinbase/tests/address_test.ts | 66 ++++++++++++++++++++--------- src/coinbase/tests/coinbase_test.ts | 10 +++++ 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/coinbase/tests/address_test.ts b/src/coinbase/tests/address_test.ts index 600957e0..6e0e493d 100644 --- a/src/coinbase/tests/address_test.ts +++ b/src/coinbase/tests/address_test.ts @@ -21,10 +21,17 @@ const VALID_ADDRESS_MODEL: AddressModel = { // Test suite for Address class describe("Address", () => { + const transactionHash = "0xdeadbeef"; let address: Address; + const getAddressBalance = jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }); + const listAddressBalances = jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }); + const requestFaucetFunds = jest + .fn() + .mockResolvedValue({ data: { transaction_hash: transactionHash } }); beforeEach(() => { address = new Address(VALID_ADDRESS_MODEL); + jest.clearAllMocks(); }); it("should initialize a new Address", () => { @@ -42,55 +49,68 @@ describe("Address", () => { it("should return the correct list of balances", async () => { Coinbase.apiClients.address = { ...addressesApiMock, - listAddressBalances: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), + listAddressBalances, }; const balances = await address.listBalances(); expect(balances.get(Coinbase.assetList.Eth)).toEqual(new Decimal(1)); expect(balances.get("usdc")).toEqual(new Decimal(5000)); expect(balances.get("weth")).toEqual(new Decimal(3)); + expect(listAddressBalances).toHaveBeenCalledWith(address.getWalletId(), address.getId()); + expect(listAddressBalances).toHaveBeenCalledTimes(1); }); it("should return the correct ETH balance", async () => { Coinbase.apiClients.address = { ...addressesApiMock, - getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + getAddressBalance, }; const ethBalance = await address.getBalance(Coinbase.assetList.Eth); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1)); + expect(getAddressBalance).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + Coinbase.assetList.Eth, + ); + expect(getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return the correct Gwei balance", async () => { + const assetId = "gwei"; Coinbase.apiClients.address = { ...addressesApiMock, - getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + getAddressBalance, }; - const ethBalance = await address.getBalance("gwei"); + const ethBalance = await address.getBalance(assetId); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000)); + expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); + expect(getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return the correct Wei balance", async () => { + const assetId = "wei"; Coinbase.apiClients.address = { ...addressesApiMock, - getAddressBalance: jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }), + getAddressBalance, }; - const ethBalance = await address.getBalance("wei"); + const ethBalance = await address.getBalance(assetId); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000000000000)); + expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); + expect(getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return an error for an unsupported asset", async () => { + const getAddressBalance = jest.fn().mockRejectedValue(new APIError("")); + const assetId = "unsupported-asset"; Coinbase.apiClients.address = { ...addressesApiMock, - getAddressBalance: jest.fn().mockRejectedValue(new APIError("")), + getAddressBalance, }; - try { - await address.getBalance("unsupported-asset"); - fail("Expect 404 to be thrown"); - } catch (error) { - expect(error).toBeInstanceOf(APIError); - } + await expect(address.getBalance(assetId)).rejects.toThrow(APIError); + expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); + expect(getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return the wallet ID", () => { @@ -102,40 +122,46 @@ describe("Address", () => { }); it("should request funds from the faucet and returns the faucet transaction", async () => { - const transactionHash = "0xdeadbeef"; Coinbase.apiClients.address = { ...addressesApiMock, - requestFaucetFunds: jest - .fn() - .mockResolvedValue({ data: { transaction_hash: transactionHash } }), + requestFaucetFunds, }; const faucetTransaction = await address.faucet(); expect(faucetTransaction).toBeInstanceOf(FaucetTransaction); expect(faucetTransaction.getTransactionHash()).toBe(transactionHash); + expect(requestFaucetFunds).toHaveBeenCalledWith(address.getWalletId(), address.getId()); + expect(requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw an APIError when the request is unsuccesful", async () => { + const requestFaucetFunds = jest.fn().mockRejectedValue(new APIError("")); Coinbase.apiClients.address = { ...addressesApiMock, - requestFaucetFunds: jest.fn().mockRejectedValue(new APIError("")), + requestFaucetFunds, }; await expect(address.faucet()).rejects.toThrow(APIError); + expect(requestFaucetFunds).toHaveBeenCalledWith(address.getWalletId(), address.getId()); + expect(requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw a FaucetLimitReachedError when the faucet limit is reached", async () => { + const requestFaucetFunds = jest.fn().mockRejectedValue(new FaucetLimitReachedError("")); Coinbase.apiClients.address = { ...addressesApiMock, - requestFaucetFunds: jest.fn().mockRejectedValue(new FaucetLimitReachedError("")), + requestFaucetFunds, }; await expect(address.faucet()).rejects.toThrow(FaucetLimitReachedError); + expect(requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw an InternalError when the request fails unexpectedly", async () => { + const requestFaucetFunds = jest.fn().mockRejectedValue(new InternalError("")); Coinbase.apiClients.address = { ...addressesApiMock, - requestFaucetFunds: jest.fn().mockRejectedValue(new InternalError("")), + requestFaucetFunds, }; await expect(address.faucet()).rejects.toThrow(InternalError); + expect(requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should return the correct string representation", () => { diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index a9e66298..9be5428e 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -50,17 +50,27 @@ describe("Coinbase tests", () => { expect(user.getId()).toBe(123); expect(user.toString()).toBe("User{ userId: 123 }"); + expect(usersApiMock.getCurrentUser).toHaveBeenCalledWith(); + expect(usersApiMock.getCurrentUser).toHaveBeenCalledTimes(1); }); it("should be able to get faucet funds", async () => { const wallet = await user.createWallet(); expect(wallet.getId()).toBe(VALID_WALLET_MODEL.id); + const payload = { wallet: { network_id: Coinbase.networkList.BaseSepolia } }; + expect(walletsApiMock.createWallet).toHaveBeenCalledWith(payload); + expect(walletsApiMock.createWallet).toHaveBeenCalledTimes(1); const defaultAddress = wallet.defaultAddress(); expect(defaultAddress?.getId()).toBe(VALID_WALLET_MODEL.default_address?.address_id); const faucetTransaction = await wallet?.faucet(); expect(faucetTransaction.getTransactionHash()).toBe("0xdeadbeef"); + expect(addressesApiMock.requestFaucetFunds).toHaveBeenCalledWith( + wallet.getId(), + wallet.defaultAddress()?.getId(), + ); + expect(addressesApiMock.requestFaucetFunds).toHaveBeenCalledTimes(1); }); }); From 59a9e351416d9796784e62ace75a0581cb889a54 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 20 May 2024 14:02:04 -0500 Subject: [PATCH 3/9] Updating Coinbase Class tests --- src/coinbase/tests/coinbase_test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index 9be5428e..f0fdf8a7 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -67,20 +67,21 @@ describe("Coinbase tests", () => { const faucetTransaction = await wallet?.faucet(); expect(faucetTransaction.getTransactionHash()).toBe("0xdeadbeef"); expect(addressesApiMock.requestFaucetFunds).toHaveBeenCalledWith( - wallet.getId(), - wallet.defaultAddress()?.getId(), + defaultAddress.getWalletId(), + defaultAddress?.getId(), ); expect(addressesApiMock.requestFaucetFunds).toHaveBeenCalledTimes(1); }); }); it("should raise an error if the user is not found", async () => { - jest.mock("../../client/api", () => ({ - UsersApiFactory: jest.fn().mockReturnValue({ - me: jest.fn().mockRejectedValue(new APIError("")), - }), - })); + Coinbase.apiClients.user = { + ...usersApiMock, + getCurrentUser: jest.fn().mockRejectedValue(new APIError("User not found")), + }; const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); await expect(cbInstance.getDefaultUser()).rejects.toThrow(APIError); + expect(usersApiMock.getCurrentUser).toHaveBeenCalledWith(); + expect(usersApiMock.getCurrentUser).toHaveBeenCalledTimes(1); }); }); From ce39bc71b769db4100373922f23599c6711f8c0d Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Mon, 20 May 2024 16:13:27 -0500 Subject: [PATCH 4/9] Updating Wallet model usage --- src/coinbase/user.ts | 1 - src/coinbase/wallet.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index 803bbd1d..593dde4a 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -1,5 +1,4 @@ import { User as UserModel } from "./../client/api"; -import { Coinbase } from "./coinbase"; import { Wallet } from "./wallet"; /** diff --git a/src/coinbase/wallet.ts b/src/coinbase/wallet.ts index 23cddb82..07cf38c0 100644 --- a/src/coinbase/wallet.ts +++ b/src/coinbase/wallet.ts @@ -41,6 +41,7 @@ export class Wallet { * Instead, use User.createWallet. * * @constructs Wallet + * @throws {ArgumentError} If the model or client is not provided. * @throws {InternalError} - If address derivation or caching fails. * @throws {APIError} - If the request fails. * @returns A promise that resolves with the new Wallet object. @@ -52,7 +53,7 @@ export class Wallet { }, }); - const wallet = await Wallet.init(walletData.data!); + const wallet = await Wallet.init(walletData.data); await wallet.createAddress(); await wallet.reload(); @@ -80,7 +81,6 @@ export class Wallet { if (!model) { throw new ArgumentError("Wallet model cannot be empty"); } - if (!seed) { seed = bip39.generateMnemonic(); } From a507f491dcc844044a29bdebbb0ce1bc072e9e25 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 21 May 2024 00:19:29 -0500 Subject: [PATCH 5/9] Updating mock usages --- src/coinbase/tests/address_test.ts | 126 ++++++++++++++-------------- src/coinbase/tests/coinbase_test.ts | 60 ++++++++++--- src/coinbase/tests/utils.ts | 32 +++++-- src/coinbase/tests/wallet_test.ts | 100 +++++++++++----------- 4 files changed, 186 insertions(+), 132 deletions(-) diff --git a/src/coinbase/tests/address_test.ts b/src/coinbase/tests/address_test.ts index 5fdbb907..9a44296b 100644 --- a/src/coinbase/tests/address_test.ts +++ b/src/coinbase/tests/address_test.ts @@ -8,19 +8,37 @@ import { InternalError } from "../errors"; import { VALID_ADDRESS_BALANCE_LIST, VALID_ADDRESS_MODEL, - VALID_BALANCE_MODEL, addressesApiMock, + generateRandomHash, + mockFn, + mockReturnRejectedValue, } from "./utils"; // Test suite for Address class describe("Address", () => { - const transactionHash = "0xdeadbeef"; - let address: Address; - const getAddressBalance = jest.fn().mockResolvedValue({ data: VALID_BALANCE_MODEL }); - const listAddressBalances = jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }); - const requestFaucetFunds = jest - .fn() - .mockResolvedValue({ data: { transaction_hash: transactionHash } }); + const transactionHash = generateRandomHash(); + let address: Address, balanceModel; + + beforeAll(() => { + Coinbase.apiClients.address = addressesApiMock; + Coinbase.apiClients.address!.getAddressBalance = mockFn(request => { + const { asset_id } = request; + balanceModel = { + amount: "1000000000000000000", + asset: { + asset_id, + network_id: Coinbase.networkList.BaseSepolia, + }, + }; + return { data: balanceModel }; + }); + Coinbase.apiClients.address!.listAddressBalances = mockFn(() => { + return { data: VALID_ADDRESS_BALANCE_LIST }; + }); + Coinbase.apiClients.address!.requestFaucetFunds = mockFn(() => { + return { data: { transaction_hash: transactionHash } }; + }); + }); beforeEach(() => { address = new Address(VALID_ADDRESS_MODEL); @@ -40,67 +58,59 @@ describe("Address", () => { }); it("should return the correct list of balances", async () => { - Coinbase.apiClients.address = { - ...addressesApiMock, - listAddressBalances, - }; const balances = await address.listBalances(); expect(balances.get(Coinbase.assetList.Eth)).toEqual(new Decimal(1)); expect(balances.get("usdc")).toEqual(new Decimal(5000)); expect(balances.get("weth")).toEqual(new Decimal(3)); - expect(listAddressBalances).toHaveBeenCalledWith(address.getWalletId(), address.getId()); - expect(listAddressBalances).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.listAddressBalances).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + ); + expect(Coinbase.apiClients.address!.listAddressBalances).toHaveBeenCalledTimes(1); }); it("should return the correct ETH balance", async () => { - Coinbase.apiClients.address = { - ...addressesApiMock, - getAddressBalance, - }; const ethBalance = await address.getBalance(Coinbase.assetList.Eth); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1)); - expect(getAddressBalance).toHaveBeenCalledWith( + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledWith( address.getWalletId(), address.getId(), Coinbase.assetList.Eth, ); - expect(getAddressBalance).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return the correct Gwei balance", async () => { const assetId = "gwei"; - Coinbase.apiClients.address = { - ...addressesApiMock, - getAddressBalance, - }; const ethBalance = await address.getBalance(assetId); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000)); - expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); - expect(getAddressBalance).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + assetId, + ); + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return the correct Wei balance", async () => { const assetId = "wei"; - Coinbase.apiClients.address = { - ...addressesApiMock, - getAddressBalance, - }; const ethBalance = await address.getBalance(assetId); expect(ethBalance).toBeInstanceOf(Decimal); expect(ethBalance).toEqual(new Decimal(1000000000000000000)); - expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); - expect(getAddressBalance).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + assetId, + ); + expect(Coinbase.apiClients.address!.getAddressBalance).toHaveBeenCalledTimes(1); }); it("should return an error for an unsupported asset", async () => { const getAddressBalance = jest.fn().mockRejectedValue(new APIError("")); const assetId = "unsupported-asset"; - Coinbase.apiClients.address = { - ...addressesApiMock, - getAddressBalance, - }; + Coinbase.apiClients.address!.getAddressBalance = getAddressBalance; await expect(address.getBalance(assetId)).rejects.toThrow(APIError); expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); expect(getAddressBalance).toHaveBeenCalledTimes(1); @@ -115,46 +125,40 @@ describe("Address", () => { }); it("should request funds from the faucet and returns the faucet transaction", async () => { - Coinbase.apiClients.address = { - ...addressesApiMock, - requestFaucetFunds, - }; const faucetTransaction = await address.faucet(); expect(faucetTransaction).toBeInstanceOf(FaucetTransaction); expect(faucetTransaction.getTransactionHash()).toBe(transactionHash); - expect(requestFaucetFunds).toHaveBeenCalledWith(address.getWalletId(), address.getId()); - expect(requestFaucetFunds).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + ); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw an APIError when the request is unsuccesful", async () => { - const requestFaucetFunds = jest.fn().mockRejectedValue(new APIError("")); - Coinbase.apiClients.address = { - ...addressesApiMock, - requestFaucetFunds, - }; + Coinbase.apiClients.address!.requestFaucetFunds = mockReturnRejectedValue(new APIError("")); await expect(address.faucet()).rejects.toThrow(APIError); - expect(requestFaucetFunds).toHaveBeenCalledWith(address.getWalletId(), address.getId()); - expect(requestFaucetFunds).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledWith( + address.getWalletId(), + address.getId(), + ); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw a FaucetLimitReachedError when the faucet limit is reached", async () => { - const requestFaucetFunds = jest.fn().mockRejectedValue(new FaucetLimitReachedError("")); - Coinbase.apiClients.address = { - ...addressesApiMock, - requestFaucetFunds, - }; + Coinbase.apiClients.address!.requestFaucetFunds = mockReturnRejectedValue( + new FaucetLimitReachedError(""), + ); await expect(address.faucet()).rejects.toThrow(FaucetLimitReachedError); - expect(requestFaucetFunds).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should throw an InternalError when the request fails unexpectedly", async () => { - const requestFaucetFunds = jest.fn().mockRejectedValue(new InternalError("")); - Coinbase.apiClients.address = { - ...addressesApiMock, - requestFaucetFunds, - }; + Coinbase.apiClients.address!.requestFaucetFunds = mockReturnRejectedValue( + new InternalError(""), + ); await expect(address.faucet()).rejects.toThrow(InternalError); - expect(requestFaucetFunds).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.requestFaucetFunds).toHaveBeenCalledTimes(1); }); it("should return the correct string representation", () => { diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index f0fdf8a7..e5528fec 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -1,6 +1,15 @@ +import { randomUUID } from "crypto"; import { APIError } from "../api_error"; import { Coinbase } from "../coinbase"; -import { VALID_WALLET_MODEL, addressesApiMock, usersApiMock, walletsApiMock } from "./utils"; +import { + VALID_WALLET_MODEL, + addressesApiMock, + generateRandomHash, + mockReturnRejectedValue, + mockReturnValue, + usersApiMock, + walletsApiMock, +} from "./utils"; const PATH_PREFIX = "./src/coinbase/tests/config"; @@ -34,38 +43,67 @@ describe("Coinbase tests", () => { }); describe("should able to interact with the API", () => { - let user; + let user, walletId, publicKey, addressId, transactionHash; const cbInstance = Coinbase.configureFromJson( `${PATH_PREFIX}/coinbase_cloud_api_key.json`, true, ); - it("should return the correct user ID", async () => { + beforeAll(async () => { Coinbase.apiClients = { user: usersApiMock, wallet: walletsApiMock, address: addressesApiMock, }; + + walletId = randomUUID(); + publicKey = generateRandomHash(8); + addressId = randomUUID(); + transactionHash = generateRandomHash(8); + + const walletModel = { + id: walletId, + network_id: Coinbase.networkList.BaseSepolia, + default_address: { + wallet_id: walletId, + address_id: addressId, + public_key: publicKey, + network_id: Coinbase.networkList.BaseSepolia, + }, + }; + + Coinbase.apiClients.user!.getCurrentUser = mockReturnValue({ id: 123 }); + Coinbase.apiClients.wallet!.createWallet = mockReturnValue(walletModel); + Coinbase.apiClients.wallet!.getWallet = mockReturnValue(walletModel); + Coinbase.apiClients.address!.requestFaucetFunds = mockReturnValue({ + transaction_hash: transactionHash, + }); + Coinbase.apiClients.address!.createAddress = mockReturnValue( + VALID_WALLET_MODEL.default_address, + ); + user = await cbInstance.getDefaultUser(); + }); + it("should return the correct user ID", async () => { expect(user.getId()).toBe(123); expect(user.toString()).toBe("User{ userId: 123 }"); - expect(usersApiMock.getCurrentUser).toHaveBeenCalledWith(); + expect(Coinbase.apiClients.user!.getCurrentUser).toHaveBeenCalledWith(); expect(usersApiMock.getCurrentUser).toHaveBeenCalledTimes(1); }); it("should be able to get faucet funds", async () => { const wallet = await user.createWallet(); - expect(wallet.getId()).toBe(VALID_WALLET_MODEL.id); + expect(wallet.getId()).toBe(walletId); const payload = { wallet: { network_id: Coinbase.networkList.BaseSepolia } }; expect(walletsApiMock.createWallet).toHaveBeenCalledWith(payload); expect(walletsApiMock.createWallet).toHaveBeenCalledTimes(1); const defaultAddress = wallet.defaultAddress(); - expect(defaultAddress?.getId()).toBe(VALID_WALLET_MODEL.default_address?.address_id); + expect(defaultAddress?.getId()).toBe(addressId); const faucetTransaction = await wallet?.faucet(); - expect(faucetTransaction.getTransactionHash()).toBe("0xdeadbeef"); + expect(faucetTransaction.getTransactionHash()).toBe(transactionHash); expect(addressesApiMock.requestFaucetFunds).toHaveBeenCalledWith( defaultAddress.getWalletId(), defaultAddress?.getId(), @@ -75,11 +113,11 @@ describe("Coinbase tests", () => { }); it("should raise an error if the user is not found", async () => { - Coinbase.apiClients.user = { - ...usersApiMock, - getCurrentUser: jest.fn().mockRejectedValue(new APIError("User not found")), - }; const cbInstance = Coinbase.configureFromJson(`${PATH_PREFIX}/coinbase_cloud_api_key.json`); + Coinbase.apiClients.user!.getCurrentUser = mockReturnRejectedValue( + new APIError("User not found"), + ); + await expect(cbInstance.getDefaultUser()).rejects.toThrow(APIError); expect(usersApiMock.getCurrentUser).toHaveBeenCalledWith(); expect(usersApiMock.getCurrentUser).toHaveBeenCalledTimes(1); diff --git a/src/coinbase/tests/utils.ts b/src/coinbase/tests/utils.ts index d946ce40..1a2ed136 100644 --- a/src/coinbase/tests/utils.ts +++ b/src/coinbase/tests/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosInstance } from "axios"; import { ethers } from "ethers"; import { randomUUID } from "crypto"; @@ -12,8 +13,21 @@ import { BASE_PATH } from "../../client/base"; import { Coinbase } from "../coinbase"; import { registerAxiosInterceptors } from "../utils"; +export const mockFn = (...args) => jest.fn(...args) as any; +export const mockReturnValue = data => jest.fn().mockResolvedValue({ data }); +export const mockReturnRejectedValue = data => jest.fn().mockRejectedValue(data); + export const walletId = randomUUID(); +export const generateRandomHash = (length = 8) => { + const characters = "abcdef0123456789"; + let hash = "0x"; + for (let i = 0; i < length; i++) { + hash += characters[Math.floor(Math.random() * characters.length)]; + } + return hash; +}; + // newAddressModel creates a new AddressModel with a random wallet ID and a random Ethereum address. export const newAddressModel = (walletId: string): AddressModel => { const ethAddress = ethers.Wallet.createRandom(); @@ -87,7 +101,7 @@ type AxiosMockType = [AxiosInstance, Configuration, string]; /** * Returns an Axios instance with interceptors and configuration for testing. * - * @returns {AxiosMockType} - The Axios instance, configuration, and base path. + * @returns The Axios instance, configuration, and base path. */ export const createAxiosMock = (): AxiosMockType => { const axiosInstance = axios.create(); @@ -101,18 +115,18 @@ export const createAxiosMock = (): AxiosMockType => { }; export const usersApiMock = { - getCurrentUser: jest.fn().mockResolvedValue({ data: { id: 123 } }), + getCurrentUser: jest.fn(), }; export const walletsApiMock = { - getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), - createWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), + getWallet: jest.fn(), + createWallet: jest.fn(), }; export const addressesApiMock = { - requestFaucetFunds: jest.fn().mockResolvedValue({ data: { transaction_hash: "0xdeadbeef" } }), - getAddress: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), - getAddressBalance: jest.fn().mockResolvedValue({ data: { VALID_BALANCE_MODEL } }), - listAddressBalances: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), - createAddress: jest.fn().mockResolvedValue({ data: VALID_WALLET_MODEL.default_address }), + requestFaucetFunds: jest.fn(), + getAddress: jest.fn(), + getAddressBalance: jest.fn(), + listAddressBalances: jest.fn(), + createAddress: jest.fn(), }; diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index 94507e42..0e517589 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -1,54 +1,45 @@ import { randomUUID } from "crypto"; import { Coinbase } from "../coinbase"; -import { ArgumentError } from "../errors"; import { Wallet } from "../wallet"; -import { addressesApiMock, newAddressModel, walletsApiMock } from "./utils"; - -const walletId = randomUUID(); - -const DEFAULT_ADDRESS_MODEL = newAddressModel(walletId); - -export const VALID_WALLET_MODEL = { - id: walletId, - network_id: Coinbase.networkList.BaseSepolia, - default_address: DEFAULT_ADDRESS_MODEL, -}; - -export const VALID_WALLET_MODEL_WITHOUT_DEFAULT_ADDRESS = { - id: walletId, - network_id: Coinbase.networkList.BaseSepolia, -}; +import { addressesApiMock, mockFn, newAddressModel, walletsApiMock } from "./utils"; +import { ArgumentError } from "../errors"; describe("Wallet Class", () => { - let wallet; - - describe(".create", () => { - beforeEach(async () => { - const createWallet = jest - .fn() - .mockResolvedValue({ data: VALID_WALLET_MODEL_WITHOUT_DEFAULT_ADDRESS }); - const getWallet = jest.fn().mockResolvedValue({ data: VALID_WALLET_MODEL }); - - Coinbase.apiClients.wallet = { - ...walletsApiMock, - createWallet, - getWallet, - }; - - Coinbase.apiClients.address = { - ...addressesApiMock, - createAddress: jest.fn().mockResolvedValue({ data: DEFAULT_ADDRESS_MODEL }), - }; - - wallet = await Wallet.create(); + let wallet, walletModel, walletId; + + beforeAll(async () => { + walletId = randomUUID(); + // Mock the API calls + Coinbase.apiClients.wallet = walletsApiMock; + Coinbase.apiClients.address = addressesApiMock; + Coinbase.apiClients.wallet!.createWallet = mockFn(request => { + const { network_id } = request.wallet; + walletModel = { id: walletId, network_id, default_address: newAddressModel(walletId) }; + return { data: walletModel }; + }); + Coinbase.apiClients.wallet!.getWallet = mockFn(() => { + return { data: walletModel }; }); + Coinbase.apiClients.address!.createAddress = mockFn(() => { + return { data: walletModel.default_address }; + }); + wallet = await Wallet.create(); + }); + describe(".create", () => { it("should return a Wallet instance", async () => { expect(wallet).toBeInstanceOf(Wallet); + expect(Coinbase.apiClients.wallet!.createWallet).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.wallet!.getWallet).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.address!.createAddress).toHaveBeenCalledTimes(1); + expect(Coinbase.apiClients.wallet!.createWallet).toHaveBeenCalledWith({ + wallet: { network_id: Coinbase.networkList.BaseSepolia }, + }); + expect(Coinbase.apiClients.wallet!.getWallet).toHaveBeenCalledWith(walletId); }); it("should return the correct wallet ID", async () => { - expect(wallet.getId()).toBe(VALID_WALLET_MODEL.id); + expect(wallet.getId()).toBe(walletModel.id); }); it("should return the correct network ID", async () => { @@ -56,7 +47,7 @@ describe("Wallet Class", () => { }); it("should return the correct default address", async () => { - expect(wallet.defaultAddress()?.getId()).toBe(DEFAULT_ADDRESS_MODEL.address_id); + expect(wallet.defaultAddress()?.getId()).toBe(walletModel.default_address.address_id); }); }); @@ -81,14 +72,21 @@ describe("Wallet Class", () => { beforeEach(async () => { jest.clearAllMocks(); const getAddress = jest.fn(); - addressList.forEach(address => getAddress.mockResolvedValue({ data: address })); - Coinbase.apiClients.address = { - ...addressesApiMock, - getAddress, - }; - - wallet = await Wallet.init(VALID_WALLET_MODEL, existingSeed, 2); - expect(Coinbase.apiClients.address.getAddress).toHaveBeenCalledTimes(2); + addressList.forEach(() => { + getAddress.mockImplementationOnce((wallet_id, address_id) => { + return Promise.resolve({ + data: { + address_id, + network_id: Coinbase.networkList.BaseSepolia, + public_key: "0x03c3379b488a32a432a4dfe91cc3a28be210eddc98b2005bb59a4cf4ed0646eb56", + wallet_id, + }, + }); + }); + }); + Coinbase.apiClients.address!.getAddress = getAddress; + wallet = await Wallet.init(walletModel, existingSeed, 2); + expect(Coinbase.apiClients.address!.getAddress).toHaveBeenCalledTimes(2); addressList.forEach((address, callIndex) => { expect(Coinbase.apiClients.address!.getAddress).toHaveBeenNthCalledWith( callIndex + 1, @@ -103,7 +101,7 @@ describe("Wallet Class", () => { }); it("should return the correct wallet ID", async () => { - expect(wallet.getId()).toBe(VALID_WALLET_MODEL.id); + expect(wallet.getId()).toBe(walletModel.id); }); it("should return the correct network ID", async () => { @@ -111,7 +109,7 @@ describe("Wallet Class", () => { }); it("should return the correct default address", async () => { - expect(wallet.defaultAddress()?.getId()).toBe(VALID_WALLET_MODEL.default_address?.address_id); + expect(wallet.defaultAddress()?.getId()).toBe(walletModel.default_address?.address_id); }); it("should derive the correct number of addresses", async () => { @@ -120,7 +118,7 @@ describe("Wallet Class", () => { it("should return the correct string representation", async () => { expect(wallet.toString()).toBe( - `Wallet{id: '${VALID_WALLET_MODEL.id}', networkId: '${Coinbase.networkList.BaseSepolia}'}`, + `Wallet{id: '${walletModel.id}', networkId: '${Coinbase.networkList.BaseSepolia}'}`, ); }); From 062196d11a24aa0d1b64e8f9eb326c49e19970f5 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 21 May 2024 10:15:49 -0500 Subject: [PATCH 6/9] updating public key --- src/coinbase/tests/coinbase_test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coinbase/tests/coinbase_test.ts b/src/coinbase/tests/coinbase_test.ts index e5528fec..a3f03ae0 100644 --- a/src/coinbase/tests/coinbase_test.ts +++ b/src/coinbase/tests/coinbase_test.ts @@ -10,6 +10,7 @@ import { usersApiMock, walletsApiMock, } from "./utils"; +import { ethers } from "ethers"; const PATH_PREFIX = "./src/coinbase/tests/config"; @@ -55,9 +56,10 @@ describe("Coinbase tests", () => { wallet: walletsApiMock, address: addressesApiMock, }; + const ethAddress = ethers.Wallet.createRandom(); walletId = randomUUID(); - publicKey = generateRandomHash(8); + publicKey = ethAddress.publicKey; addressId = randomUUID(); transactionHash = generateRandomHash(8); From 1f97afff054bdac3a7e4d609a494e9a421398a48 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 21 May 2024 10:29:15 -0500 Subject: [PATCH 7/9] updating createWallet and getWallet mocks --- src/coinbase/tests/wallet_test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index 0e517589..d8bdb031 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -6,6 +6,7 @@ import { ArgumentError } from "../errors"; describe("Wallet Class", () => { let wallet, walletModel, walletId; + const apiResponses = {}; beforeAll(async () => { walletId = randomUUID(); @@ -14,14 +15,19 @@ describe("Wallet Class", () => { Coinbase.apiClients.address = addressesApiMock; Coinbase.apiClients.wallet!.createWallet = mockFn(request => { const { network_id } = request.wallet; - walletModel = { id: walletId, network_id, default_address: newAddressModel(walletId) }; - return { data: walletModel }; + apiResponses[walletId] = { + id: walletId, + network_id, + default_address: newAddressModel(walletId), + }; + return { data: apiResponses[walletId] }; }); - Coinbase.apiClients.wallet!.getWallet = mockFn(() => { - return { data: walletModel }; + Coinbase.apiClients.wallet!.getWallet = mockFn(walletId => { + walletModel = apiResponses[walletId]; + return { data: apiResponses[walletId] }; }); Coinbase.apiClients.address!.createAddress = mockFn(() => { - return { data: walletModel.default_address }; + return { data: apiResponses[walletId].default_address }; }); wallet = await Wallet.create(); }); From e07612ee2059fb62434c09462513ad53968f96b4 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 21 May 2024 11:22:12 -0500 Subject: [PATCH 8/9] Adding missing walletId param --- src/coinbase/tests/wallet_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index d8bdb031..e5242e2c 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -26,7 +26,7 @@ describe("Wallet Class", () => { walletModel = apiResponses[walletId]; return { data: apiResponses[walletId] }; }); - Coinbase.apiClients.address!.createAddress = mockFn(() => { + Coinbase.apiClients.address!.createAddress = mockFn(walletId => { return { data: apiResponses[walletId].default_address }; }); wallet = await Wallet.create(); From 8933f368c3a91eb39d61ced1eda2692db041a0c3 Mon Sep 17 00:00:00 2001 From: Erdi Maden Date: Tue, 21 May 2024 11:44:12 -0500 Subject: [PATCH 9/9] Updating utils usage in address test and moving beforeAll under .create section --- src/coinbase/tests/address_test.ts | 2 +- src/coinbase/tests/wallet_test.ts | 48 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/coinbase/tests/address_test.ts b/src/coinbase/tests/address_test.ts index 9a44296b..7e15b9ab 100644 --- a/src/coinbase/tests/address_test.ts +++ b/src/coinbase/tests/address_test.ts @@ -108,7 +108,7 @@ describe("Address", () => { }); it("should return an error for an unsupported asset", async () => { - const getAddressBalance = jest.fn().mockRejectedValue(new APIError("")); + const getAddressBalance = mockReturnRejectedValue(new APIError("")); const assetId = "unsupported-asset"; Coinbase.apiClients.address!.getAddressBalance = getAddressBalance; await expect(address.getBalance(assetId)).rejects.toThrow(APIError); diff --git a/src/coinbase/tests/wallet_test.ts b/src/coinbase/tests/wallet_test.ts index e5242e2c..d36dd1de 100644 --- a/src/coinbase/tests/wallet_test.ts +++ b/src/coinbase/tests/wallet_test.ts @@ -6,33 +6,33 @@ import { ArgumentError } from "../errors"; describe("Wallet Class", () => { let wallet, walletModel, walletId; - const apiResponses = {}; + describe(".create", () => { + const apiResponses = {}; - beforeAll(async () => { - walletId = randomUUID(); - // Mock the API calls - Coinbase.apiClients.wallet = walletsApiMock; - Coinbase.apiClients.address = addressesApiMock; - Coinbase.apiClients.wallet!.createWallet = mockFn(request => { - const { network_id } = request.wallet; - apiResponses[walletId] = { - id: walletId, - network_id, - default_address: newAddressModel(walletId), - }; - return { data: apiResponses[walletId] }; - }); - Coinbase.apiClients.wallet!.getWallet = mockFn(walletId => { - walletModel = apiResponses[walletId]; - return { data: apiResponses[walletId] }; - }); - Coinbase.apiClients.address!.createAddress = mockFn(walletId => { - return { data: apiResponses[walletId].default_address }; + beforeAll(async () => { + walletId = randomUUID(); + // Mock the API calls + Coinbase.apiClients.wallet = walletsApiMock; + Coinbase.apiClients.address = addressesApiMock; + Coinbase.apiClients.wallet!.createWallet = mockFn(request => { + const { network_id } = request.wallet; + apiResponses[walletId] = { + id: walletId, + network_id, + default_address: newAddressModel(walletId), + }; + return { data: apiResponses[walletId] }; + }); + Coinbase.apiClients.wallet!.getWallet = mockFn(walletId => { + walletModel = apiResponses[walletId]; + return { data: apiResponses[walletId] }; + }); + Coinbase.apiClients.address!.createAddress = mockFn(walletId => { + return { data: apiResponses[walletId].default_address }; + }); + wallet = await Wallet.create(); }); - wallet = await Wallet.create(); - }); - describe(".create", () => { it("should return a Wallet instance", async () => { expect(wallet).toBeInstanceOf(Wallet); expect(Coinbase.apiClients.wallet!.createWallet).toHaveBeenCalledTimes(1);