Formatting

This commit is contained in:
2026-04-20 12:26:35 +00:00
parent 32c42cdc2d
commit dbfb2c68d2
32 changed files with 3557 additions and 1828 deletions

View File

@@ -73,11 +73,19 @@ describe("command handler contracts", () => {
const { io } = createMockIO();
await expect(
handleResourceCommand(createCommandDeps(fakeApp, io), ["does-not-exist"], {}),
handleResourceCommand(
createCommandDeps(fakeApp, io),
["does-not-exist"],
{},
),
).rejects.toThrow(CommandError);
try {
await handleResourceCommand(createCommandDeps(fakeApp, io), ["does-not-exist"], {});
await handleResourceCommand(
createCommandDeps(fakeApp, io),
["does-not-exist"],
{},
);
} catch (error) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe("resource.subcommand.unknown");

View File

@@ -11,18 +11,40 @@
*/
import { expect, test, describe, beforeEach, afterEach } from "vitest";
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import {
existsSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { addFakeResource, createMockAppService, createMockEngine, DEFAULT_SEED, randomTxHash } from "../mocks/engine";
import {
addFakeResource,
createMockAppService,
createMockEngine,
DEFAULT_SEED,
randomTxHash,
} from "../mocks/engine";
import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate, p2pkhTemplateIdentifier } from "../mocks/template-p2pkh";
import {
p2pkhTemplate,
p2pkhTemplateIdentifier,
} from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app";
import { handleInvitationCommand } from "../../../src/cli/commands/invitation";
import { CommandError, CommandPaths } from "../../../src/cli/commands/types";
import { createCommandDeps, createMockIO, createMockPaths, expectLogs, type LogExpectation } from "../mocks/command";
import {
createCommandDeps,
createMockIO,
createMockPaths,
expectLogs,
type LogExpectation,
} from "../mocks/command";
// ============================================================================
// Error Cases - Validate argument parsing and error handling
@@ -150,7 +172,11 @@ describe("invitation command - error cases", () => {
const { io } = createMockIO();
try {
await handleInvitationCommand(createCommandDeps(app, io, paths), inputs, {});
await handleInvitationCommand(
createCommandDeps(app, io, paths),
inputs,
{},
);
expect.fail("Expected command to throw");
} catch (error) {
expect(error).toBeInstanceOf(CommandError);
@@ -211,7 +237,10 @@ describe("invitation command - receive flow", () => {
{},
);
const expectedFile = path.join(tempDir, `inv-${result.invitationIdentifier}.json`);
const expectedFile = path.join(
tempDir,
`inv-${result.invitationIdentifier}.json`,
);
expect(existsSync(expectedFile)).toBe(true);
});
@@ -325,8 +354,12 @@ describe("invitation command - request satoshis flow", () => {
);
expect(invitation).toBeDefined();
const variables = invitation?.data.commits.flatMap((c) => c.data.variables ?? []);
const requestedSatoshis = variables?.find((v) => v.variableIdentifier === "requestedSatoshis");
const variables = invitation?.data.commits.flatMap(
(c) => c.data.variables ?? [],
);
const requestedSatoshis = variables?.find(
(v) => v.variableIdentifier === "requestedSatoshis",
);
expect(requestedSatoshis).toBeDefined();
expect(requestedSatoshis?.value).toBe("10000");
});
@@ -347,8 +380,12 @@ describe("invitation command - request satoshis flow", () => {
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
);
const variables = invitation?.data.commits.flatMap((c) => c.data.variables ?? []);
const requestedSatoshis = variables?.find((v) => v.variableIdentifier === "requestedSatoshis");
const variables = invitation?.data.commits.flatMap(
(c) => c.data.variables ?? [],
);
const requestedSatoshis = variables?.find(
(v) => v.variableIdentifier === "requestedSatoshis",
);
expect(requestedSatoshis?.roleIdentifier).toBe("receiver");
});
});
@@ -388,7 +425,8 @@ describe("invitation command - send flow with resources", () => {
["create", "Wallet (P2PKH)", "sendSatoshis"],
{
varTransferredSatoshis: "10000",
varRecipientLockingscript: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
role: "sender",
},
);
@@ -408,7 +446,8 @@ describe("invitation command - send flow with resources", () => {
["create", "Wallet (P2PKH)", "sendSatoshis"],
{
varTransferredSatoshis: "10000",
varRecipientLockingscript: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
role: "sender",
},
);
@@ -418,8 +457,12 @@ describe("invitation command - send flow with resources", () => {
);
expect(invitation).toBeDefined();
const variables = invitation?.data.commits.flatMap((c) => c.data.variables ?? []);
const transferredSatoshis = variables?.find((v) => v.variableIdentifier === "transferredSatoshis");
const variables = invitation?.data.commits.flatMap(
(c) => c.data.variables ?? [],
);
const transferredSatoshis = variables?.find(
(v) => v.variableIdentifier === "transferredSatoshis",
);
expect(transferredSatoshis).toBeDefined();
expect(transferredSatoshis?.value).toBe("10000");
});
@@ -436,8 +479,10 @@ describe("invitation command - send flow with resources", () => {
["create", "Wallet (P2PKH)", "sendSatoshis"],
{
varTransferredSatoshis: "10000",
varRecipientLockingscript: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
addInput: "0000000000000000000000000000000000000000000000000000000000000000:0",
varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
addInput:
"0000000000000000000000000000000000000000000000000000000000000000:0",
role: "sender",
},
);
@@ -516,10 +561,15 @@ describe("invitation command - multi-step append", () => {
expectLogs(spies, [{ out: "Invitation appended" }]);
const invitation = app.invitations.find(
(inv) => inv.data.invitationIdentifier === createResult.invitationIdentifier,
(inv) =>
inv.data.invitationIdentifier === createResult.invitationIdentifier,
);
const variables = invitation?.data.commits.flatMap(
(c) => c.data.variables ?? [],
);
const requestedSatoshis = variables?.find(
(v) => v.variableIdentifier === "requestedSatoshis",
);
const variables = invitation?.data.commits.flatMap((c) => c.data.variables ?? []);
const requestedSatoshis = variables?.find((v) => v.variableIdentifier === "requestedSatoshis");
expect(requestedSatoshis?.value).toBe("25000");
});
@@ -569,7 +619,10 @@ describe("invitation command - multi-step append", () => {
expectLogs(spies, [{ out: "Invitation updated" }]);
const expectedFile = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const expectedFile = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
expect(existsSync(expectedFile)).toBe(true);
});
@@ -594,12 +647,17 @@ describe("invitation command - multi-step append", () => {
);
const invitation = app.invitations.find(
(inv) => inv.data.invitationIdentifier === createResult.invitationIdentifier,
(inv) =>
inv.data.invitationIdentifier === createResult.invitationIdentifier,
);
expect(invitation?.data.commits.length).toBeGreaterThan(1);
const variables = invitation?.data.commits.flatMap((c) => c.data.variables ?? []);
const requestedSatoshis = variables?.find((v) => v.variableIdentifier === "requestedSatoshis");
const variables = invitation?.data.commits.flatMap(
(c) => c.data.variables ?? [],
);
const requestedSatoshis = variables?.find(
(v) => v.variableIdentifier === "requestedSatoshis",
);
expect(requestedSatoshis?.value).toBe("10000");
});
});
@@ -724,7 +782,10 @@ describe("invitation command - list and inspect", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
const { io: inspectIO } = createMockIO();
@@ -813,7 +874,9 @@ describe("invitation command - sign flow", () => {
{},
);
expect(signResult.invitationIdentifier).toBe(createResult.invitationIdentifier);
expect(signResult.invitationIdentifier).toBe(
createResult.invitationIdentifier,
);
expectLogs(spies, [{ out: "Invitation signed" }]);
});
@@ -840,7 +903,8 @@ describe("invitation command - sign flow", () => {
);
const invitation = app.invitations.find(
(inv) => inv.data.invitationIdentifier === createResult.invitationIdentifier,
(inv) =>
inv.data.invitationIdentifier === createResult.invitationIdentifier,
);
expect(invitation).toBeDefined();
@@ -921,7 +985,10 @@ describe("invitation command - import flow", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
const secondApp = await createMockAppService(engine);
const { io: importIO } = createMockIO();
@@ -965,7 +1032,10 @@ describe("invitation command - import flow", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
const secondApp = await createMockAppService(engine);
const { io: importIO } = createMockIO();
@@ -991,7 +1061,10 @@ describe("invitation command - import flow", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
const secondApp = await createMockAppService(engine);
const { io: importIO } = createMockIO();
@@ -1044,7 +1117,8 @@ describe("invitation command - auto-inputs flow", () => {
["create", "Wallet (P2PKH)", "sendSatoshis"],
{
varTransferredSatoshis: "10000",
varRecipientLockingscript: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
role: "sender",
autoInputs: "true",
},
@@ -1052,7 +1126,9 @@ describe("invitation command - auto-inputs flow", () => {
expect.fail("Expected command to throw");
} catch (error) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe("invitation.create.append_params_failed");
expect((error as CommandError).event).toBe(
"invitation.create.append_params_failed",
);
}
});
@@ -1068,7 +1144,8 @@ describe("invitation command - auto-inputs flow", () => {
["create", "Wallet (P2PKH)", "sendSatoshis"],
{
varTransferredSatoshis: "10000",
varRecipientLockingscript: "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
role: "sender",
autoInputs: "true",
},
@@ -1117,7 +1194,9 @@ describe("invitation command - broadcast flow", () => {
expect.fail("Expected command to throw");
} catch (error) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe("invitation.broadcast.not_found");
expect((error as CommandError).event).toBe(
"invitation.broadcast.not_found",
);
}
});
@@ -1188,11 +1267,14 @@ describe("invitation command - full lifecycle", () => {
{},
);
expect(signResult.invitationIdentifier).toBe(createResult.invitationIdentifier);
expect(signResult.invitationIdentifier).toBe(
createResult.invitationIdentifier,
);
expectLogs(signSpies, [{ out: "Invitation signed" }]);
const invitation = app.invitations.find(
(inv) => inv.data.invitationIdentifier === createResult.invitationIdentifier,
(inv) =>
inv.data.invitationIdentifier === createResult.invitationIdentifier,
);
expect(invitation).toBeDefined();
expect(invitation?.data.commits.length).toBeGreaterThan(0);
@@ -1210,7 +1292,10 @@ describe("invitation command - full lifecycle", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
expect(existsSync(invitationFilePath)).toBe(true);
const { io: inspectIO } = createMockIO();
@@ -1254,7 +1339,9 @@ describe("invitation command - full lifecycle", () => {
{},
);
expect(signResult.invitationIdentifier).toBe(createResult.invitationIdentifier);
expect(signResult.invitationIdentifier).toBe(
createResult.invitationIdentifier,
);
expectLogs(signSpies, [{ out: "Invitation signed" }]);
});
@@ -1270,7 +1357,10 @@ describe("invitation command - full lifecycle", () => {
{},
);
const invitationFilePath = path.join(tempDir, `inv-${createResult.invitationIdentifier}.json`);
const invitationFilePath = path.join(
tempDir,
`inv-${createResult.invitationIdentifier}.json`,
);
const afterCreate = JSON.parse(readFileSync(invitationFilePath, "utf-8"));
const createCommitCount = afterCreate.commits?.length ?? 0;
@@ -1335,7 +1425,9 @@ describe("invitation command - full lifecycle", () => {
{},
);
expect(reqResult.invitationIdentifier).toBe(createResult.invitationIdentifier);
expect(reqResult.invitationIdentifier).toBe(
createResult.invitationIdentifier,
);
expect(spies.out).toHaveBeenCalled();
});
});

View File

@@ -7,7 +7,12 @@ import { DEFAULT_SEED } from "../mocks/engine";
import { handleMnemonicCommand } from "../../../src/cli/commands/mnemonic";
import { CommandError } from "../../../src/cli/commands/types";
import { createMockIO, createMockPaths, expectLogs, type LogExpectation } from "../mocks/command";
import {
createMockIO,
createMockPaths,
expectLogs,
type LogExpectation,
} from "../mocks/command";
import { BCHMnemonicURL } from "../../../src/utils/bch-mnemonic-url";
type TestCase = {
@@ -103,7 +108,7 @@ describe("mnemonic commands", () => {
beforeEach(async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-mnemonic-tests-"));
// Write a single test mnemonic file to the temp directory
writeFileSync(
path.join(tempDir, "mnemonic-test"),
@@ -116,31 +121,45 @@ describe("mnemonic commands", () => {
rmSync(tempDir, { recursive: true, force: true });
});
test.each(testCases)("mnemonic command: $inputs", async ({ inputs, options, shouldThrow, expectedEvent, expectedData, logs }) => {
const { io, spies } = createMockIO();
const paths = createMockPaths(tempDir);
test.each(testCases)(
"mnemonic command: $inputs",
async ({
inputs,
options,
shouldThrow,
expectedEvent,
expectedData,
logs,
}) => {
const { io, spies } = createMockIO();
const paths = createMockPaths(tempDir);
if (shouldThrow) {
try {
await handleMnemonicCommand({ io, paths }, inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
if (shouldThrow) {
try {
await handleMnemonicCommand({ io, paths }, inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
}
}
} else {
const result = await handleMnemonicCommand(
{ io, paths },
inputs,
options ?? {},
);
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
} else {
const result = await handleMnemonicCommand({ io, paths }, inputs, options ?? {});
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
if (logs) {
expectLogs(spies, logs);
}
});
});
if (logs) {
expectLogs(spies, logs);
}
},
);
});

View File

@@ -3,14 +3,23 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { createMockAppService, createMockEngine, DEFAULT_SEED } from "../mocks/engine";
import {
createMockAppService,
createMockEngine,
DEFAULT_SEED,
} from "../mocks/engine";
import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate } from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app";
import { handleReceiveCommand } from "../../../src/cli/commands/receive";
import { CommandError } from "../../../src/cli/commands/types";
import { createCommandDeps, createMockIO, expectLogs, type LogExpectation } from "../mocks/command";
import {
createCommandDeps,
createMockIO,
expectLogs,
type LogExpectation,
} from "../mocks/command";
type TestCase = {
name: string;
@@ -85,30 +94,48 @@ describe("receive command", () => {
rmSync(tempDir, { recursive: true, force: true });
});
test.each(testCases)("$name", async ({ inputs, options, shouldThrow, expectedEvent, expectedData, logs }) => {
const { io, spies } = createMockIO();
test.each(testCases)(
"$name",
async ({
inputs,
options,
shouldThrow,
expectedEvent,
expectedData,
logs,
}) => {
const { io, spies } = createMockIO();
if (shouldThrow) {
try {
await handleReceiveCommand(createCommandDeps(app, io), inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
if (shouldThrow) {
try {
await handleReceiveCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
}
}
} else {
const result = await handleReceiveCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
} else {
const result = await handleReceiveCommand(createCommandDeps(app, io), inputs, options ?? {});
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
if (logs) {
expectLogs(spies, logs);
}
});
if (logs) {
expectLogs(spies, logs);
}
},
);
});

View File

@@ -3,14 +3,25 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { addFakeResource, createMockAppService, createMockEngine, DEFAULT_SEED, reserveResource } from "../mocks/engine";
import {
addFakeResource,
createMockAppService,
createMockEngine,
DEFAULT_SEED,
reserveResource,
} from "../mocks/engine";
import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate } from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app";
import { handleResourceCommand } from "../../../src/cli/commands/resource";
import { CommandError } from "../../../src/cli/commands/types";
import { createCommandDeps, createMockIO, expectLogs, type LogExpectation } from "../mocks/command";
import {
createCommandDeps,
createMockIO,
expectLogs,
type LogExpectation,
} from "../mocks/command";
type TestCase = {
name: string;
@@ -94,7 +105,10 @@ const testCases: TestCase[] = [
},
{
name: "throws when unreserve called with non-existent UTXO",
inputs: ["unreserve", "0000000000000000000000000000000000000000000000000000000000000000:0"],
inputs: [
"unreserve",
"0000000000000000000000000000000000000000000000000000000000000000:0",
],
shouldThrow: true,
expectedEvent: "resource.unreserve.utxo_missing",
},
@@ -119,32 +133,50 @@ describe("resource command", () => {
rmSync(tempDir, { recursive: true, force: true });
});
test.each(testCases)("$name", async ({ inputs, options, shouldThrow, expectedEvent, expectedData, logs }) => {
const { io, spies } = createMockIO();
test.each(testCases)(
"$name",
async ({
inputs,
options,
shouldThrow,
expectedEvent,
expectedData,
logs,
}) => {
const { io, spies } = createMockIO();
if (shouldThrow) {
try {
await handleResourceCommand(createCommandDeps(app, io), inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
if (shouldThrow) {
try {
await handleResourceCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
}
}
} else {
const result = await handleResourceCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
} else {
const result = await handleResourceCommand(createCommandDeps(app, io), inputs, options ?? {});
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
if (logs) {
expectLogs(spies, logs);
}
});
if (logs) {
expectLogs(spies, logs);
}
},
);
});
describe("resource command with populated data", () => {
@@ -169,7 +201,11 @@ describe("resource command with populated data", () => {
await addFakeResource(engine, { valueSatoshis: 25000 });
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
const result = await handleResourceCommand(
createCommandDeps(app, io),
["list"],
{},
);
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "Total resources: 2" }]);
@@ -187,21 +223,38 @@ describe("resource command with populated data", () => {
test("list excludes reserved resources by default", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, {
valueSatoshis: 25000,
reservedBy: "inv-123",
});
const { io } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
const result = await handleResourceCommand(
createCommandDeps(app, io),
["list"],
{},
);
expect(result.count).toBe(1);
});
test("list reserved shows only reserved resources", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, { valueSatoshis: 10000, reservedBy: "inv-456" });
await addFakeResource(engine, {
valueSatoshis: 25000,
reservedBy: "inv-123",
});
await addFakeResource(engine, {
valueSatoshis: 10000,
reservedBy: "inv-456",
});
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list", "reserved"], {});
const result = await handleResourceCommand(
createCommandDeps(app, io),
["list", "reserved"],
{},
);
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "reserved for inv-123" }]);
@@ -209,29 +262,45 @@ describe("resource command with populated data", () => {
test("list all shows both reserved and unreserved", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, {
valueSatoshis: 25000,
reservedBy: "inv-123",
});
const { io } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list", "all"], {});
const result = await handleResourceCommand(
createCommandDeps(app, io),
["list", "all"],
{},
);
expect(result.count).toBe(2);
});
test("unreserve releases a reserved UTXO", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
const resource = await addFakeResource(engine, {
valueSatoshis: 25000,
reservedBy: "inv-123",
});
const { io, spies } = createMockIO();
await handleResourceCommand(
createCommandDeps(app, io),
["unreserve", `${resource.outpointTransactionHash}:${resource.outpointIndex}`],
[
"unreserve",
`${resource.outpointTransactionHash}:${resource.outpointIndex}`,
],
{},
);
expectLogs(spies, [{ out: "Unreserved" }, { out: "was reserved for inv-123" }]);
expectLogs(spies, [
{ out: "Unreserved" },
{ out: "was reserved for inv-123" },
]);
const resources = await engine.listUnspentOutputsData();
const target = resources.find(
r => r.outpointTransactionHash === resource.outpointTransactionHash,
(r) => r.outpointTransactionHash === resource.outpointTransactionHash,
);
expect(target?.reservedBy).toBeUndefined();
});
@@ -242,7 +311,10 @@ describe("resource command with populated data", () => {
const { io, spies } = createMockIO();
await handleResourceCommand(
createCommandDeps(app, io),
["unreserve", `${resource.outpointTransactionHash}:${resource.outpointIndex}`],
[
"unreserve",
`${resource.outpointTransactionHash}:${resource.outpointIndex}`,
],
{},
);
@@ -251,17 +323,27 @@ describe("resource command with populated data", () => {
test("unreserve-all releases all reserved UTXOs", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, { valueSatoshis: 10000, reservedBy: "inv-456" });
await addFakeResource(engine, {
valueSatoshis: 25000,
reservedBy: "inv-123",
});
await addFakeResource(engine, {
valueSatoshis: 10000,
reservedBy: "inv-456",
});
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["unreserve-all"], {});
const result = await handleResourceCommand(
createCommandDeps(app, io),
["unreserve-all"],
{},
);
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "Unreserved" }, { out: "2" }]);
const resources = await engine.listUnspentOutputsData();
const reserved = resources.filter(r => r.reservedBy);
const reserved = resources.filter((r) => r.reservedBy);
expect(reserved).toHaveLength(0);
});

View File

@@ -3,14 +3,26 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { createMockAppService, createMockEngine, DEFAULT_SEED } from "../mocks/engine";
import {
createMockAppService,
createMockEngine,
DEFAULT_SEED,
} from "../mocks/engine";
import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate, p2pkhTemplateIdentifier } from "../mocks/template-p2pkh";
import {
p2pkhTemplate,
p2pkhTemplateIdentifier,
} from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app";
import { handleTemplateCommand } from "../../../src/cli/commands/template";
import { CommandError } from "../../../src/cli/commands/types";
import { createCommandDeps, createMockIO, expectLogs, type LogExpectation } from "../mocks/command";
import {
createCommandDeps,
createMockIO,
expectLogs,
type LogExpectation,
} from "../mocks/command";
type TestCase = {
name: string;
@@ -184,42 +196,60 @@ describe("template command", () => {
rmSync(tempDir, { recursive: true, force: true });
});
test.each(testCases)("$name", async ({ inputs, options, shouldThrow, expectedEvent, expectedData, logs }) => {
const { io, spies } = createMockIO();
test.each(testCases)(
"$name",
async ({
inputs,
options,
shouldThrow,
expectedEvent,
expectedData,
logs,
}) => {
const { io, spies } = createMockIO();
if (shouldThrow) {
try {
await handleTemplateCommand(createCommandDeps(app, io), inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
if (shouldThrow) {
try {
await handleTemplateCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
}
}
} else {
const result = await handleTemplateCommand(
createCommandDeps(app, io),
inputs,
options ?? {},
);
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
} else {
const result = await handleTemplateCommand(createCommandDeps(app, io), inputs, options ?? {});
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
if (logs) {
expectLogs(spies, logs);
}
});
if (logs) {
expectLogs(spies, logs);
}
},
);
test("import imports template from file", async () => {
const templatePath = path.join(tempDir, "test-template.json");
writeFileSync(templatePath, JSON.stringify(p2pkhTemplate));
const { io } = createMockIO();
const originalCwd = process.cwd();
process.chdir(tempDir);
try {
const result = await handleTemplateCommand(
createCommandDeps(app, io),