215 lines
6.7 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|