Files

83 lines
4.3 KiB
TypeScript

import { after, test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "gashboard-recovery-"));
after(() => rmSync(dir, { recursive: true, force: true }));
process.env.JWT_SECRET = "synthetic-jwt-secret-at-least-32-characters";
process.env.DATUM_ADMIN_PASSWORD = "synthetic-datum-password";
process.env.NOSTR_OWNER_PUBKEYS = "a".repeat(64);
process.env.DATUM_URL = "http://datum:7152";
process.env.MEMPOOL_API_URL = "https://mempool.invalid/api";
process.env.CONTRIBUTION_LEDGER_PATH = join(dir, "ledger.json");
process.env.LOG_LEVEL = "silent";
// Legacy offline ledger names must not reappear as payout addresses after
// startup, and renaming must preserve the original contribution accounting.
writeFileSync(process.env.CONTRIBUTION_LEDGER_PATH, JSON.stringify({ version: 1, entries: [
["offline.fixture", { miner: { authUsername: "offline.fixture", nickname: "old-payout-address", userAgent: "nerdqaxe/fixture" },
lastRawAcceptedWork: 123, lastRawAcceptedShares: 1, totalAcceptedWork: 123,
totalAcceptedShares: 1, firstSeenAt: 1, lastSeenAt: 2 }],
] }));
const originalFetch = globalThis.fetch;
after(() => { globalThis.fetch = originalFetch; });
const { getSnapshot, pollNow } = await import("../src/datum/poller.js");
const clients = `<table><tr><th>Auth Username</th></tr><tr>${[
"0", "192.0.2.3", "payout.fixture", "Subscribed", "2 s", "16384",
"32768 (2)", "0 (0) 0%", "1.2 Th/s (1 s)", "", "bitaxe/BM1368",
].map(value => `<td>${value}</td>`).join("")}</tr></table>`;
const threads = '<table><tr><th>Approx. Hashrate</th></tr><tr><td>0</td><td>1</td><td>1</td><td>1.2 Th/s</td></tr></table>';
test("DATUM outage fails closed, recovery reauthenticates, and later failure retains the last data", async () => {
globalThis.fetch = async input => {
const url = String(input);
if (url.endsWith("/clients")) throw Object.assign(new TypeError("synthetic outage"), { cause: Object.assign(new Error("not found"), { code: "ENOTFOUND", hostname: "datum" }) });
return new Response("", { status: 503 });
};
const failed = await pollNow();
assert.equal(failed.ok, false);
assert.equal(failed.error?.code, "DATUM_UNREACHABLE");
assert.equal(getSnapshot(), failed);
let clientsCalls = 0;
globalThis.fetch = async (input, init) => {
const url = String(input);
if (url.endsWith("/clients")) {
clientsCalls += 1;
const auth = new Headers(init?.headers).get("authorization");
if (!auth) return new Response("auth", { status: 401, headers: { "www-authenticate": 'Digest realm="datum", nonce="fixture", algorithm=SHA-256, qop="auth"' } });
assert.match(auth, /^Digest /);
assert.doesNotMatch(auth, /synthetic-datum-password/);
return new Response(clients, { status: 200 });
}
if (url.endsWith("/threads")) return new Response(threads, { status: 200 });
if (url.endsWith("/blocks/tip/height")) return new Response("900001", { status: 200 });
if (url.endsWith("/v1/mining/hashrate/3d")) return Response.json({ currentHashrate: 600e18, currentDifficulty: 100 });
throw new Error(`unexpected synthetic URL ${url}`);
};
const recovered = await pollNow();
assert.equal(clientsCalls, 2);
assert.equal(recovered.ok, true);
assert.equal(recovered.pool.connectionStatus, "ok");
assert.equal(recovered.miners[0]?.authUsername, "payout.fixture");
assert.equal(recovered.miners[0]?.nickname, "bitaxe/BM1368");
const offline = recovered.miners.find(miner => miner.authUsername === "offline.fixture");
assert.equal(offline?.nickname, "nerdqaxe/fixture");
assert.equal(offline?.contributionAcceptedWork, 123);
assert.equal(offline?.currentlyConnected, false);
assert.equal(recovered.network.blockHeight, 900001);
assert.doesNotMatch(JSON.stringify(recovered), /synthetic-datum-password/);
globalThis.fetch = async input => {
if (String(input).endsWith("/clients")) throw new Error("synthetic restart window");
return new Response("", { status: 503 });
};
const interrupted = await pollNow();
assert.equal(interrupted.ok, false);
assert.equal(interrupted.error?.code, "DATUM_UNREACHABLE");
assert.equal(interrupted.miners[0]?.authUsername, "payout.fixture");
assert.equal(interrupted.network.blockHeight, 900001);
});