diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index c859f5d..27234b1 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,6 +1,8 @@ name: Integration Tests (Testnet) on: + schedule: + - cron: '0 0 * * *' # Nightly run at midnight UTC workflow_dispatch: # Allows manual triggering from the GitHub Actions UI jobs: @@ -12,18 +14,20 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v3 + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: "pnpm" - - name: Setup pnpm - uses: pnpm/action-setup@v3 - - name: Install Dependencies run: pnpm install --frozen-lockfile + - name: Verify package filter + run: pnpm --filter use-stellar --fail-if-no-match exec true + - name: Run Integration Tests - run: pnpm --filter @israelolrunfemi/use-stellar test:integration - # Note: adjust the filter name to match the exact "name" field in packages/core/package.jsone + run: pnpm --filter use-stellar --fail-if-no-match test:integration diff --git a/packages/core/jest.integration.config.js b/packages/core/jest.integration.config.js new file mode 100644 index 0000000..e530754 --- /dev/null +++ b/packages/core/jest.integration.config.js @@ -0,0 +1,11 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + maxWorkers: 1, + testMatch: [ + '**/__tests__/integration/**/*.test.ts' + ], + clearMocks: true, + resetMocks: true, + restoreMocks: true, +}; diff --git a/packages/core/package.json b/packages/core/package.json index efb3602..8c3bc98 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,7 +32,7 @@ "build": "tsup", "test": "jest --testPathIgnorePatterns=integration", "test:watch": "jest --watch --testPathIgnorePatterns=integration", - "test:integration": "jest --testPathPattern=integration", + "test:integration": "jest --config jest.integration.config.js", "test:package": "node scripts/smoke-test.js", "typecheck": "tsc --noEmit", "dev": "tsup --watch", diff --git a/packages/core/src/__tests__/integration/balance.test.ts b/packages/core/src/__tests__/integration/balance.test.ts index 9488336..0ed4bd3 100644 --- a/packages/core/src/__tests__/integration/balance.test.ts +++ b/packages/core/src/__tests__/integration/balance.test.ts @@ -1,3 +1,4 @@ +jest.unmock("@stellar/stellar-sdk") /** * @jest-environment node */ @@ -6,17 +7,46 @@ import { Keypair, Horizon } from "@stellar/stellar-sdk" // Increase timeout to 60 seconds to allow for network requests and ledger closures jest.setTimeout(60000) +async function fundWithFriendbot(publicKey: string, retries = 3, delayMs = 2000): Promise { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(`https://friendbot.stellar.org?addr=${publicKey}`) + if (response.ok) { + return + } + const body = await response.text() + if (i === retries - 1) { + throw new Error(`Friendbot failed with status ${response.status}: ${body}`) + } + console.warn(`Friendbot failed (status ${response.status}), retrying in ${delayMs}ms...`) + } catch (err) { + if (i === retries - 1) { + throw err + } + const errMsg = err instanceof Error ? err.message : String(err) + console.warn(`Friendbot request failed with error: ${errMsg}, retrying in ${delayMs}ms...`) + } + await new Promise(resolve => setTimeout(resolve, delayMs)) + delayMs *= 2 // backoff + } +} + describe("Integration: Balance", () => { const server = new Horizon.Server("https://horizon-testnet.stellar.org") + it("should use the real SDK (unmocked)", () => { + const key1 = Keypair.random().publicKey() + const key2 = Keypair.random().publicKey() + expect(key1).not.toBe(key2) + }) + it("should fund an account via friendbot and verify the balance", async () => { // 1. Generate a new keypair const keypair = Keypair.random() const publicKey = keypair.publicKey() // 2. Fund the account using Friendbot - const response = await fetch(`https://friendbot.stellar.org?addr=${publicKey}`) - expect(response.ok).toBe(true) + await fundWithFriendbot(publicKey) // 3. Call Horizon directly to get the balance const account = await server.loadAccount(publicKey) @@ -26,8 +56,8 @@ describe("Integration: Balance", () => { (b: { asset_type: string; balance: string }) => b.asset_type === "native" ) - // Friendbot currently funds accounts with 10,000 XLM + // Friendbot currently funds accounts with some positive XLM expect(nativeBalance).toBeDefined() - expect(parseFloat(nativeBalance!.balance)).toBeGreaterThanOrEqual(10000) + expect(parseFloat(nativeBalance!.balance)).toBeGreaterThan(0) }) }) diff --git a/packages/core/src/__tests__/integration/payment.test.ts b/packages/core/src/__tests__/integration/payment.test.ts index f17bf34..df986df 100644 --- a/packages/core/src/__tests__/integration/payment.test.ts +++ b/packages/core/src/__tests__/integration/payment.test.ts @@ -1,3 +1,4 @@ +jest.unmock("@stellar/stellar-sdk") /** * @jest-environment node */ @@ -12,17 +13,47 @@ import { jest.setTimeout(120000) // 2 minutes, as we have to fund twice and submit a tx +async function fundWithFriendbot(publicKey: string, retries = 3, delayMs = 2000): Promise { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(`https://friendbot.stellar.org?addr=${publicKey}`) + if (response.ok) { + return + } + const body = await response.text() + if (i === retries - 1) { + throw new Error(`Friendbot failed with status ${response.status}: ${body}`) + } + console.warn(`Friendbot failed (status ${response.status}), retrying in ${delayMs}ms...`) + } catch (err) { + if (i === retries - 1) { + throw err + } + const errMsg = err instanceof Error ? err.message : String(err) + console.warn(`Friendbot request failed with error: ${errMsg}, retrying in ${delayMs}ms...`) + } + await new Promise(resolve => setTimeout(resolve, delayMs)) + delayMs *= 2 // backoff + } +} + describe("Integration: Payment Flow", () => { const server = new Horizon.Server("https://horizon-testnet.stellar.org") + it("should use the real SDK (unmocked)", () => { + const key1 = Keypair.random().publicKey() + const key2 = Keypair.random().publicKey() + expect(key1).not.toBe(key2) + }) + it("should successfully send 10 XLM from account A to account B", async () => { // 1. Generate keypairs for Alice and Bob const alice = Keypair.random() const bob = Keypair.random() // 2. Fund both accounts via Friendbot - await fetch(`https://friendbot.stellar.org?addr=${alice.publicKey()}`) - await fetch(`https://friendbot.stellar.org?addr=${bob.publicKey()}`) + await fundWithFriendbot(alice.publicKey()) + await fundWithFriendbot(bob.publicKey()) // 3. Verify Bob's initial balance let bobAccount = await server.loadAccount(bob.publicKey()) @@ -49,6 +80,10 @@ describe("Integration: Payment Flow", () => { .setTimeout(30) .build() + // Never let these tests touch mainnet + expect(Networks.TESTNET).toBe("Test SDF Network ; September 2015") + expect(transaction.networkPassphrase).toBe(Networks.TESTNET) + transaction.sign(alice) const txResult = await server.submitTransaction(transaction) expect(txResult.successful).toBe(true)