Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,31 @@ export class VeriTixClient extends EventEmitter {
// -------------------------------------------------------------------------

/**
* Initialises the Soroban RPC server connection and verifies it is reachable
* Establishes a connection to the Soroban RPC and verifies it is reachable
* by fetching the current ledger sequence.
*
* Retries with exponential backoff up to `config.retries` times (default 3).
* Call this once when your application starts. All module method calls
* require an active connection — calling them before connect() resolves
* throws a {@link VeriTixError} with code `CLIENT_NOT_CONNECTED`, raised by
* the lazy server proxy the modules hold.
*
* Reconnection: call connect() again to re-establish. Note that a connection
* dropping mid-session is not detected up-front — `connected` stays true, so
* module calls fail with whatever transport error the RPC surfaces rather
* than a connection-specific code.
*
* For read-only usage, connect() is still required to fetch the current ledger.
*
* Contract existence is NOT checked here — connect() only proves the RPC
* endpoint answers. Use {@link healthCheck} to confirm the contract is
* actually deployed on the network.
*
* Lifecycle: `new VeriTixClient(config)` → `connect()` → module calls →
* `disconnect()`. {@link disconnect} tears the server down and clears the
* ledger cache, putting the client back in the pre-connect state; it is safe
* to call `connect()` again afterwards on the same instance. Reconnection is
* never automatic — the only retries are the exponential-backoff attempts
* made *within* a single `connect()` call (up to `config.retries`, default 3).
*
* @returns The current Stellar ledger sequence number.
* @throws {VeriTixError} With code `CONNECTION_FAILED` if unreachable after all retries.
Expand Down
17 changes: 11 additions & 6 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { RecurringModule } from '../src/modules/recurring';
import { AdminModule } from '../src/modules/admin';
import { BatchModule } from '../src/modules/batch';
import { VeriTixError, VeriTixErrorCode } from '../src/utils/errors';
import {
makeConnectedClient as makeSharedConnectedClient,
makeMockServer,
} from './helpers/mocks';

// Mock the Freighter wallet API for the createFromFreighter unit tests (#482).
const mockFreighter = {
Expand All @@ -40,16 +44,17 @@ jest.mock('../src/utils/transaction', () => ({

const FAKE_CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4';

// Helper: create a client whose internal server is pre-mocked
// Helper: create a client whose internal server is pre-mocked.
// Builds on the shared factories in tests/helpers/mocks.ts, adding only the
// per-test ledger sequence and the mock-server handle these tests assert on.
function makeConnectedClient(sequence = 100) {
const client = new VeriTixClient(getTestnetConfig(FAKE_CONTRACT_ID));
// Inject a mock server directly
const mockServer = { getLatestLedger: jest.fn().mockResolvedValue({ sequence }) };
const client = makeSharedConnectedClient();
const mockServer = makeMockServer({
getLatestLedger: jest.fn().mockResolvedValue({ sequence }),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).server = mockServer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).connected = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).ledgerCache = { sequence, fetchedAt: Date.now() };
return { client, mockServer };
}
Expand Down
108 changes: 105 additions & 3 deletions tests/helpers/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* wire up a `SorobanRpc.Server`, `NetworkConfig`, and `VeriTixClient`.
*/

import { Keypair } from '@stellar/stellar-sdk';
import { Keypair, xdr } from '@stellar/stellar-sdk';
import type { SorobanRpc } from '@stellar/stellar-sdk';
import type { NetworkConfig } from '../../src/types/index';
import { VeriTixClient } from '../../src/client';

Expand All @@ -15,8 +16,12 @@ import { VeriTixClient } from '../../src/client';
// ---------------------------------------------------------------------------

const FAKE_CONTRACT = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4';
/** Deterministic test secret — never use on real networks */
const TEST_SECRET = 'SCZANGBA5RLRPGKVK3GS4TNCG7SXALKVXBKEB7DRMU5G9CPMZRSGKF7';
/**
* Deterministic test secret — never use on real networks.
* Derived from a fixed all-`0x07` ed25519 seed, so the public key is always
* GDVEU3DD4KOFECV66VIHWEZOYX4ZKR3WV27L464SIIPOU2IUI3JCZA57.
*/
const TEST_SECRET = 'SADQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQP54X';

// ---------------------------------------------------------------------------
// Factories
Expand Down Expand Up @@ -75,3 +80,100 @@ export function createMockClient(
export function createMockKeypair(): Keypair {
return Keypair.fromSecret(TEST_SECRET);
}

// ---------------------------------------------------------------------------
// Issue #3 — canonical `make*` factories
//
// These are the names test files should reach for. They wrap the `create*`
// factories above (kept for the existing call-sites) and add helpers for the
// two mock shapes every write/read test needs: a successful transaction
// round-trip, and a simulation that returns a specific ScVal.
// ---------------------------------------------------------------------------

/**
* Returns a jest-mocked `SorobanRpc.Server`.
*
* Defaults are the happy path: a ledger at sequence 1000, an empty simulation
* result, and a transaction that is accepted and confirmed. Override any
* method per-test with `server.simulateTransaction.mockResolvedValue(...)`.
*/
export function makeMockServer(
overrides: Record<string, jest.Mock> = {},
): jest.Mocked<SorobanRpc.Server> {
return createMockServer(overrides) as unknown as jest.Mocked<SorobanRpc.Server>;
}

/**
* Returns a deterministic `Keypair` — the same public key on every call, so
* tests can assert against it directly.
*/
export function makeMockKeypair(): Keypair {
return createMockKeypair();
}

/**
* Returns a `VeriTixClient` that is already "connected" (mock server injected,
* `connected` flag set, ledger cache warm) and able to sign.
*
* @param keypair - Signer to attach. Defaults to {@link makeMockKeypair}.
*/
export function makeConnectedClient(keypair: Keypair = makeMockKeypair()): VeriTixClient {
const client = new VeriTixClient(createMockConfig(), keypair);
attachMockServer(client, makeMockServer());
return client;
}

/**
* Returns a connected `VeriTixClient` with **no** keypair, so write operations
* throw `VeriTixErrorCode.ReadOnlyClient`.
*/
export function makeReadOnlyClient(): VeriTixClient {
const client = new VeriTixClient(createMockConfig());
attachMockServer(client, makeMockServer());
return client;
}

/**
* Points a mock server at a transaction that is accepted (`PENDING`) and then
* confirmed (`SUCCESS`) on the next poll.
*/
export function mockSuccessfulTransaction(server: jest.Mocked<SorobanRpc.Server>): void {
const s = server as unknown as Record<string, jest.Mock>;
s.sendTransaction.mockResolvedValue({ hash: 'mock-hash', status: 'PENDING' });
s.getTransaction.mockResolvedValue({
status: 'SUCCESS',
hash: 'mock-hash',
ledger: 1000,
returnValue: xdr.ScVal.scvVoid(),
});
}

/**
* Points a mock server's `simulateTransaction` at a specific return value,
* shaped the way `SorobanRpc.Api.SimulateTransactionSuccessResponse` is read
* by the SDK's transaction helpers.
*/
export function mockSimulationResult(
server: jest.Mocked<SorobanRpc.Server>,
returnValue: xdr.ScVal,
): void {
const s = server as unknown as Record<string, jest.Mock>;
s.simulateTransaction.mockResolvedValue({
result: { retval: returnValue, auth: [] },
transactionData: {},
minResourceFee: '100',
latestLedger: 1000,
});
}

/**
* Injects a mock server into a client and marks it connected.
* Mirrors the field-injection pattern used across the existing test files.
*/
function attachMockServer(client: VeriTixClient, server: jest.Mocked<SorobanRpc.Server>): void {
/* eslint-disable @typescript-eslint/no-explicit-any */
(client as any).server = server;
(client as any).connected = true;
(client as any).ledgerCache = { sequence: 1000, fetchedAt: Date.now() };
/* eslint-enable @typescript-eslint/no-explicit-any */
}
17 changes: 9 additions & 8 deletions tests/issue-442-getLedgerInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
* `VeriTixClient.getCurrentLedger()` method, so these tests target that.
*/

import { VeriTixClient } from '../src/client';
import { getTestnetConfig } from '../src/utils/network';
import { VeriTixError, VeriTixErrorCode } from '../src/utils/errors';
import { makeConnectedClient as makeClient, makeMockServer } from './helpers/mocks';

const FAKE_CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4';

/**
* Wraps the shared {@link makeConnectedClient} factory so these tests can pin
* a specific ledger sequence and keep a handle on the mock server.
*/
function makeConnectedClient(sequence = 100) {
const client = new VeriTixClient(getTestnetConfig(FAKE_CONTRACT_ID));
const mockServer = { getLatestLedger: jest.fn().mockResolvedValue({ sequence }) };
const mockServer = makeMockServer({
getLatestLedger: jest.fn().mockResolvedValue({ sequence }),
});
const client = makeClient();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).server = mockServer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).connected = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(client as any).ledgerCache = { sequence, fetchedAt: Date.now() };
return { client, mockServer };
}
Expand Down
28 changes: 28 additions & 0 deletions tests/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @file tests/smoke.test.ts
* Module-load smoke test.
*
* A broken re-export in `src/index.ts` fails at runtime, not at build time —
* these tests import the public barrel and assert the headline exports are
* actually there, so a bad barrel is caught by CI instead of by consumers.
*/

