Fix tests

This commit is contained in:
2026-04-27 09:45:38 +00:00
parent e97054fa34
commit bd1ae909b5
7 changed files with 92 additions and 44 deletions

View File

@@ -56,7 +56,7 @@ export class RatesService extends EventEmitter<RatesServiceEventMap> {
private unsubscribeFromAdapter: (() => void) | null = null; private unsubscribeFromAdapter: (() => void) | null = null;
private started = false; private started = false;
private constructor(adapter: RatesAdapter) { constructor(adapter: RatesAdapter) {
super(); super();
this.adapter = adapter; this.adapter = adapter;
} }

View File

@@ -45,6 +45,7 @@ import {
expectLogs, expectLogs,
type LogExpectation, type LogExpectation,
} from "../mocks/command"; } from "../mocks/command";
import { State } from "@xo-cash/state";
// ============================================================================ // ============================================================================
// Error Cases - Validate argument parsing and error handling // Error Cases - Validate argument parsing and error handling
@@ -156,7 +157,8 @@ describe("invitation command - error cases", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-errors-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-errors-"));
@@ -196,7 +198,8 @@ describe("invitation command - receive flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-receive-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-receive-"));
@@ -308,7 +311,8 @@ describe("invitation command - request satoshis flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-request-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-request-"));
@@ -396,12 +400,15 @@ describe("invitation command - request satoshis flow", () => {
describe("invitation command - send flow with resources", () => { describe("invitation command - send flow with resources", () => {
let engine: Engine; let engine: Engine;
let state: State;
let app: AppService; let app: AppService;
let tempDir: string; let tempDir: string;
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
state = mockEngine.state;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-send-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-send-"));
@@ -497,7 +504,7 @@ describe("invitation command - send flow with resources", () => {
* This validates our test infrastructure works correctly. * This validates our test infrastructure works correctly.
*/ */
test("fake resources are accessible via engine", async () => { test("fake resources are accessible via engine", async () => {
const resource = await addFakeResource(engine, { const resource = await addFakeResource(state!, {
valueSatoshis: 50000, valueSatoshis: 50000,
templateIdentifier: p2pkhTemplateIdentifier, templateIdentifier: p2pkhTemplateIdentifier,
outputIdentifier: "receiveOutput", outputIdentifier: "receiveOutput",
@@ -526,7 +533,8 @@ describe("invitation command - multi-step append", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-append-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-append-"));
@@ -673,7 +681,8 @@ describe("invitation command - list and inspect", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-list-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-list-"));
@@ -841,7 +850,8 @@ describe("invitation command - sign flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-sign-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-sign-"));
@@ -961,7 +971,8 @@ describe("invitation command - import flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-import-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-import-"));
@@ -1092,7 +1103,8 @@ describe("invitation command - auto-inputs flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-autoinputs-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-autoinputs-"));
@@ -1167,7 +1179,8 @@ describe("invitation command - broadcast flow", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-broadcast-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-broadcast-"));
@@ -1231,7 +1244,8 @@ describe("invitation command - full lifecycle", () => {
let paths: CommandPaths; let paths: CommandPaths;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-lifecycle-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-lifecycle-"));

View File

@@ -81,7 +81,8 @@ describe("receive command", () => {
let tempDir: string; let tempDir: string;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);

View File

@@ -22,6 +22,7 @@ import {
expectLogs, expectLogs,
type LogExpectation, type LogExpectation,
} from "../mocks/command"; } from "../mocks/command";
import { State } from "@xo-cash/state";
type TestCase = { type TestCase = {
name: string; name: string;
@@ -120,7 +121,8 @@ describe("resource command", () => {
let tempDir: string; let tempDir: string;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
@@ -181,11 +183,14 @@ describe("resource command", () => {
describe("resource command with populated data", () => { describe("resource command with populated data", () => {
let engine: Engine; let engine: Engine;
let state: State;
let app: AppService; let app: AppService;
let tempDir: string; let tempDir: string;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
state = mockEngine.state;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-"));
@@ -197,8 +202,8 @@ describe("resource command with populated data", () => {
}); });
test("list returns count when resources exist", async () => { test("list returns count when resources exist", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000 }); await addFakeResource(state, { valueSatoshis: 25000 });
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
const result = await handleResourceCommand( const result = await handleResourceCommand(
@@ -212,8 +217,8 @@ describe("resource command with populated data", () => {
}); });
test("list shows total satoshis", async () => { test("list shows total satoshis", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000 }); await addFakeResource(state, { valueSatoshis: 25000 });
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
await handleResourceCommand(createCommandDeps(app, io), ["list"], {}); await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
@@ -222,8 +227,8 @@ describe("resource command with populated data", () => {
}); });
test("list excludes reserved resources by default", async () => { test("list excludes reserved resources by default", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: "inv-123",
}); });
@@ -239,12 +244,12 @@ describe("resource command with populated data", () => {
}); });
test("list reserved shows only reserved resources", async () => { test("list reserved shows only reserved resources", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: "inv-123",
}); });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 10000, valueSatoshis: 10000,
reservedBy: "inv-456", reservedBy: "inv-456",
}); });
@@ -261,8 +266,8 @@ describe("resource command with populated data", () => {
}); });
test("list all shows both reserved and unreserved", async () => { test("list all shows both reserved and unreserved", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: "inv-123",
}); });
@@ -278,7 +283,7 @@ describe("resource command with populated data", () => {
}); });
test("unreserve releases a reserved UTXO", async () => { test("unreserve releases a reserved UTXO", async () => {
const resource = await addFakeResource(engine, { const resource = await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: "inv-123",
}); });
@@ -306,7 +311,7 @@ describe("resource command with populated data", () => {
}); });
test("unreserve reports when UTXO is not reserved", async () => { test("unreserve reports when UTXO is not reserved", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 25000 }); const resource = await addFakeResource(state, { valueSatoshis: 25000 });
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
await handleResourceCommand( await handleResourceCommand(
@@ -322,12 +327,12 @@ describe("resource command with populated data", () => {
}); });
test("unreserve-all releases all reserved UTXOs", async () => { test("unreserve-all releases all reserved UTXOs", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: "inv-123",
}); });
await addFakeResource(engine, { await addFakeResource(state, {
valueSatoshis: 10000, valueSatoshis: 10000,
reservedBy: "inv-456", reservedBy: "inv-456",
}); });
@@ -348,7 +353,7 @@ describe("resource command with populated data", () => {
}); });
test("list displays outpoint information", async () => { test("list displays outpoint information", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 12345 }); const resource = await addFakeResource(state, { valueSatoshis: 12345 });
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
await handleResourceCommand(createCommandDeps(app, io), ["list"], {}); await handleResourceCommand(createCommandDeps(app, io), ["list"], {});

View File

@@ -183,7 +183,8 @@ describe("template command", () => {
let tempDir: string; let tempDir: string;
beforeEach(async () => { beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine);

View File

@@ -4,6 +4,7 @@ import {
createStorageAdapter, createStorageAdapter,
State, State,
StorageType, StorageType,
UnspentOutputStatus,
type UnspentOutputData, type UnspentOutputData,
} from "@xo-cash/state"; } from "@xo-cash/state";
import { InMemoryBlockchainProvider } from "@xo-cash/engine"; import { InMemoryBlockchainProvider } from "@xo-cash/engine";
@@ -13,6 +14,8 @@ import { binToHex, sha256 } from "@bitauth/libauth";
import { AppService } from "../../../src/services/app"; import { AppService } from "../../../src/services/app";
import { InMemoryStorage } from "../../../src/services/storage"; import { InMemoryStorage } from "../../../src/services/storage";
import { MockElectrumService } from "./electrum-service"; import { MockElectrumService } from "./electrum-service";
import { MockRatesService } from "./rates-service";
import { RatesService } from "../../../src/services/rates";
export const DEFAULT_SEED = export const DEFAULT_SEED =
"page pencil stock planet limb cluster assault speak off joke private pioneer"; "page pencil stock planet limb cluster assault speak off joke private pioneer";
@@ -57,11 +60,11 @@ export const randomTxHash = (): string => {
* @returns The created UnspentOutputData object. * @returns The created UnspentOutputData object.
*/ */
export const addFakeResource = async ( export const addFakeResource = async (
engine: Engine, state: State,
options: FakeResourceOptions = {}, options: FakeResourceOptions = {},
): Promise<UnspentOutputData> => { ): Promise<UnspentOutputData> => {
const resource: UnspentOutputData = { const resource: UnspentOutputData = {
status: "confirmed", status: UnspentOutputStatus.CONFIRMED,
selectable: true, selectable: true,
privacy: false, privacy: false,
templateIdentifier: options.templateIdentifier ?? "test-template", templateIdentifier: options.templateIdentifier ?? "test-template",
@@ -76,7 +79,7 @@ export const addFakeResource = async (
reservedBy: options.reservedBy, reservedBy: options.reservedBy,
}; };
await engine.state.storeUnspentOutputData(resource); await state.storeUnspentOutputData(resource);
return resource; return resource;
}; };
@@ -88,12 +91,12 @@ export const addFakeResource = async (
* @param invitationIdentifier - The invitation identifier to reserve for. * @param invitationIdentifier - The invitation identifier to reserve for.
*/ */
export const reserveResource = async ( export const reserveResource = async (
engine: Engine, state: State,
outpointTransactionHash: string, outpointTransactionHash: string,
outpointIndex: number, outpointIndex: number,
invitationIdentifier: string, invitationIdentifier: string,
): Promise<void> => { ): Promise<void> => {
await engine.state.executeBulkUnspentOutputReservation( await state.executeBulkUnspentOutputReservation(
[{ outpointTransactionHash, outpointIndex }], [{ outpointTransactionHash, outpointIndex }],
true, true,
invitationIdentifier, invitationIdentifier,
@@ -108,12 +111,12 @@ export const reserveResource = async (
* @param invitationIdentifier - The invitation identifier to unreserve from. * @param invitationIdentifier - The invitation identifier to unreserve from.
*/ */
export const unreserveResource = async ( export const unreserveResource = async (
engine: Engine, state: State,
outpointTransactionHash: string, outpointTransactionHash: string,
outpointIndex: number, outpointIndex: number,
invitationIdentifier: string, invitationIdentifier: string,
): Promise<void> => { ): Promise<void> => {
await engine.state.executeBulkUnspentOutputReservation( await state.executeBulkUnspentOutputReservation(
[{ outpointTransactionHash, outpointIndex }], [{ outpointTransactionHash, outpointIndex }],
false, false,
invitationIdentifier, invitationIdentifier,
@@ -153,13 +156,14 @@ export const createMockEngine = async (seed: string) => {
const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider); const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider);
await engine.initializeStateSync(); await engine.initializeStateSync();
return engine; return { engine, state, blockchainMonitor, blockchainProvider };
}; };
export const createMockAppService = async (engine: Engine) => { export const createMockAppService = async (engine: Engine) => {
const storage = await InMemoryStorage.create(); const storage = await InMemoryStorage.create();
const electrum = new MockElectrumService(); const mockRates = new MockRatesService();
const rates = new RatesService(mockRates);
const config = { const config = {
syncServerUrl: "http://localhost:3000", syncServerUrl: "http://localhost:3000",
@@ -170,5 +174,5 @@ export const createMockAppService = async (engine: Engine) => {
invitationStoragePath: "test-invitations.db", invitationStoragePath: "test-invitations.db",
}; };
return new AppService(engine, storage, config, electrum); return new AppService(engine, storage, config, rates);
}; };

View File

@@ -0,0 +1,23 @@
import { BaseRates } from "../../../src/utils/rates/base-rates";
export class MockRatesService extends BaseRates {
constructor() {
super();
}
async getRate(numeratorUnitCode: string, denominatorUnitCode: string): Promise<number> {
return 1;
}
async start(): Promise<void> {
return;
}
async stop(): Promise<void> {
return;
}
async listPairs(): Promise<Set<string>> {
return new Set();
}
}