From fb0705a03d8f58fcb2bfd81f9793a367b6000e12 Mon Sep 17 00:00:00 2001 From: lohit-40 Date: Sun, 12 Apr 2026 11:43:41 +0530 Subject: [PATCH 1/2] test: add comprehensive unit tests for stakeF.ts and contract.ts with deterministic RPC mocking --- .env.example | 14 ++- package-lock.json | 11 -- tests/unit/lib/contract.test.ts | 202 ++++++++++++++++++++++++++++++++ tests/unit/lib/stakeF.test.ts | 187 +++++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 16 deletions(-) create mode 100644 tests/unit/lib/contract.test.ts create mode 100644 tests/unit/lib/stakeF.test.ts diff --git a/.env.example b/.env.example index 428523b3..677cd0f1 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,20 @@ # Stellar Network Configuration # ============================================ -# Testnet Configuration (default) +# The network to operate on: 'testnet' (default) or 'mainnet' +STELLAR_NETWORK=testnet + +# Testnet Configuration STELLAR_PUBLIC_KEY=GTEST... STELLAR_PRIVATE_KEY=STEST... -SRB_PROVIDER_URL=https://soroban-testnet.stellar.org +SOROBAN_RPC_URL=https://soroban-testnet.stellar.org +# Legacy alias if needed: SRB_PROVIDER_URL=https://soroban-testnet.stellar.org -# Mainnet Configuration -# Uncomment these when using mainnet +# Mainnet Configuration (Uncomment and replace to use mainnet) +# STELLAR_NETWORK=mainnet # STELLAR_PUBLIC_KEY=GMAIN... # STELLAR_PRIVATE_KEY=SMAIN... -# SRB_PROVIDER_URL=https://soroban.stellar.org +# SOROBAN_RPC_URL=https://soroban-mainnet.stellar.org # ============================================ # Bridge Mainnet Safeguard diff --git a/package-lock.json b/package-lock.json index 184ce2b5..57e6b475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5459,17 +5459,6 @@ } } }, - "node_modules/web3-eth-abi/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/web3-eth-accounts": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-4.3.1.tgz", diff --git a/tests/unit/lib/contract.test.ts b/tests/unit/lib/contract.test.ts new file mode 100644 index 00000000..d8bbd678 --- /dev/null +++ b/tests/unit/lib/contract.test.ts @@ -0,0 +1,202 @@ +/** + * Unit tests for lib/contract.ts (Soroban Liquidity Pool interactions) + * + * All Soroban RPC calls are mocked via vi.mock so tests are fully deterministic + * and never touch a live network. Pattern mirrors lib/dex.test.ts. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Keypair } from "@stellar/stellar-sdk"; + +// ── Stable test addresses ────────────────────────────────────────────────── +const CALLER = Keypair.random().publicKey(); +const RECIPIENT = Keypair.random().publicKey(); + +// ── The shared mock server object — methods are set in beforeEach ────────── +const mockServer = { + getAccount: vi.fn(), + simulateTransaction: vi.fn(), + prepareTransaction: vi.fn(), + sendTransaction: vi.fn(), + getTransaction: vi.fn(), +}; + +// vi.mock is hoisted before imports, so we declare the mock before importing the module. +vi.mock("@stellar/stellar-sdk", async (importOriginal) => { + const actual = await importOriginal(); + class MockServer { + constructor() { + return mockServer; + } + } + return { + ...actual, + rpc: { + ...actual.rpc, + Server: MockServer, + }, + TransactionBuilder: { + ...actual.TransactionBuilder, + // Prevent real XDR parsing of our mock string + fromXDR: vi.fn().mockReturnValue({ toXDR: () => "mock-tx-xdr" }), + }, + }; +}); + +vi.mock("../../../lib/stellar", () => ({ + signTransaction: vi.fn().mockReturnValue("mock-signed-xdr"), +})); + +vi.mock("../../../utils/buildTransaction", () => ({ + buildTransaction: vi.fn().mockReturnValue({ + toXDR: () => "unsigned-mock-xdr", + }), +})); + +import { + getShareId, + deposit, + swap, + withdraw, + getReserves, +} from "../../../lib/contract"; + +// ── Reset methods to safe defaults before each test ──────────────────────── +beforeEach(() => { + vi.clearAllMocks(); + mockServer.getAccount.mockResolvedValue({ id: CALLER, sequence: "1" }); + mockServer.simulateTransaction.mockResolvedValue({ + minResourceFee: "100", + transactionData: {}, + }); + // prepareTransaction must return a Promise so .catch() works in lib/contract.ts + mockServer.prepareTransaction.mockResolvedValue({ + toXDR: () => "mock-prepared-xdr", + }); + mockServer.sendTransaction.mockResolvedValue({ hash: "mock-tx-hash-abc" }); + mockServer.getTransaction.mockResolvedValue({ status: "SUCCESS" }); +}); + +// ── Address validation ───────────────────────────────────────────────────── +describe("contract — address validation", () => { + it("throws for an invalid deposit recipient address", async () => { + await expect( + deposit(CALLER, "0xNOT_STELLAR", "1000", "900", "500", "400") + ).rejects.toThrow("Invalid address format"); + }); + + it("throws for an invalid swap recipient address", async () => { + await expect( + swap(CALLER, "bad-address", true, "100", "110") + ).rejects.toThrow("Invalid address format"); + }); + + it("throws for an invalid withdraw recipient address", async () => { + await expect( + withdraw(CALLER, "ZZZZ", "200", "100", "100") + ).rejects.toThrow("Invalid address format"); + }); +}); + +// ── getShareId ───────────────────────────────────────────────────────────── +describe("contract — getShareId", () => { + it("returns the share ID string from a read-only simulation result", async () => { + const { nativeToScVal } = await import("@stellar/stellar-sdk"); + const fakeShareId = "CSHARE00PLACEHOLDER000000000000000000000000000000000000000"; + const encoded = nativeToScVal(fakeShareId, { type: "string" }).toXDR("base64"); + + mockServer.simulateTransaction.mockResolvedValue({ + results: [{ xdr: encoded }], + }); + + const result = await getShareId(CALLER); + expect(result).toBe(fakeShareId); + }); + + it("throws when simulation returns an error field", async () => { + mockServer.simulateTransaction.mockResolvedValue({ + error: "Contract not found", + }); + await expect(getShareId(CALLER)).rejects.toThrow("Simulation failed"); + }); + + it("throws when simulation results have no xdr field", async () => { + mockServer.simulateTransaction.mockResolvedValue({ + results: [{}], + }); + await expect(getShareId(CALLER)).rejects.toThrow( + "No return value in simulation results" + ); + }); +}); + +// ── getReserves ──────────────────────────────────────────────────────────── +describe("contract — getReserves", () => { + it("returns null on SUCCESS when no returnValue is provided", async () => { + mockServer.getTransaction.mockResolvedValue({ status: "SUCCESS" }); + const result = await getReserves(CALLER); + expect(result).toBeNull(); + }); +}); + +// ── deposit ──────────────────────────────────────────────────────────────── +describe("contract — deposit", () => { + it("completes without error for valid inputs", async () => { + await expect( + deposit(CALLER, RECIPIENT, "1000", "900", "500", "450") + ).resolves.not.toThrow(); + }); + + it("throws when the transaction submission fails", async () => { + mockServer.sendTransaction.mockRejectedValue(new Error("Insufficient balance")); + await expect( + deposit(CALLER, RECIPIENT, "1000", "900", "500", "450") + ).rejects.toThrow(); + }); + + it("throws when getAccount fails (account does not exist)", async () => { + mockServer.getAccount.mockRejectedValue(new Error("Account not found")); + await expect( + deposit(CALLER, RECIPIENT, "1000", "900", "500", "450") + ).rejects.toThrow("Failed to fetch account"); + }); +}); + +// ── swap ────────────────────────────────────────────────────────────────── +describe("contract — swap", () => { + it("completes without error for a buyA=true swap", async () => { + await expect( + swap(CALLER, RECIPIENT, true, "100", "110") + ).resolves.not.toThrow(); + }); + + it("completes without error for a buyA=false swap", async () => { + await expect( + swap(CALLER, RECIPIENT, false, "200", "220") + ).resolves.not.toThrow(); + }); + + it("throws when simulation explicitly returns an error", async () => { + mockServer.simulateTransaction.mockResolvedValue({ + error: "Slippage exceeded", + }); + await expect( + swap(CALLER, RECIPIENT, true, "100", "110") + ).rejects.toThrow("Simulation failed: Slippage exceeded"); + }); +}); + +// ── withdraw ────────────────────────────────────────────────────────────── +describe("contract — withdraw", () => { + it("completes without error for valid inputs", async () => { + await expect( + withdraw(CALLER, RECIPIENT, "200", "90", "90") + ).resolves.not.toThrow(); + }); + + it("throws when the transaction status is FAILED", async () => { + mockServer.getTransaction.mockResolvedValue({ status: "FAILED" }); + await expect( + withdraw(CALLER, RECIPIENT, "200", "90", "90") + ).rejects.toThrow("Transaction failed with status"); + }); +}); diff --git a/tests/unit/lib/stakeF.test.ts b/tests/unit/lib/stakeF.test.ts new file mode 100644 index 00000000..4c58d1fb --- /dev/null +++ b/tests/unit/lib/stakeF.test.ts @@ -0,0 +1,187 @@ +/** + * Unit tests for lib/stakeF.ts + * + * All Soroban RPC calls are mocked so tests are fully deterministic + * and never touch a live network. Pattern mirrors lib/dex.test.ts. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Keypair } from "@stellar/stellar-sdk"; + +// ── Stable test addresses ────────────────────────────────────────────────── +const CALLER = Keypair.random().publicKey(); +const USER_ADDRESS = Keypair.random().publicKey(); +const TOKEN_ADDRESS = Keypair.random().publicKey(); +const CONTRACT_ADDRESS = Keypair.random().publicKey(); + +// ── Mock the Soroban RPC server and signing helpers ─────────────────────── +const mockServer = { + getAccount: vi.fn(), + prepareTransaction: vi.fn(), + sendTransaction: vi.fn(), + getTransaction: vi.fn(), +}; + +vi.mock("@stellar/stellar-sdk", async (importOriginal) => { + const actual = await importOriginal(); + class MockServer { + constructor() { + return mockServer; + } + } + return { + ...actual, + rpc: { + ...actual.rpc, + Server: MockServer, + }, + TransactionBuilder: { + ...actual.TransactionBuilder, + fromXDR: vi.fn().mockReturnValue({ toXDR: () => "mock-tx-xdr" }), + }, + }; +}); + +vi.mock("../../../lib/stellar", () => ({ + signTransaction: vi.fn().mockReturnValue("signed-xdr-string"), +})); + +vi.mock("../../../utils/buildTransaction", () => ({ + buildTransaction: vi.fn().mockReturnValue({ + toXDR: () => "unsigned-xdr", + }), +})); + +import { initialize, stake, unstake, claimRewards, getStake } from "../../../lib/stakeF"; + +// ── Reset server methods before each test ───────────────────────────────── +beforeEach(() => { + vi.clearAllMocks(); + mockServer.getAccount.mockResolvedValue({ id: CALLER, sequence: "1" }); + // Must return a Promise so .catch() works in stakeF.ts line 56 + mockServer.prepareTransaction.mockResolvedValue({ + toXDR: () => "mock-xdr-string", + }); + mockServer.sendTransaction.mockResolvedValue({ hash: "mock-tx-hash-abc123" }); + mockServer.getTransaction.mockResolvedValue({ status: "SUCCESS" }); +}); + +// ── Helpers ──────────────────────────────────────────────────────────────── +const testnetConfig = { + network: "testnet" as const, + rpcUrl: "https://soroban-testnet.stellar.org", + contractAddress: CONTRACT_ADDRESS, +}; + +const mainnetConfig = { + network: "mainnet" as const, + rpcUrl: "https://soroban-mainnet.stellar.org", + contractAddress: CONTRACT_ADDRESS, +}; + +// ── Tests ────────────────────────────────────────────────────────────────── +describe("stakeF — addressToScVal validation", () => { + it("rejects addresses that are not valid Stellar public keys", async () => { + const result = await stake("INVALID_ADDRESS", 100, testnetConfig); + expect(typeof result).toBe("string"); + expect(result).toMatch(/Invalid address format/i); + }); + + it("rejects token addresses with wrong format in initialize", async () => { + const result = await initialize(CALLER, "bad-token-address", 10, testnetConfig); + expect(typeof result).toBe("string"); + expect(result).toMatch(/Invalid address format/i); + }); +}); + +describe("stakeF — initialize", () => { + it("returns a success message on valid testnet initialization", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 100, testnetConfig); + expect(result).toBe("Contract initialized successfully"); + }); + + it("returns a success message on valid mainnet initialization with explicit contract", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 50, mainnetConfig); + expect(result).toBe("Contract initialized successfully"); + }); + + it("accepts zero as a reward rate (edge case)", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 0, testnetConfig); + expect(result).toBe("Contract initialized successfully"); + }); +}); + +describe("stakeF — stake", () => { + it("returns a confirmation message with the staked amount", async () => { + const result = await stake(CALLER, 1000, testnetConfig); + expect(result).toBe("Staked 1000 successfully"); + }); + + it("handles large stake amounts correctly", async () => { + const result = await stake(CALLER, 999_999_999, testnetConfig); + expect(result).toBe("Staked 999999999 successfully"); + }); + + it("propagates mainnet config without falling back to testnet contract", async () => { + const result = await stake(CALLER, 500, mainnetConfig); + expect(result).toBe("Staked 500 successfully"); + }); +}); + +describe("stakeF — unstake", () => { + it("returns a confirmation message with the unstaked amount", async () => { + const result = await unstake(CALLER, 200, testnetConfig); + expect(result).toBe("Unstaked 200 successfully"); + }); + + it("handles partial unstake (amount less than full stake)", async () => { + const result = await unstake(CALLER, 1, testnetConfig); + expect(result).toBe("Unstaked 1 successfully"); + }); +}); + +describe("stakeF — claimRewards", () => { + it("returns success message on successful claim", async () => { + const result = await claimRewards(CALLER, testnetConfig); + expect(result).toBe("Rewards claimed successfully"); + }); + + it("works on mainnet config without error", async () => { + const result = await claimRewards(CALLER, mainnetConfig); + expect(result).toBe("Rewards claimed successfully"); + }); +}); + +describe("stakeF — getStake", () => { + it("returns a stake info string for the queried user", async () => { + const result = await getStake(CALLER, USER_ADDRESS, testnetConfig); + expect(typeof result).toBe("string"); + expect(result).toContain(USER_ADDRESS); + }); + + it("rejects invalid user addresses early", async () => { + const result = await getStake(CALLER, "0xBADADDRESS", testnetConfig); + expect(typeof result).toBe("string"); + expect(result).toMatch(/Invalid address format/i); + }); +}); + +describe("stakeF — mainnet safeguard", () => { + it("succeeds when mainnet config includes a valid contractAddress", async () => { + const result = await stake(CALLER, 100, mainnetConfig); + expect(result).toBe("Staked 100 successfully"); + }); +}); + +describe("stakeF — network switching (RPC URL propagation)", () => { + it("uses testnet RPC URL config for testnet operations", async () => { + // Verify testnet config succeeds end-to-end (no throws) + const result = await stake(CALLER, 10, testnetConfig); + expect(result).toBe("Staked 10 successfully"); + }); + + it("uses mainnet RPC URL config for mainnet operations", async () => { + // Verify mainnet config with a contract address succeeds end-to-end + const result = await stake(CALLER, 10, mainnetConfig); + expect(result).toBe("Staked 10 successfully"); + }); +}); From 810d0acecdd3227d315cacc66d11d9ad7ddfa848 Mon Sep 17 00:00:00 2001 From: lohit-40 Date: Mon, 13 Apr 2026 00:01:43 +0530 Subject: [PATCH 2/2] fix: align stakeF.test.ts signatures with lib/stakeF.ts and fix missing TargetChain import in agent.ts --- agent.ts | 2 +- tests/unit/lib/stakeF.test.ts | 101 +++++++++++++++++----------------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/agent.ts b/agent.ts index 82ec5daa..14145806 100644 --- a/agent.ts +++ b/agent.ts @@ -14,7 +14,7 @@ import { type SwapBestRouteParams, type SwapBestRouteResult, } from "./lib/dex"; -import { bridgeTokenTool } from "./tools/bridge"; +import { bridgeTokenTool, type TargetChain } from "./tools/bridge"; import { Horizon, Keypair, diff --git a/tests/unit/lib/stakeF.test.ts b/tests/unit/lib/stakeF.test.ts index 4c58d1fb..26570e5c 100644 --- a/tests/unit/lib/stakeF.test.ts +++ b/tests/unit/lib/stakeF.test.ts @@ -11,7 +11,6 @@ import { Keypair } from "@stellar/stellar-sdk"; const CALLER = Keypair.random().publicKey(); const USER_ADDRESS = Keypair.random().publicKey(); const TOKEN_ADDRESS = Keypair.random().publicKey(); -const CONTRACT_ADDRESS = Keypair.random().publicKey(); // ── Mock the Soroban RPC server and signing helpers ─────────────────────── const mockServer = { @@ -57,7 +56,6 @@ import { initialize, stake, unstake, claimRewards, getStake } from "../../../lib beforeEach(() => { vi.clearAllMocks(); mockServer.getAccount.mockResolvedValue({ id: CALLER, sequence: "1" }); - // Must return a Promise so .catch() works in stakeF.ts line 56 mockServer.prepareTransaction.mockResolvedValue({ toXDR: () => "mock-xdr-string", }); @@ -65,123 +63,122 @@ beforeEach(() => { mockServer.getTransaction.mockResolvedValue({ status: "SUCCESS" }); }); -// ── Helpers ──────────────────────────────────────────────────────────────── -const testnetConfig = { - network: "testnet" as const, - rpcUrl: "https://soroban-testnet.stellar.org", - contractAddress: CONTRACT_ADDRESS, -}; - -const mainnetConfig = { - network: "mainnet" as const, - rpcUrl: "https://soroban-mainnet.stellar.org", - contractAddress: CONTRACT_ADDRESS, -}; - // ── Tests ────────────────────────────────────────────────────────────────── describe("stakeF — addressToScVal validation", () => { it("rejects addresses that are not valid Stellar public keys", async () => { - const result = await stake("INVALID_ADDRESS", 100, testnetConfig); + const result = await stake("INVALID_ADDRESS", 100); expect(typeof result).toBe("string"); expect(result).toMatch(/Invalid address format/i); }); it("rejects token addresses with wrong format in initialize", async () => { - const result = await initialize(CALLER, "bad-token-address", 10, testnetConfig); + const result = await initialize(CALLER, "bad-token-address", 10); expect(typeof result).toBe("string"); expect(result).toMatch(/Invalid address format/i); }); }); describe("stakeF — initialize", () => { - it("returns a success message on valid testnet initialization", async () => { - const result = await initialize(CALLER, TOKEN_ADDRESS, 100, testnetConfig); + it("returns a success message on valid initialization", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 100); expect(result).toBe("Contract initialized successfully"); }); - it("returns a success message on valid mainnet initialization with explicit contract", async () => { - const result = await initialize(CALLER, TOKEN_ADDRESS, 50, mainnetConfig); + it("returns a success message with zero reward rate (edge case)", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 0); expect(result).toBe("Contract initialized successfully"); }); - it("accepts zero as a reward rate (edge case)", async () => { - const result = await initialize(CALLER, TOKEN_ADDRESS, 0, testnetConfig); + it("returns a success message with a large reward rate", async () => { + const result = await initialize(CALLER, TOKEN_ADDRESS, 999_999); expect(result).toBe("Contract initialized successfully"); }); }); describe("stakeF — stake", () => { it("returns a confirmation message with the staked amount", async () => { - const result = await stake(CALLER, 1000, testnetConfig); + const result = await stake(CALLER, 1000); expect(result).toBe("Staked 1000 successfully"); }); it("handles large stake amounts correctly", async () => { - const result = await stake(CALLER, 999_999_999, testnetConfig); + const result = await stake(CALLER, 999_999_999); expect(result).toBe("Staked 999999999 successfully"); }); - it("propagates mainnet config without falling back to testnet contract", async () => { - const result = await stake(CALLER, 500, mainnetConfig); - expect(result).toBe("Staked 500 successfully"); + it("handles minimum stake amount of 1", async () => { + const result = await stake(CALLER, 1); + expect(result).toBe("Staked 1 successfully"); }); }); describe("stakeF — unstake", () => { it("returns a confirmation message with the unstaked amount", async () => { - const result = await unstake(CALLER, 200, testnetConfig); + const result = await unstake(CALLER, 200); expect(result).toBe("Unstaked 200 successfully"); }); - it("handles partial unstake (amount less than full stake)", async () => { - const result = await unstake(CALLER, 1, testnetConfig); + it("handles partial unstake correctly", async () => { + const result = await unstake(CALLER, 1); expect(result).toBe("Unstaked 1 successfully"); }); + + it("handles large unstake amounts", async () => { + const result = await unstake(CALLER, 500_000); + expect(result).toBe("Unstaked 500000 successfully"); + }); }); describe("stakeF — claimRewards", () => { it("returns success message on successful claim", async () => { - const result = await claimRewards(CALLER, testnetConfig); + const result = await claimRewards(CALLER); expect(result).toBe("Rewards claimed successfully"); }); - it("works on mainnet config without error", async () => { - const result = await claimRewards(CALLER, mainnetConfig); + it("returns success message when called a second time (idempotent mock)", async () => { + await claimRewards(CALLER); + const result = await claimRewards(CALLER); expect(result).toBe("Rewards claimed successfully"); }); }); describe("stakeF — getStake", () => { it("returns a stake info string for the queried user", async () => { - const result = await getStake(CALLER, USER_ADDRESS, testnetConfig); + const result = await getStake(CALLER, USER_ADDRESS); expect(typeof result).toBe("string"); expect(result).toContain(USER_ADDRESS); }); it("rejects invalid user addresses early", async () => { - const result = await getStake(CALLER, "0xBADADDRESS", testnetConfig); + const result = await getStake(CALLER, "0xBADADDRESS"); expect(typeof result).toBe("string"); expect(result).toMatch(/Invalid address format/i); }); -}); -describe("stakeF — mainnet safeguard", () => { - it("succeeds when mainnet config includes a valid contractAddress", async () => { - const result = await stake(CALLER, 100, mainnetConfig); - expect(result).toBe("Staked 100 successfully"); + it("rejects addresses with wrong prefix (not G or C)", async () => { + const result = await getStake(CALLER, "XBADADDRESSFORMAT0000000000000000000000000000000000000000"); + expect(typeof result).toBe("string"); + expect(result).toMatch(/Invalid address format/i); }); }); -describe("stakeF — network switching (RPC URL propagation)", () => { - it("uses testnet RPC URL config for testnet operations", async () => { - // Verify testnet config succeeds end-to-end (no throws) - const result = await stake(CALLER, 10, testnetConfig); - expect(result).toBe("Staked 10 successfully"); - }); - - it("uses mainnet RPC URL config for mainnet operations", async () => { - // Verify mainnet config with a contract address succeeds end-to-end - const result = await stake(CALLER, 10, mainnetConfig); - expect(result).toBe("Staked 10 successfully"); +describe("stakeF — transaction failure handling", () => { + it("returns an error-like string when the transaction ends in a non-SUCCESS status", async () => { + mockServer.getTransaction.mockResolvedValueOnce({ status: "FAILED" }); + const result = await stake(CALLER, 100); + expect(typeof result).toBe("string"); + // stakeF returns 'Staked X successfully' only on SUCCESS; + // on FAILED status it returns the status string or an error message + expect(result).toBeDefined(); + }); + + it("returns a defined string when getAccount rejects (account does not exist)", async () => { + // getAccount is wrapped in .catch() in stakeF, so it throws which bubbles to outer catch + // The outer catch returns the error message as a string + mockServer.getAccount.mockImplementationOnce(() => + Promise.reject(new Error("Account not found")) + ); + const result = await stake(CALLER, 100); + expect(typeof result).toBe("string"); }); });