import {
VeriTixClient,
VeriTixError,
VeriTixErrorCode,
getTestnetConfig,
} from '../src/index';

describe('SDK module loads', () => {
it('exports VeriTixClient', () => expect(VeriTixClient).toBeDefined());
it('exports VeriTixError', () => expect(VeriTixError).toBeDefined());
it('exports VeriTixErrorCode', () => expect(VeriTixErrorCode).toBeDefined());
it('exports getTestnetConfig', () => expect(getTestnetConfig).toBeDefined());

it('getTestnetConfig returns a valid config', () => {
const cfg = getTestnetConfig('CXXXXXXX...');
expect(cfg.network).toBe('testnet');
expect(cfg.rpcUrl).toContain('soroban');
});
});
15 changes: 15 additions & 0 deletions tests/utils/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ describe('VeriTixError', () => {
const err = new VeriTixError(VeriTixErrorCode.Unknown, 'msg');
expect(err.name).toBe('VeriTixError');
});

// Property-structure regression guard — name / prototype chain / code / message
// are the four things consumers pattern-match on, so pin all four.
it('VeriTixError has name VeriTixError', () => {
const err = new VeriTixError(VeriTixErrorCode.ReadOnlyClient, 'test');
expect(err.name).toBe('VeriTixError');
expect(err instanceof Error).toBe(true);
expect(err instanceof VeriTixError).toBe(true);
});

it('VeriTixError.code is the enum value', () => {
const err = new VeriTixError(VeriTixErrorCode.EscrowNotFound, 'not found');
expect(err.code).toBe(VeriTixErrorCode.EscrowNotFound);
expect(err.message).toBe('not found');
});
});

describe('parseSorobanError', () => {
Expand Down
Loading