mirror of
https://github.com/Routstr/routstrd.git
synced 2026-08-09 03:44:38 +00:00
test(wallet): verify legacy cocod database migration
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it, mock } from "bun:test";
|
||||
import { afterEach, describe, expect, it, mock } from "bun:test";
|
||||
import { gunzipSync } from "bun";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import {
|
||||
assertLegacyCocodNotRunning,
|
||||
claimLegacyCocodPidFile,
|
||||
createCocoClient,
|
||||
} from "./coco-client";
|
||||
|
||||
type GuardOptions = NonNullable<
|
||||
@@ -11,11 +16,67 @@ type LegacyFetch = NonNullable<GuardOptions["fetchImpl"]>;
|
||||
|
||||
const SOCKET_PATH = "/tmp/routstrd-test/cocod.sock";
|
||||
const PID_FILE_PATH = "/tmp/routstrd-test/cocod.pid";
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-migration-test-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function socketOnly(path: string): boolean {
|
||||
return path === SOCKET_PATH;
|
||||
}
|
||||
|
||||
describe("legacy cocod wallet migration", () => {
|
||||
it("opens an existing unencrypted config and preserves database balances", async () => {
|
||||
const walletDir = join(makeTempDir(), ".cocod");
|
||||
const mintUrl = "https://mint.example.com";
|
||||
mkdirSync(walletDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(walletDir, "config.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
mnemonic:
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
encrypted: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// This fixture was generated with @routstr/cocod 0.0.24 using its
|
||||
// coco-cashu-sqlite-bun 1.1.2-rc.50 adapter. Keeping it frozen prevents
|
||||
// this test from accidentally creating its "legacy" database with the
|
||||
// same current adapter that createCocoClient uses to read it.
|
||||
const fixture = readFileSync(
|
||||
join(import.meta.dir, "fixtures", "cocod-0.0.24-wallet.db.gz"),
|
||||
);
|
||||
writeFileSync(join(walletDir, "coco.db"), gunzipSync(fixture));
|
||||
|
||||
const client = await createCocoClient({ configDir: walletDir });
|
||||
try {
|
||||
expect(await client.getStatus()).toBe("UNLOCKED");
|
||||
expect(await client.getBalances()).toEqual({ [mintUrl]: 10 });
|
||||
} finally {
|
||||
await client.dispose?.();
|
||||
}
|
||||
|
||||
// Prove the migrated schema remains reopenable and the pre-existing
|
||||
// proofs survive a complete in-process wallet restart.
|
||||
const reopenedClient = await createCocoClient({ configDir: walletDir });
|
||||
try {
|
||||
expect(await reopenedClient.getBalances()).toEqual({ [mintUrl]: 10 });
|
||||
} finally {
|
||||
await reopenedClient.dispose?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertLegacyCocodNotRunning", () => {
|
||||
it("does not probe when the legacy socket and PID file do not exist", async () => {
|
||||
const fetchImpl = mock<LegacyFetch>(async () => new Response("pong"));
|
||||
|
||||
@@ -64,13 +64,13 @@ interface CocodConfig {
|
||||
encrypted: boolean;
|
||||
}
|
||||
|
||||
function loadMnemonic(): string {
|
||||
if (!existsSync(CONFIG_FILE)) {
|
||||
function loadMnemonic(configFile: string = CONFIG_FILE): string {
|
||||
if (!existsSync(configFile)) {
|
||||
throw new Error(
|
||||
`Config file not found at ${CONFIG_FILE}. Run 'routstrd onboard' first.`,
|
||||
`Config file not found at ${configFile}. Run 'routstrd onboard' first.`,
|
||||
);
|
||||
}
|
||||
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as CocodConfig;
|
||||
const config = JSON.parse(readFileSync(configFile, "utf-8")) as CocodConfig;
|
||||
if (config.encrypted) {
|
||||
throw new Error(
|
||||
"Encrypted wallets are not supported yet. Please use an unencrypted wallet.",
|
||||
@@ -79,11 +79,6 @@ function loadMnemonic(): string {
|
||||
return config.mnemonic;
|
||||
}
|
||||
|
||||
async function seedGetter(): Promise<Uint8Array> {
|
||||
const mnemonic = loadMnemonic();
|
||||
return mnemonicToSeedSync(mnemonic);
|
||||
}
|
||||
|
||||
function defaultIsProcessRunning(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
@@ -282,21 +277,41 @@ export function claimLegacyCocodPidFile(
|
||||
};
|
||||
}
|
||||
|
||||
export async function createCocoClient(): Promise<CocodClient> {
|
||||
await assertLegacyCocodNotRunning();
|
||||
const releaseLegacyPidClaim = claimLegacyCocodPidFile();
|
||||
export interface CreateCocoClientOptions {
|
||||
/** Override the wallet directory, primarily for migration tests and tooling. */
|
||||
configDir?: string;
|
||||
socketPath?: string;
|
||||
pidFilePath?: string;
|
||||
}
|
||||
|
||||
export async function createCocoClient(
|
||||
options: CreateCocoClientOptions = {},
|
||||
): Promise<CocodClient> {
|
||||
const configDir = options.configDir || CONFIG_DIR;
|
||||
const configFile = join(configDir, "config.json");
|
||||
const dbPath = join(configDir, "coco.db");
|
||||
const socketPath = options.socketPath ||
|
||||
(options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET);
|
||||
const pidFilePath = options.pidFilePath ||
|
||||
(options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE);
|
||||
|
||||
await assertLegacyCocodNotRunning({ socketPath, pidFilePath });
|
||||
const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath });
|
||||
|
||||
let database: Database | undefined;
|
||||
let coco: Awaited<ReturnType<typeof initializeCoco>> | undefined;
|
||||
|
||||
try {
|
||||
database = new Database(DB_PATH);
|
||||
// Read and validate the existing cocod config during startup rather than
|
||||
// deferring failure until coco-core first needs wallet key material.
|
||||
const seed = mnemonicToSeedSync(loadMnemonic(configFile));
|
||||
database = new Database(dbPath);
|
||||
const repo = new SqliteRepositories({ database });
|
||||
await repo.init();
|
||||
|
||||
coco = await initializeCoco({
|
||||
repo,
|
||||
seedGetter,
|
||||
seedGetter: async () => seed,
|
||||
});
|
||||
} catch (error) {
|
||||
database?.close();
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user