1448 lines
42 KiB
TypeScript
1448 lines
42 KiB
TypeScript
/**
|
|
* Comprehensive tests for the invitation CLI command.
|
|
*
|
|
* These tests are organized by flow complexity:
|
|
* 1. Error cases - Validate argument parsing and error handling
|
|
* 2. Receive flow - Simple invitation to receive funds
|
|
* 3. Request satoshis flow - Invitation with variables
|
|
* 4. Send flow - Requires UTXOs (uses addFakeResource)
|
|
* 5. Multi-step append flow - Incremental building of invitations
|
|
* 6. List and inspect - Viewing invitations
|
|
*/
|
|
|
|
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
|
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 { type Engine } from "@xo-cash/engine";
|
|
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 { State } from "@xo-cash/state";
|
|
|
|
// ============================================================================
|
|
// Error Cases - Validate argument parsing and error handling
|
|
// ============================================================================
|
|
|
|
type ErrorTestCase = {
|
|
name: string;
|
|
inputs: string[];
|
|
expectedEvent: string;
|
|
};
|
|
|
|
const errorTestCases: ErrorTestCase[] = [
|
|
// Subcommand errors
|
|
{
|
|
name: "throws when no subcommand provided",
|
|
inputs: [],
|
|
expectedEvent: "invitation.subcommand.missing",
|
|
},
|
|
{
|
|
name: "throws when unknown subcommand provided",
|
|
inputs: ["unknown-subcommand"],
|
|
expectedEvent: "invitation.subcommand.unknown",
|
|
},
|
|
|
|
// Create errors
|
|
{
|
|
name: "create throws when no arguments provided",
|
|
inputs: ["create"],
|
|
expectedEvent: "invitation.create.arguments_missing",
|
|
},
|
|
{
|
|
name: "create throws when only template provided (missing action)",
|
|
inputs: ["create", "Wallet (P2PKH)"],
|
|
expectedEvent: "invitation.create.arguments_missing",
|
|
},
|
|
{
|
|
name: "create throws when template not found",
|
|
inputs: ["create", "unknown-template", "receive"],
|
|
expectedEvent: "template.resolve.not_found",
|
|
},
|
|
|
|
// Append errors
|
|
{
|
|
name: "append throws when no invitation ID provided",
|
|
inputs: ["append"],
|
|
expectedEvent: "invitation.append.identifier_missing",
|
|
},
|
|
{
|
|
name: "append throws when invitation not found",
|
|
inputs: ["append", "nonexistent-invitation-id"],
|
|
expectedEvent: "invitation.append.not_found",
|
|
},
|
|
|
|
// Sign errors
|
|
{
|
|
name: "sign throws when no invitation ID provided",
|
|
inputs: ["sign"],
|
|
expectedEvent: "invitation.sign.identifier_missing",
|
|
},
|
|
{
|
|
name: "sign throws when invitation not found",
|
|
inputs: ["sign", "nonexistent-invitation-id"],
|
|
expectedEvent: "invitation.sign.not_found",
|
|
},
|
|
|
|
// Broadcast errors
|
|
{
|
|
name: "broadcast throws when no invitation ID provided",
|
|
inputs: ["broadcast"],
|
|
expectedEvent: "invitation.broadcast.identifier_missing",
|
|
},
|
|
{
|
|
name: "broadcast throws when invitation not found",
|
|
inputs: ["broadcast", "nonexistent-invitation-id"],
|
|
expectedEvent: "invitation.broadcast.not_found",
|
|
},
|
|
|
|
// Requirements errors
|
|
{
|
|
name: "requirements throws when no invitation ID provided",
|
|
inputs: ["requirements"],
|
|
expectedEvent: "invitation.requirements.identifier_missing",
|
|
},
|
|
{
|
|
name: "requirements throws when invitation not found",
|
|
inputs: ["requirements", "nonexistent-invitation-id"],
|
|
expectedEvent: "invitation.requirements.not_found",
|
|
},
|
|
|
|
// Import errors
|
|
{
|
|
name: "import throws when no file provided",
|
|
inputs: ["import"],
|
|
expectedEvent: "invitation.import.file_missing",
|
|
},
|
|
|
|
// Inspect errors
|
|
{
|
|
name: "inspect throws when no file provided",
|
|
inputs: ["inspect"],
|
|
expectedEvent: "invitation.inspect.file_missing",
|
|
},
|
|
];
|
|
|
|
describe("invitation command - error cases", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-errors-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test.each(errorTestCases)("$name", async ({ inputs, expectedEvent }) => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
inputs,
|
|
{},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(CommandError);
|
|
expect((error as CommandError).event).toBe(expectedEvent);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Receive Flow - Simple invitation to receive funds
|
|
// ============================================================================
|
|
|
|
describe("invitation command - receive flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-receive-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that a basic receive invitation can be created.
|
|
* The receive action generates an address for receiving funds.
|
|
*/
|
|
test("creates receive invitation successfully", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
expectLogs(spies, [{ out: "Invitation created" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that an invitation file is written to the working directory.
|
|
*/
|
|
test("writes invitation file to working directory", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const expectedFile = path.join(
|
|
tempDir,
|
|
`inv-${result.invitationIdentifier}.json`,
|
|
);
|
|
expect(existsSync(expectedFile)).toBe(true);
|
|
});
|
|
|
|
/**
|
|
* Tests that the invitation is tracked by the app service.
|
|
*/
|
|
test("invitation is tracked by app service", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const tracked = app.invitations.find(
|
|
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
|
|
);
|
|
expect(tracked).toBeDefined();
|
|
expect(tracked?.data.actionIdentifier).toBe("receive");
|
|
});
|
|
|
|
/**
|
|
* Tests that appropriate message is shown after creation.
|
|
* The receive action for receiver role is self-complete, so it shows "All requirements satisfied".
|
|
*/
|
|
test("shows completion message after creation", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
expectLogs(spies, [{ out: "All requirements satisfied" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that the invitation identifier is returned in the result.
|
|
*/
|
|
test("returns invitation identifier in result", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
expect(result).toHaveProperty("invitationIdentifier");
|
|
expect(typeof result.invitationIdentifier).toBe("string");
|
|
expect(result.invitationIdentifier).toHaveLength(32);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Request Satoshis Flow - Invitation with variables
|
|
// ============================================================================
|
|
|
|
describe("invitation command - request satoshis flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-request-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests creating an invitation with a variable via -var-requested-satoshis.
|
|
* The requestSatoshis action requires the receiver to specify an amount.
|
|
*/
|
|
test("creates invitation with variable", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
expectLogs(spies, [{ out: "Invitation created" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that the variable is committed to the invitation.
|
|
*/
|
|
test("variable is committed to invitation", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
|
|
);
|
|
expect(invitation).toBeDefined();
|
|
|
|
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");
|
|
});
|
|
|
|
/**
|
|
* Tests that the role identifier is attached to the variable.
|
|
*/
|
|
test("role is attached to variable", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
|
|
);
|
|
|
|
const variables = invitation?.data.commits.flatMap(
|
|
(c) => c.data.variables ?? [],
|
|
);
|
|
const requestedSatoshis = variables?.find(
|
|
(v) => v.variableIdentifier === "requestedSatoshis",
|
|
);
|
|
expect(requestedSatoshis?.roleIdentifier).toBe("receiver");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Send Flow - Requires UTXOs (uses addFakeResource)
|
|
// ============================================================================
|
|
|
|
describe("invitation command - send flow with resources", () => {
|
|
let engine: Engine;
|
|
let state: State;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
state = mockEngine.state;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-send-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that sendSatoshis invitation can be created with variables.
|
|
* Note: This doesn't add inputs yet, just tests variable handling.
|
|
*/
|
|
test("creates sendSatoshis invitation with variables", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "sendSatoshis"],
|
|
{
|
|
varTransferredSatoshis: "10000",
|
|
varRecipientLockingscript:
|
|
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
|
|
role: "sender",
|
|
},
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
expectLogs(spies, [{ out: "Invitation created" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that variables are committed to sendSatoshis invitation.
|
|
*/
|
|
test("variables are committed to sendSatoshis invitation", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "sendSatoshis"],
|
|
{
|
|
varTransferredSatoshis: "10000",
|
|
varRecipientLockingscript:
|
|
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
|
|
role: "sender",
|
|
},
|
|
);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
|
|
);
|
|
expect(invitation).toBeDefined();
|
|
|
|
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");
|
|
});
|
|
|
|
/**
|
|
* Tests that an error is thrown when the specified UTXO doesn't exist.
|
|
*/
|
|
test("throws when specified UTXO not found", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "sendSatoshis"],
|
|
{
|
|
varTransferredSatoshis: "10000",
|
|
varRecipientLockingscript:
|
|
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
|
|
addInput:
|
|
"0000000000000000000000000000000000000000000000000000000000000000:0",
|
|
role: "sender",
|
|
},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(CommandError);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tests that fake resources can be added and listed.
|
|
* This validates our test infrastructure works correctly.
|
|
*/
|
|
test("fake resources are accessible via engine", async () => {
|
|
const resource = await addFakeResource(state!, {
|
|
valueSatoshis: 50000,
|
|
templateIdentifier: p2pkhTemplateIdentifier,
|
|
outputIdentifier: "receiveOutput",
|
|
});
|
|
|
|
const allResources = await engine.listUnspentOutputsData();
|
|
const found = allResources.find(
|
|
(r) =>
|
|
r.outpointTransactionHash === resource.outpointTransactionHash &&
|
|
r.outpointIndex === resource.outpointIndex,
|
|
);
|
|
|
|
expect(found).toBeDefined();
|
|
expect(found?.valueSatoshis).toBe(50000);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Multi-Step Append Flow - Incremental building of invitations
|
|
// ============================================================================
|
|
|
|
describe("invitation command - multi-step append", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-append-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests appending variables to an existing invitation.
|
|
*/
|
|
test("appends variables to existing invitation", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: appendIO, spies } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, appendIO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "25000", role: "receiver" },
|
|
);
|
|
|
|
expectLogs(spies, [{ out: "Invitation appended" }]);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) =>
|
|
inv.data.invitationIdentifier === createResult.invitationIdentifier,
|
|
);
|
|
const variables = invitation?.data.commits.flatMap(
|
|
(c) => c.data.variables ?? [],
|
|
);
|
|
const requestedSatoshis = variables?.find(
|
|
(v) => v.variableIdentifier === "requestedSatoshis",
|
|
);
|
|
expect(requestedSatoshis?.value).toBe("25000");
|
|
});
|
|
|
|
/**
|
|
* Tests that append succeeds when only variables are provided.
|
|
* When outputs are auto-discovered from the template, append can succeed with just variables.
|
|
*/
|
|
test("append succeeds with just variables on fresh invitation", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: appendIO, spies } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, appendIO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "5000", role: "receiver" },
|
|
);
|
|
|
|
expectLogs(spies, [{ out: "Invitation appended" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that the invitation file is updated after append.
|
|
*/
|
|
test("updates invitation file after append", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: appendIO, spies } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, appendIO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "25000", role: "receiver" },
|
|
);
|
|
|
|
expectLogs(spies, [{ out: "Invitation updated" }]);
|
|
|
|
const expectedFile = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
expect(existsSync(expectedFile)).toBe(true);
|
|
});
|
|
|
|
/**
|
|
* Tests that multiple append operations can add variables incrementally.
|
|
* Uses requestSatoshis which has straightforward requirements.
|
|
*/
|
|
test("multiple appends add variables incrementally", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: append1IO } = createMockIO();
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, append1IO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
const invitation = app.invitations.find(
|
|
(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",
|
|
);
|
|
expect(requestedSatoshis?.value).toBe("10000");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// List and Inspect - Viewing invitations
|
|
// ============================================================================
|
|
|
|
describe("invitation command - list and inspect", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-list-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that list returns count of created invitations.
|
|
*/
|
|
test("list returns count of invitations", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: listIO } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, listIO, paths),
|
|
["list"],
|
|
{},
|
|
);
|
|
|
|
expect(result.count).toBe(1);
|
|
});
|
|
|
|
/**
|
|
* Tests that list shows template name.
|
|
*/
|
|
test("list shows template name", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: listIO, spies } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, listIO, paths),
|
|
["list"],
|
|
{},
|
|
);
|
|
|
|
expectLogs(spies, [{ out: "Wallet (P2PKH)" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that list returns zero when no invitations exist.
|
|
*/
|
|
test("list returns zero when no invitations", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["list"],
|
|
{},
|
|
);
|
|
|
|
expect(result.count).toBe(0);
|
|
});
|
|
|
|
/**
|
|
* Tests that requirements shows invitation requirements.
|
|
*/
|
|
test("requirements shows invitation requirements", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: reqIO, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, reqIO, paths),
|
|
["requirements", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toBe(createResult.invitationIdentifier);
|
|
expect(spies.out).toHaveBeenCalled();
|
|
});
|
|
|
|
/**
|
|
* Tests that inspect reads an invitation file and shows details.
|
|
*/
|
|
test("inspect reads invitation file", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
|
|
const { io: inspectIO } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, inspectIO, paths),
|
|
["inspect", invitationFilePath],
|
|
{},
|
|
);
|
|
|
|
expect(result.templateName).toBe("Wallet (P2PKH)");
|
|
expect(result.actionIdentifier).toBe("receive");
|
|
expect(result.status).toBeDefined();
|
|
});
|
|
|
|
/**
|
|
* Tests that multiple invitations can be listed.
|
|
*/
|
|
test("list shows multiple invitations", async () => {
|
|
const { io: create1IO } = createMockIO();
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, create1IO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: create2IO } = createMockIO();
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, create2IO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: listIO } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, listIO, paths),
|
|
["list"],
|
|
{},
|
|
);
|
|
|
|
expect(result.count).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Sign Flow - Signing invitations
|
|
// ============================================================================
|
|
|
|
describe("invitation command - sign flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-sign-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that a satisfied invitation can be signed.
|
|
* The receive action is self-complete, so it can be signed immediately.
|
|
*/
|
|
test("signs a satisfied invitation", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: signIO, spies } = createMockIO();
|
|
|
|
const signResult = await handleInvitationCommand(
|
|
createCommandDeps(app, signIO, paths),
|
|
["sign", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
expect(signResult.invitationIdentifier).toBe(
|
|
createResult.invitationIdentifier,
|
|
);
|
|
expectLogs(spies, [{ out: "Invitation signed" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that the invitation has signed commits after signing.
|
|
* Note: Status check depends on internal Invitation class logic which
|
|
* may require specific conditions (inputs with mergesWith and unlockingBytecode).
|
|
*/
|
|
test("invitation has signed commits after signing", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const { io: signIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, signIO, paths),
|
|
["sign", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) =>
|
|
inv.data.invitationIdentifier === createResult.invitationIdentifier,
|
|
);
|
|
|
|
expect(invitation).toBeDefined();
|
|
expect(invitation?.data.commits.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
/**
|
|
* Tests the --sign flag on create auto-signs when requirements are satisfied.
|
|
*/
|
|
test("--sign flag auto-signs on create", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{ sign: "true" },
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
expectLogs(spies, [
|
|
{ out: "Invitation created" },
|
|
{ out: "Invitation signed" },
|
|
]);
|
|
});
|
|
|
|
/**
|
|
* Tests that signing fails when invitation doesn't exist.
|
|
*/
|
|
test("sign throws when invitation not found", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["sign", "nonexistent-id"],
|
|
{},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(CommandError);
|
|
expect((error as CommandError).event).toBe("invitation.sign.not_found");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Import Flow - Importing invitation files
|
|
// ============================================================================
|
|
|
|
describe("invitation command - import flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-import-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that an invitation file can be imported.
|
|
*/
|
|
test("imports invitation from file", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
|
|
const secondApp = await createMockAppService(engine);
|
|
const { io: importIO } = createMockIO();
|
|
|
|
const importResult = await handleInvitationCommand(
|
|
createCommandDeps(secondApp, importIO, paths),
|
|
["import", invitationFilePath],
|
|
{},
|
|
);
|
|
|
|
expect(importResult.invitationIdentifier).toBeDefined();
|
|
});
|
|
|
|
/**
|
|
* Tests that import fails when file doesn't exist.
|
|
*/
|
|
test("import throws when file not found", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["import", "/nonexistent/path/invitation.json"],
|
|
{},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeDefined();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tests that imported invitation is tracked by app service.
|
|
*/
|
|
test("imported invitation is tracked by app service", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
|
|
const secondApp = await createMockAppService(engine);
|
|
const { io: importIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(secondApp, importIO, paths),
|
|
["import", invitationFilePath],
|
|
{},
|
|
);
|
|
|
|
expect(secondApp.invitations.length).toBe(1);
|
|
});
|
|
|
|
/**
|
|
* Tests that an imported invitation retains its data.
|
|
*/
|
|
test("imported invitation retains action identifier", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
|
|
const secondApp = await createMockAppService(engine);
|
|
const { io: importIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(secondApp, importIO, paths),
|
|
["import", invitationFilePath],
|
|
{},
|
|
);
|
|
|
|
const imported = secondApp.invitations[0];
|
|
expect(imported.data.actionIdentifier).toBe("receive");
|
|
expect(imported.data.templateIdentifier).toBe(p2pkhTemplateIdentifier);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Auto-Inputs Flow - Automatic UTXO selection
|
|
// ============================================================================
|
|
|
|
describe("invitation command - auto-inputs flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-autoinputs-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that --auto-inputs flag triggers automatic UTXO selection.
|
|
* When no suitable UTXOs exist, it should fail gracefully.
|
|
*/
|
|
test("auto-inputs fails when no suitable UTXOs available", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "sendSatoshis"],
|
|
{
|
|
varTransferredSatoshis: "10000",
|
|
varRecipientLockingscript:
|
|
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
|
|
role: "sender",
|
|
autoInputs: "true",
|
|
},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(CommandError);
|
|
expect((error as CommandError).event).toBe(
|
|
"invitation.create.append_params_failed",
|
|
);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tests that auto-inputs logs when no UTXOs are found.
|
|
*/
|
|
test("auto-inputs logs error when no UTXOs found", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "sendSatoshis"],
|
|
{
|
|
varTransferredSatoshis: "10000",
|
|
varRecipientLockingscript:
|
|
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
|
|
role: "sender",
|
|
autoInputs: "true",
|
|
},
|
|
);
|
|
} catch (error) {
|
|
expectLogs(spies, [{ err: "No suitable UTXOs found" }]);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Broadcast Flow - Broadcasting invitations
|
|
// ============================================================================
|
|
|
|
describe("invitation command - broadcast flow", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-broadcast-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests that broadcast fails when invitation not found.
|
|
*/
|
|
test("broadcast throws when invitation not found", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["broadcast", "nonexistent-id"],
|
|
{},
|
|
);
|
|
expect.fail("Expected command to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(CommandError);
|
|
expect((error as CommandError).event).toBe(
|
|
"invitation.broadcast.not_found",
|
|
);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tests the --broadcast flag on create.
|
|
* Note: This will attempt to broadcast, which may fail in test environment
|
|
* without proper mocking, but the flag should be recognized.
|
|
*/
|
|
test("--broadcast flag is recognized on create", async () => {
|
|
const { io } = createMockIO();
|
|
|
|
try {
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{ broadcast: "true" },
|
|
);
|
|
} catch (error) {
|
|
expect((error as Error).message).not.toContain("unknown option");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Full Lifecycle - End-to-end flows
|
|
// ============================================================================
|
|
|
|
describe("invitation command - full lifecycle", () => {
|
|
let engine: Engine;
|
|
let app: AppService;
|
|
let tempDir: string;
|
|
let paths: CommandPaths;
|
|
|
|
beforeEach(async () => {
|
|
const mockEngine = await createMockEngine(DEFAULT_SEED);
|
|
engine = mockEngine.engine;
|
|
await engine.importTemplate(p2pkhTemplate);
|
|
app = await createMockAppService(engine);
|
|
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-lifecycle-"));
|
|
paths = createMockPaths(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await engine.stop();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Tests complete create → sign flow for receive action.
|
|
* The receive action is self-complete and can be signed immediately.
|
|
*/
|
|
test("create → sign flow for receive", async () => {
|
|
const { io: createIO, spies: createSpies } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
expect(createResult.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
expectLogs(createSpies, [{ out: "Invitation created" }]);
|
|
|
|
const { io: signIO, spies: signSpies } = createMockIO();
|
|
|
|
const signResult = await handleInvitationCommand(
|
|
createCommandDeps(app, signIO, paths),
|
|
["sign", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
expect(signResult.invitationIdentifier).toBe(
|
|
createResult.invitationIdentifier,
|
|
);
|
|
expectLogs(signSpies, [{ out: "Invitation signed" }]);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) =>
|
|
inv.data.invitationIdentifier === createResult.invitationIdentifier,
|
|
);
|
|
expect(invitation).toBeDefined();
|
|
expect(invitation?.data.commits.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
/**
|
|
* Tests that invitation file is persisted and can be inspected after creation.
|
|
*/
|
|
test("create → inspect round-trip", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
expect(existsSync(invitationFilePath)).toBe(true);
|
|
|
|
const { io: inspectIO } = createMockIO();
|
|
|
|
const inspectResult = await handleInvitationCommand(
|
|
createCommandDeps(app, inspectIO, paths),
|
|
["inspect", invitationFilePath],
|
|
{},
|
|
);
|
|
|
|
expect(inspectResult.templateName).toBe("Wallet (P2PKH)");
|
|
expect(inspectResult.actionIdentifier).toBe("receive");
|
|
});
|
|
|
|
/**
|
|
* Tests create → append → sign flow for requestSatoshis.
|
|
* First creates without variables, then appends them, then signs.
|
|
*/
|
|
test("create → append → sign flow for requestSatoshis", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: appendIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, appendIO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
const { io: signIO, spies: signSpies } = createMockIO();
|
|
|
|
const signResult = await handleInvitationCommand(
|
|
createCommandDeps(app, signIO, paths),
|
|
["sign", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
expect(signResult.invitationIdentifier).toBe(
|
|
createResult.invitationIdentifier,
|
|
);
|
|
expectLogs(signSpies, [{ out: "Invitation signed" }]);
|
|
});
|
|
|
|
/**
|
|
* Tests that invitation file is updated after each operation.
|
|
*/
|
|
test("invitation file is updated at each step", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const invitationFilePath = path.join(
|
|
tempDir,
|
|
`inv-${createResult.invitationIdentifier}.json`,
|
|
);
|
|
|
|
const afterCreate = JSON.parse(readFileSync(invitationFilePath, "utf-8"));
|
|
const createCommitCount = afterCreate.commits?.length ?? 0;
|
|
|
|
const { io: appendIO } = createMockIO();
|
|
|
|
await handleInvitationCommand(
|
|
createCommandDeps(app, appendIO, paths),
|
|
["append", createResult.invitationIdentifier!],
|
|
{ varRequestedSatoshis: "10000", role: "receiver" },
|
|
);
|
|
|
|
const afterAppend = JSON.parse(readFileSync(invitationFilePath, "utf-8"));
|
|
const appendCommitCount = afterAppend.commits?.length ?? 0;
|
|
|
|
expect(appendCommitCount).toBeGreaterThan(createCommitCount);
|
|
});
|
|
|
|
/**
|
|
* Tests create with --sign flag for immediate signing.
|
|
*/
|
|
test("create with --sign for immediate signing", async () => {
|
|
const { io, spies } = createMockIO();
|
|
|
|
const result = await handleInvitationCommand(
|
|
createCommandDeps(app, io, paths),
|
|
["create", "Wallet (P2PKH)", "receive"],
|
|
{ sign: "true" },
|
|
);
|
|
|
|
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
|
|
|
|
const invitation = app.invitations.find(
|
|
(inv) => inv.data.invitationIdentifier === result.invitationIdentifier,
|
|
);
|
|
expect(invitation).toBeDefined();
|
|
expect(invitation?.data.commits.length).toBeGreaterThan(0);
|
|
|
|
expectLogs(spies, [
|
|
{ out: "Invitation created" },
|
|
{ out: "Invitation signed" },
|
|
]);
|
|
});
|
|
|
|
/**
|
|
* Tests that requirements can be checked at any point.
|
|
*/
|
|
test("requirements can be checked mid-flow", async () => {
|
|
const { io: createIO } = createMockIO();
|
|
|
|
const createResult = await handleInvitationCommand(
|
|
createCommandDeps(app, createIO, paths),
|
|
["create", "Wallet (P2PKH)", "requestSatoshis"],
|
|
{},
|
|
);
|
|
|
|
const { io: reqIO, spies } = createMockIO();
|
|
|
|
const reqResult = await handleInvitationCommand(
|
|
createCommandDeps(app, reqIO, paths),
|
|
["requirements", createResult.invitationIdentifier!],
|
|
{},
|
|
);
|
|
|
|
expect(reqResult.invitationIdentifier).toBe(
|
|
createResult.invitationIdentifier,
|
|
);
|
|
expect(spies.out).toHaveBeenCalled();
|
|
});
|
|
});
|