Tests. Autocomplete. Few Fixes. Mocks for Electrum Service. Template-to-Json parser. Fix global paths. Use IO Dependency injection for logging from cli. Additional commands in CLI.
This commit is contained in:
162
tests/cli/mocks/command.ts
Normal file
162
tests/cli/mocks/command.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { vi, expect, type Mock } from "vitest";
|
||||
import type {
|
||||
BaseCommandDependencies,
|
||||
CommandDependencies,
|
||||
CommandIO,
|
||||
CommandPaths,
|
||||
} from "../../../src/cli/commands/types";
|
||||
import type { AppService } from "../../../src/services/app";
|
||||
|
||||
/**
|
||||
* Captured CLI IO buffers used by tests.
|
||||
*/
|
||||
export type MockIOCapture = {
|
||||
out: string[];
|
||||
err: string[];
|
||||
verbose: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Spy functions for each IO channel.
|
||||
*/
|
||||
export type MockIOSpies = {
|
||||
out: Mock;
|
||||
err: Mock;
|
||||
verbose: Mock;
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete mock IO result including the IO adapter, capture buffers, and spies.
|
||||
*/
|
||||
export type MockIO = {
|
||||
io: CommandIO;
|
||||
capture: MockIOCapture;
|
||||
spies: MockIOSpies;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines an expected log message for assertion.
|
||||
* At least one of out, err, or verbose should be specified.
|
||||
*/
|
||||
export type LogExpectation = {
|
||||
/** Expected substring (or exact match if exact=true) in io.out */
|
||||
out?: string;
|
||||
/** Expected substring (or exact match if exact=true) in io.err */
|
||||
err?: string;
|
||||
/** Expected substring (or exact match if exact=true) in io.verbose */
|
||||
verbose?: string;
|
||||
/** If true, match the string exactly instead of using contains (default: false) */
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a command IO adapter that records every message using vi.fn() spies.
|
||||
* This enables vitest's built-in matchers like toHaveBeenCalledWith.
|
||||
*/
|
||||
export const createMockIO = (): MockIO => {
|
||||
const capture: MockIOCapture = {
|
||||
out: [],
|
||||
err: [],
|
||||
verbose: [],
|
||||
};
|
||||
|
||||
const outSpy = vi.fn((message: string) => {
|
||||
capture.out.push(message);
|
||||
});
|
||||
const errSpy = vi.fn((message: string) => {
|
||||
capture.err.push(message);
|
||||
});
|
||||
const verboseSpy = vi.fn((message: string) => {
|
||||
capture.verbose.push(message);
|
||||
});
|
||||
|
||||
const io: CommandIO = {
|
||||
out: outSpy,
|
||||
err: errSpy,
|
||||
verbose: verboseSpy,
|
||||
};
|
||||
|
||||
return {
|
||||
io,
|
||||
capture,
|
||||
spies: {
|
||||
out: outSpy,
|
||||
err: errSpy,
|
||||
verbose: verboseSpy,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts that the expected log messages were printed to the appropriate IO channels.
|
||||
* @param spies - The mock IO spies from createMockIO
|
||||
* @param logs - Array of log expectations to validate
|
||||
*/
|
||||
export const expectLogs = (spies: MockIOSpies, logs: LogExpectation[]): void => {
|
||||
for (const log of logs) {
|
||||
if (log.out !== undefined) {
|
||||
if (log.exact) {
|
||||
expect(spies.out).toHaveBeenCalledWith(log.out);
|
||||
} else {
|
||||
expect(spies.out).toHaveBeenCalledWith(expect.stringContaining(log.out));
|
||||
}
|
||||
}
|
||||
if (log.err !== undefined) {
|
||||
if (log.exact) {
|
||||
expect(spies.err).toHaveBeenCalledWith(log.err);
|
||||
} else {
|
||||
expect(spies.err).toHaveBeenCalledWith(expect.stringContaining(log.err));
|
||||
}
|
||||
}
|
||||
if (log.verbose !== undefined) {
|
||||
if (log.exact) {
|
||||
expect(spies.verbose).toHaveBeenCalledWith(log.verbose);
|
||||
} else {
|
||||
expect(spies.verbose).toHaveBeenCalledWith(expect.stringContaining(log.verbose));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates mock paths for testing.
|
||||
* @param tempDir - Optional temp directory to use as base for all paths
|
||||
*/
|
||||
export const createMockPaths = (tempDir?: string): CommandPaths => {
|
||||
const base = tempDir ?? "/tmp/xo-cli-test";
|
||||
return {
|
||||
mnemonicsDir: base,
|
||||
dataDir: base,
|
||||
walletConfigPath: `${base}/.wallet`,
|
||||
workingDir: base,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates base command dependencies for commands that do not require the app.
|
||||
* @param io - Command IO adapter
|
||||
* @param paths - Optional custom paths (defaults to mock paths)
|
||||
*/
|
||||
export const createBaseCommandDeps = (
|
||||
io: CommandIO,
|
||||
paths?: CommandPaths,
|
||||
): BaseCommandDependencies => ({
|
||||
io,
|
||||
paths: paths ?? createMockPaths(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates command dependencies for app-backed command handlers.
|
||||
* @param app - App service instance
|
||||
* @param io - Command IO adapter
|
||||
* @param paths - Optional custom paths (defaults to mock paths)
|
||||
*/
|
||||
export const createCommandDeps = (
|
||||
app: AppService,
|
||||
io: CommandIO,
|
||||
paths?: CommandPaths,
|
||||
): CommandDependencies => ({
|
||||
app,
|
||||
io,
|
||||
paths: paths ?? createMockPaths(),
|
||||
});
|
||||
7
tests/cli/mocks/electrum-service.ts
Normal file
7
tests/cli/mocks/electrum-service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class MockElectrumService {
|
||||
constructor() {}
|
||||
|
||||
async hasSeenTransaction(transactionHash: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
164
tests/cli/mocks/engine.ts
Normal file
164
tests/cli/mocks/engine.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { BlockchainMonitor, Engine } from "@xo-cash/engine";
|
||||
|
||||
import { createStorageAdapter, State, StorageType, type UnspentOutputData } from "@xo-cash/state";
|
||||
import { InMemoryBlockchainProvider } from "@xo-cash/engine";
|
||||
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
|
||||
|
||||
import { binToHex, sha256 } from "@bitauth/libauth";
|
||||
import { AppService } from "../../../src/services/app";
|
||||
import { InMemoryStorage } from "../../../src/services/storage";
|
||||
import { MockElectrumService } from "./electrum-service";
|
||||
|
||||
export const DEFAULT_SEED = "page pencil stock planet limb cluster assault speak off joke private pioneer";
|
||||
|
||||
/**
|
||||
* Options for creating a fake resource (UTXO) in tests.
|
||||
*/
|
||||
export type FakeResourceOptions = {
|
||||
/** Transaction hash of the outpoint. Auto-generated if not provided. */
|
||||
outpointTransactionHash?: string;
|
||||
/** Index of the outpoint in the transaction. Defaults to 0. */
|
||||
outpointIndex?: number;
|
||||
/** Value in satoshis. Defaults to 10000. */
|
||||
valueSatoshis?: number;
|
||||
/** Template identifier. Defaults to "test-template". */
|
||||
templateIdentifier?: string;
|
||||
/** Output identifier from the template. Defaults to "receiveOutput". */
|
||||
outputIdentifier?: string;
|
||||
/** Locking bytecode for this output. Defaults to a placeholder. */
|
||||
lockingBytecode?: string;
|
||||
/** Block height where the UTXO was mined. Defaults to 800000. */
|
||||
minedAtHeight?: number;
|
||||
/** Invitation identifier that reserves this output. Undefined means unreserved. */
|
||||
reservedBy?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random 64-character hex string representing a transaction hash.
|
||||
*/
|
||||
export const randomTxHash = (): string => {
|
||||
const bytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(bytes);
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a fake resource (UTXO) to the engine's state for testing purposes.
|
||||
* @param engine - The engine instance to add the resource to.
|
||||
* @param options - Options for the fake resource. All fields have sensible defaults.
|
||||
* @returns The created UnspentOutputData object.
|
||||
*/
|
||||
export const addFakeResource = async (
|
||||
engine: Engine,
|
||||
options: FakeResourceOptions = {},
|
||||
): Promise<UnspentOutputData> => {
|
||||
const resource: UnspentOutputData = {
|
||||
status: "confirmed",
|
||||
selectable: true,
|
||||
privacy: false,
|
||||
templateIdentifier: options.templateIdentifier ?? "test-template",
|
||||
outputIdentifier: options.outputIdentifier ?? "receiveOutput",
|
||||
outpointIndex: options.outpointIndex ?? 0,
|
||||
outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(),
|
||||
minedAtHeight: options.minedAtHeight ?? 800000,
|
||||
valueSatoshis: options.valueSatoshis ?? 10000,
|
||||
lockingBytecode: options.lockingBytecode ?? "76a914000000000000000000000000000000000000000088ac",
|
||||
reservedBy: options.reservedBy,
|
||||
};
|
||||
|
||||
await engine.state.storeUnspentOutputData(resource);
|
||||
return resource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reserves a resource for a specific invitation.
|
||||
* @param engine - The engine instance.
|
||||
* @param outpointTransactionHash - The transaction hash of the UTXO to reserve.
|
||||
* @param outpointIndex - The output index of the UTXO to reserve.
|
||||
* @param invitationIdentifier - The invitation identifier to reserve for.
|
||||
*/
|
||||
export const reserveResource = async (
|
||||
engine: Engine,
|
||||
outpointTransactionHash: string,
|
||||
outpointIndex: number,
|
||||
invitationIdentifier: string,
|
||||
): Promise<void> => {
|
||||
await engine.state.executeBulkUnspentOutputReservation(
|
||||
[{ outpointTransactionHash, outpointIndex }],
|
||||
true,
|
||||
invitationIdentifier,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unreserves a resource from a specific invitation.
|
||||
* @param engine - The engine instance.
|
||||
* @param outpointTransactionHash - The transaction hash of the UTXO to unreserve.
|
||||
* @param outpointIndex - The output index of the UTXO to unreserve.
|
||||
* @param invitationIdentifier - The invitation identifier to unreserve from.
|
||||
*/
|
||||
export const unreserveResource = async (
|
||||
engine: Engine,
|
||||
outpointTransactionHash: string,
|
||||
outpointIndex: number,
|
||||
invitationIdentifier: string,
|
||||
): Promise<void> => {
|
||||
await engine.state.executeBulkUnspentOutputReservation(
|
||||
[{ outpointTransactionHash, outpointIndex }],
|
||||
false,
|
||||
invitationIdentifier,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a mock engine instance with a given seed. Uses the in-memory storage and blockchain provider.
|
||||
* @param seed - The seed to use for the engine.
|
||||
* @returns A mock engine instance.
|
||||
*/
|
||||
export const createMockEngine = async (seed: string) => {
|
||||
// Create the in-memory storage adapter.
|
||||
const storage = await createStorageAdapter({
|
||||
storageType: StorageType.INMEMORY,
|
||||
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
|
||||
});
|
||||
|
||||
// Initialize the storage adapter.
|
||||
await storage.initialize();
|
||||
|
||||
// Create the state instance.
|
||||
const state = new State(storage);
|
||||
|
||||
// Create the in-memory blockchain provider.
|
||||
const blockchainProvider = new InMemoryBlockchainProvider();
|
||||
await blockchainProvider.initialize({
|
||||
applicationIdentifier: "xo-cli-tests",
|
||||
electrumOptions: {},
|
||||
});
|
||||
|
||||
// Create the blockchain monitor instance.
|
||||
const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider);
|
||||
await blockchainMonitor.initializeEventListeners();
|
||||
|
||||
// Create the engine instance.
|
||||
const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider);
|
||||
await engine.initializeStateSync();
|
||||
|
||||
return engine;
|
||||
};
|
||||
|
||||
export const createMockAppService = async (engine: Engine) => {
|
||||
const storage = await InMemoryStorage.create();
|
||||
|
||||
const electrum = new MockElectrumService();
|
||||
|
||||
const config = {
|
||||
syncServerUrl: "http://localhost:3000",
|
||||
engineConfig: {
|
||||
databasePath: "test-data",
|
||||
databaseFilename: "xo-wallet.db",
|
||||
},
|
||||
invitationStoragePath: "test-invitations.db",
|
||||
};
|
||||
|
||||
return new AppService(engine, storage, config, electrum);
|
||||
};
|
||||
1395
tests/cli/mocks/template-p2pkh.ts
Normal file
1395
tests/cli/mocks/template-p2pkh.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user