Files
xo-cli/tests/cli/mnemonic.test.ts
2026-04-20 12:26:35 +00:00

215 lines
6.7 KiB
TypeScript

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);
});
});
});