import { expect, test, describe, beforeEach, afterEach } from "vitest"; 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 { type Engine } from "@xo-cash/engine"; 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"; type TestCase = { name: string; inputs: string[]; options?: Record; shouldThrow: boolean; expectedEvent?: string; expectedData?: Record; logs?: LogExpectation[]; }; const testCases: TestCase[] = [ // List command { name: "list returns count of imported templates", inputs: ["list"], shouldThrow: false, expectedData: { count: 1, }, logs: [{ out: "Wallet (P2PKH)" }], }, // List by category { name: "list action returns actions for template", inputs: ["list", "action", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, logs: [{ out: "receive" }], }, { name: "list output returns outputs for template", inputs: ["list", "output", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, logs: [{ out: "receiveOutput" }], }, { name: "list variable returns variables for template", inputs: ["list", "variable", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, logs: [{ out: "ownerKey" }], }, { name: "list transaction returns transactions for template", inputs: ["list", "transaction", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, }, { name: "list lockingscript returns locking scripts for template", inputs: ["list", "lockingscript", p2pkhTemplateIdentifier], shouldThrow: false, expectedData: {}, }, // Inspect command { name: "inspect action returns action details", inputs: ["inspect", "action", p2pkhTemplateIdentifier, "receive"], shouldThrow: false, expectedData: {}, }, { name: "inspect output returns output details", inputs: ["inspect", "output", p2pkhTemplateIdentifier, "receiveOutput"], shouldThrow: false, expectedData: {}, }, { name: "inspect variable returns variable details", inputs: ["inspect", "variable", p2pkhTemplateIdentifier, "ownerKey"], shouldThrow: false, expectedData: {}, }, // Error cases - subcommand { name: "throws when no subcommand provided", inputs: [], shouldThrow: true, expectedEvent: "template.subcommand.missing", }, { name: "throws when unknown subcommand provided", inputs: ["unknown-subcommand"], shouldThrow: true, expectedEvent: "template.subcommand.unknown", }, // Error cases - import { name: "throws when import called without file", inputs: ["import"], shouldThrow: true, expectedEvent: "template.import.file_missing", }, { name: "throws when import called with non-existent file", inputs: ["import", "non-existent-file.json"], shouldThrow: true, expectedEvent: "template.import.file_not_found", }, // Error cases - list category { name: "throws when list category called without template identifier", inputs: ["list", "action"], shouldThrow: true, expectedEvent: "template.list.identifier_missing", }, { name: "throws when list category called with unknown template", inputs: ["list", "action", "unknown-template"], shouldThrow: true, expectedEvent: "template.list.not_found", }, { name: "throws when list called with unknown category", inputs: ["list", "unknown-category", p2pkhTemplateIdentifier], shouldThrow: true, expectedEvent: "template.list.category_unknown", }, // Error cases - inspect { name: "throws when inspect called without all arguments", inputs: ["inspect", "action"], shouldThrow: true, expectedEvent: "template.inspect.arguments_missing", }, { name: "throws when inspect called with unknown template", inputs: ["inspect", "action", "unknown-template", "receive"], shouldThrow: true, expectedEvent: "template.resolve.not_found", }, { name: "throws when inspect called with unknown action", inputs: ["inspect", "action", p2pkhTemplateIdentifier, "unknown-action"], shouldThrow: true, expectedEvent: "template.inspect.action_missing", }, { name: "throws when inspect called with unknown category", inputs: ["inspect", "unknown-category", p2pkhTemplateIdentifier, "field"], shouldThrow: true, expectedEvent: "template.inspect.category_unknown", }, // Error cases - set-default { name: "throws when set-default called without all arguments", inputs: ["set-default", "template"], shouldThrow: true, expectedEvent: "template.default.arguments_missing", }, ]; describe("template command", () => { let engine: Engine; let app: AppService; let tempDir: string; beforeEach(async () => { engine = await createMockEngine(DEFAULT_SEED); await engine.importTemplate(p2pkhTemplate); app = await createMockAppService(engine); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-template-tests-")); }); afterEach(async () => { await engine.stop(); rmSync(tempDir, { recursive: true, force: true }); }); 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); } } } 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); } }); 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), ["import", "test-template.json"], {}, ); expect(result.templateFile).toBe("test-template.json"); } finally { process.chdir(originalCwd); } }); });