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 { 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 { BCHMnemonicURL } from "../../../src/utils/bch-mnemonic-url"; type TestCase = { inputs: string[]; options?: Record; shouldThrow: boolean; expectedEvent?: string; expectedData?: Record; logs?: LogExpectation[]; }; const testCases: TestCase[] = [ // Successful creation of a mnemonic file { inputs: ["create"], shouldThrow: false, expectedData: { savedAs: expect.stringMatching(/^mnemonic-\w+$/), }, logs: [{ out: "Mnemonic file created" }], }, // Successfully creating a mnemonic file with a custom filename { inputs: ["create"], options: { output: "custom-filename" }, shouldThrow: false, expectedData: { savedAs: "custom-filename", }, logs: [{ out: "custom-filename" }], }, // Successfully listing mnemonic files { inputs: ["list"], shouldThrow: false, expectedData: { count: expect.toSatisfy((count: number) => count >= 1), }, logs: [{ out: "mnemonic-test" }], }, // Successfully exposing a mnemonic file { inputs: ["expose", "mnemonic-test"], shouldThrow: false, expectedData: { mnemonic: DEFAULT_SEED, }, logs: [{ out: DEFAULT_SEED }], }, // Successfully importing a mnemonic file { inputs: ["import", ...DEFAULT_SEED.split(" ")], shouldThrow: false, expectedData: { savedAs: expect.stringMatching(/^mnemonic-\w+$/), }, logs: [{ out: "Mnemonic file created" }], }, // Failure to import a mnemonic file due to missing arguments { inputs: ["import"], shouldThrow: true, expectedEvent: "mnemonic.import.seed_missing", }, // Failure to expose a mnemonic file due to missing arguments { inputs: ["expose"], shouldThrow: true, expectedEvent: "mnemonic.expose.file_missing", }, // Failure to expose a mnemonic file due to unknown mnemonic file { inputs: ["expose", "unknown-mnemonic-file"], shouldThrow: true, expectedEvent: "mnemonic.expose.file_not_found", }, // Missing sub-command { inputs: [], shouldThrow: true, expectedEvent: "mnemonic.subcommand.missing", }, // Unknown sub-command { inputs: ["unknown"], shouldThrow: true, expectedEvent: "mnemonic.subcommand.unknown", }, ]; describe("mnemonic commands", () => { let tempDir: string; 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"), BCHMnemonicURL.fromSeed(DEFAULT_SEED).toURL(), "utf8", ); }); afterEach(async () => { 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); 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); }); } } if (logs) { expectLogs(spies, logs); } }); });