import { expect, test, describe, beforeEach, afterEach } from "vitest"; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { createMnemonicSeed, createMnemonicFile, resolveMnemonicFilePath, loadMnemonic, listMnemonicFiles, } from "../../src/cli/mnemonic"; import { BCHMnemonicURL } from "../../src/utils/bch-mnemonic-url"; const TEST_SEED = "page pencil stock planet limb cluster assault speak off joke private pioneer"; describe("mnemonic utilities", () => { let tempDir: string; beforeEach(() => { tempDir = path.join(tmpdir(), `xo-cli-mnemonic-utils-test-${Date.now()}`); mkdirSync(tempDir, { recursive: true }); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); describe("createMnemonicSeed", () => { test("generates a valid BIP39 mnemonic", () => { const mnemonic = createMnemonicSeed(); expect(typeof mnemonic).toBe("string"); const words = mnemonic.split(" "); expect(words.length).toBe(12); }); test("generates unique mnemonics on each call", () => { const mnemonic1 = createMnemonicSeed(); const mnemonic2 = createMnemonicSeed(); expect(mnemonic1).not.toBe(mnemonic2); }); }); describe("createMnemonicFile", () => { test("creates a mnemonic file with auto-generated name", () => { const filename = createMnemonicFile(tempDir, TEST_SEED); expect(filename).toMatch(/^mnemonic-page$/); expect(existsSync(path.join(tempDir, filename))).toBe(true); }); test("creates a mnemonic file with custom name", () => { const filename = createMnemonicFile(tempDir, TEST_SEED, "my-wallet"); expect(filename).toBe("my-wallet"); expect(existsSync(path.join(tempDir, filename))).toBe(true); }); test("writes valid BCHMnemonicURL format", () => { const filename = createMnemonicFile(tempDir, TEST_SEED, "test-wallet"); const content = readFileSync(path.join(tempDir, filename), "utf8"); expect(content).toMatch(/^bch-mnemonic:/); const parsed = BCHMnemonicURL.fromURL(content); expect(parsed).toBeDefined(); }); test("sanitizes filename to basename only", () => { const filename = createMnemonicFile(tempDir, TEST_SEED, "../../../evil-path"); expect(filename).toBe("evil-path"); expect(existsSync(path.join(tempDir, "evil-path"))).toBe(true); expect(existsSync(path.join(tempDir, "../../../evil-path"))).toBe(false); }); test("throws when mnemonic is empty", () => { expect(() => createMnemonicFile(tempDir, "")).toThrow(); }); }); describe("resolveMnemonicFilePath", () => { test("resolves absolute path when file exists", () => { const filePath = path.join(tempDir, "mnemonic-absolute"); writeFileSync(filePath, "test"); const resolved = resolveMnemonicFilePath(tempDir, filePath); expect(resolved).toBe(filePath); }); test("resolves path relative to cwd when file exists", () => { const originalCwd = process.cwd(); process.chdir(tempDir); try { writeFileSync(path.join(tempDir, "mnemonic-relative"), "test"); const resolved = resolveMnemonicFilePath("/nonexistent", "mnemonic-relative"); expect(resolved).toBe(path.join(tempDir, "mnemonic-relative")); } finally { process.chdir(originalCwd); } }); test("resolves from mnemonicsDir when file exists there", () => { writeFileSync(path.join(tempDir, "mnemonic-test"), "test"); const resolved = resolveMnemonicFilePath(tempDir, "mnemonic-test"); expect(resolved).toBe(path.join(tempDir, "mnemonic-test")); }); test("throws when file not found anywhere", () => { expect(() => resolveMnemonicFilePath(tempDir, "nonexistent-file")).toThrow( /Mnemonic file not found/, ); }); test("strips path components and looks up basename in mnemonicsDir", () => { writeFileSync(path.join(tempDir, "mnemonic-basename"), "test"); const resolved = resolveMnemonicFilePath(tempDir, "some/path/mnemonic-basename"); expect(resolved).toBe(path.join(tempDir, "mnemonic-basename")); }); }); describe("loadMnemonic", () => { test("loads mnemonic from file", () => { createMnemonicFile(tempDir, TEST_SEED, "test-load"); const loaded = loadMnemonic(tempDir, "test-load"); expect(loaded).toBe(TEST_SEED); }); test("loads mnemonic from absolute path", () => { const filePath = path.join(tempDir, "mnemonic-absolute-load"); createMnemonicFile(tempDir, TEST_SEED, "mnemonic-absolute-load"); const loaded = loadMnemonic(tempDir, filePath); expect(loaded).toBe(TEST_SEED); }); test("throws when file not found", () => { expect(() => loadMnemonic(tempDir, "nonexistent")).toThrow(/Mnemonic file not found/); }); test("throws when file contains invalid data", () => { writeFileSync(path.join(tempDir, "mnemonic-invalid"), "not a valid mnemonic url"); expect(() => loadMnemonic(tempDir, "mnemonic-invalid")).toThrow(); }); }); describe("listMnemonicFiles", () => { test("returns empty array when no mnemonic files exist", () => { const files = listMnemonicFiles(tempDir); expect(files).toEqual([]); }); test("lists only files starting with 'mnemonic-'", () => { writeFileSync(path.join(tempDir, "mnemonic-one"), "test"); writeFileSync(path.join(tempDir, "mnemonic-two"), "test"); writeFileSync(path.join(tempDir, "other-file"), "test"); writeFileSync(path.join(tempDir, "wallet.json"), "test"); const files = listMnemonicFiles(tempDir); expect(files).toHaveLength(2); expect(files).toContain("mnemonic-one"); expect(files).toContain("mnemonic-two"); expect(files).not.toContain("other-file"); expect(files).not.toContain("wallet.json"); }); test("returns sorted or consistent ordering", () => { writeFileSync(path.join(tempDir, "mnemonic-zebra"), "test"); writeFileSync(path.join(tempDir, "mnemonic-alpha"), "test"); writeFileSync(path.join(tempDir, "mnemonic-beta"), "test"); const files = listMnemonicFiles(tempDir); expect(files).toHaveLength(3); }); }); describe("round-trip", () => { test("create and load preserves mnemonic exactly", () => { const original = createMnemonicSeed(); createMnemonicFile(tempDir, original, "roundtrip-test"); const loaded = loadMnemonic(tempDir, "roundtrip-test"); expect(loaded).toBe(original); }); }); });