Add currency settings, Settings service, and dialog to select fiat currency. Add support for non Official currencies like DOGE when using rates.
This commit is contained in:
83
tests/cli/commands/settings.test.ts
Normal file
83
tests/cli/commands/settings.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { handleSettingsCommand } from "../../../src/cli/commands/settings";
|
||||
import { CommandError } from "../../../src/cli/commands/types";
|
||||
import { createMockIO, createMockPaths } from "../mocks/command";
|
||||
|
||||
describe("settings command", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-settings-command-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("shows default settings when .wallet does not exist", async () => {
|
||||
const { io, capture } = createMockIO();
|
||||
const paths = createMockPaths(tempDir);
|
||||
|
||||
const result = await handleSettingsCommand({ io, paths }, ["show"], {});
|
||||
|
||||
expect(result).toEqual({ currency: "USD" });
|
||||
expect(capture.out.join("\n")).toContain("currency");
|
||||
expect(capture.out.join("\n")).toContain("USD");
|
||||
});
|
||||
|
||||
test("sets and gets currency", async () => {
|
||||
const { io, capture } = createMockIO();
|
||||
const paths = createMockPaths(tempDir);
|
||||
|
||||
await handleSettingsCommand({ io, paths }, ["set", "currency", "aud"], {});
|
||||
const getResult = await handleSettingsCommand(
|
||||
{ io, paths },
|
||||
["get", "currency"],
|
||||
{},
|
||||
);
|
||||
|
||||
expect(getResult).toEqual({ key: "currency", value: "AUD" });
|
||||
expect(capture.out).toContain("Updated currency: AUD");
|
||||
expect(capture.out).toContain("AUD");
|
||||
});
|
||||
|
||||
test("sets default-mnemonic and persists JSON .wallet", async () => {
|
||||
const { io } = createMockIO();
|
||||
const paths = createMockPaths(tempDir);
|
||||
|
||||
await handleSettingsCommand(
|
||||
{ io, paths },
|
||||
["set", "default-mnemonic", "mnemonic-primary"],
|
||||
{},
|
||||
);
|
||||
|
||||
const persisted = JSON.parse(readFileSync(paths.walletConfigPath, "utf8")) as {
|
||||
currency: string;
|
||||
"default-mnemonic"?: string;
|
||||
};
|
||||
|
||||
expect(persisted).toEqual({
|
||||
currency: "USD",
|
||||
"default-mnemonic": "mnemonic-primary",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws command error for unknown subcommand", async () => {
|
||||
const { io } = createMockIO();
|
||||
const paths = createMockPaths(tempDir);
|
||||
|
||||
try {
|
||||
await handleSettingsCommand({ io, paths }, ["unknown"], {});
|
||||
expect.fail("Expected settings command to throw");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CommandError);
|
||||
expect((error as CommandError).event).toBe("settings.subcommand.unknown");
|
||||
}
|
||||
});
|
||||
});
|
||||
40
tests/cli/rates-format.test.ts
Normal file
40
tests/cli/rates-format.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { BaseRates } from "../../src/utils/rates/base-rates";
|
||||
|
||||
/**
|
||||
* Minimal concrete adapter used only for testing BaseRates helpers.
|
||||
*/
|
||||
class TestRatesAdapter extends BaseRates {
|
||||
public async start(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public async listPairs(): Promise<Set<string>> {
|
||||
return new Set(["USD/BCH", "DOGE/BCH"]);
|
||||
}
|
||||
}
|
||||
|
||||
describe("BaseRates.formatCurrency", () => {
|
||||
test("formats ISO currency codes with Intl currency style", () => {
|
||||
const rates = new TestRatesAdapter();
|
||||
const formatted = rates.formatCurrency(12.5, "USD");
|
||||
|
||||
expect(formatted).toContain("$");
|
||||
expect(formatted).toContain("12.50");
|
||||
});
|
||||
|
||||
test("formats non-ISO symbols without throwing", () => {
|
||||
const rates = new TestRatesAdapter();
|
||||
const formatted = rates.formatCurrency(12.3456789, "DOGE");
|
||||
|
||||
expect(formatted).toContain("DOGE");
|
||||
expect(formatted).toContain("12.3456789");
|
||||
});
|
||||
});
|
||||
96
tests/cli/settings.test.ts
Normal file
96
tests/cli/settings.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { beforeEach, afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { SettingsService } from "../../src/services/settings";
|
||||
|
||||
/**
|
||||
* Tests for SettingsService persistence and migration behavior.
|
||||
*/
|
||||
describe("SettingsService", () => {
|
||||
let testDir: string;
|
||||
let settingsPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = mkdtempSync(join(tmpdir(), "xo-cli-settings-test-"));
|
||||
settingsPath = join(testDir, ".wallet");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("returns defaults when settings file does not exist", () => {
|
||||
const settings = new SettingsService(settingsPath);
|
||||
|
||||
expect(settings.getDefaultMnemonic()).toBeUndefined();
|
||||
expect(settings.getCurrency()).toBe("USD");
|
||||
expect(settings.getSettings()).toEqual({ currency: "USD" });
|
||||
expect(existsSync(settingsPath)).toBe(false);
|
||||
});
|
||||
|
||||
test("migrates legacy .wallet plaintext mnemonic to JSON", () => {
|
||||
writeFileSync(settingsPath, "mnemonic-legacy", "utf8");
|
||||
|
||||
const settings = new SettingsService(settingsPath);
|
||||
|
||||
expect(settings.getDefaultMnemonic()).toBe("mnemonic-legacy");
|
||||
expect(settings.getCurrency()).toBe("USD");
|
||||
|
||||
const persisted = JSON.parse(readFileSync(settingsPath, "utf8")) as {
|
||||
"default-mnemonic"?: string;
|
||||
currency: string;
|
||||
};
|
||||
expect(persisted).toEqual({
|
||||
"default-mnemonic": "mnemonic-legacy",
|
||||
currency: "USD",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes and persists currency and default-mnemonic", () => {
|
||||
const settings = new SettingsService(settingsPath);
|
||||
|
||||
settings.setDefaultMnemonic(" mnemonic-primary ");
|
||||
settings.setCurrency("aud");
|
||||
|
||||
expect(settings.getSettings()).toEqual({
|
||||
"default-mnemonic": "mnemonic-primary",
|
||||
currency: "AUD",
|
||||
});
|
||||
|
||||
const persisted = JSON.parse(readFileSync(settingsPath, "utf8")) as {
|
||||
"default-mnemonic"?: string;
|
||||
currency: string;
|
||||
};
|
||||
expect(persisted).toEqual({
|
||||
"default-mnemonic": "mnemonic-primary",
|
||||
currency: "AUD",
|
||||
});
|
||||
});
|
||||
|
||||
test("emits settings-updated events on setting changes", () => {
|
||||
const settings = new SettingsService(settingsPath);
|
||||
const events: Array<{ key: string; value: string | undefined }> = [];
|
||||
|
||||
settings.on("settings-updated", (event) => {
|
||||
events.push({ key: event.key, value: event.value });
|
||||
});
|
||||
|
||||
settings.setCurrency("cad");
|
||||
settings.setDefaultMnemonic("mnemonic-blue");
|
||||
|
||||
expect(events).toEqual([
|
||||
{ key: "currency", value: "CAD" },
|
||||
{ key: "default-mnemonic", value: "mnemonic-blue" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user