Files
bitvid/tests/zap-requests.test.mjs
T
Claude 75136cb613 test: add comprehensive payment system tests (lnurl, platformFee, zapReceipts, zapRequests)
Add 4 new test files covering previously untested payment/zap logic:

- lnurl.test.mjs: bech32 encode/decode roundtrip, resolveLightningAddress
  (email, lnurl, URL formats), fetchPayServiceData (success, HTTP errors,
  invalid JSON, error status), validateInvoiceAmount (boundaries, invalid
  inputs), requestInvoice (comment truncation, amount formats, error paths)

- platform-fee.test.mjs: parsePercentValue (numbers, bigints, strings with
  %, fractions, edge cases), clampPercent (boundaries, NaN, non-finite),
  getDefaultPlatformFeePercent, resolvePlatformFeePercent (override vs
  fallback behavior)

- zap-receipt-validator.test.mjs: computeZapRequestHash determinism,
  extractDescriptionHashFromBolt11, validateZapReceipt covering all failure
  modes (missing request, invalid JSON, no relays, missing invoice,
  hash mismatch, amount mismatch, wrong pubkey, invalid signature,
  no receipt found) plus success path and override injection

- zap-requests.test.mjs: normalizeRelayList (dedup, filtering, empty),
  resolveLnurlTag (bech32 vs email addresses), buildZapRequestPayload
  (minimal and full params, timestamp handling), signZapRequest (signer
  validation)

Also updates todo-test-coverage-gaps.md with a prioritized, detailed plan
covering all remaining untested areas (P0 through P3).

https://claude.ai/code/session_01QDsPRh4f31ZpDYqS1kETFV
2026-02-05 14:50:41 +00:00

281 lines
8.2 KiB
JavaScript

import assert from "node:assert/strict";
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
}
if (typeof globalThis.localStorage === "undefined") {
const storage = new Map();
globalThis.localStorage = {
getItem(key) {
return storage.has(key) ? storage.get(key) : null;
},
setItem(key, value) {
storage.set(String(key), String(value));
},
removeItem(key) {
storage.delete(String(key));
},
clear() {
storage.clear();
},
};
}
if (typeof window.localStorage === "undefined") {
window.localStorage = globalThis.localStorage;
}
const nostrToolsStub = {
getEventHash(event) {
const base = JSON.stringify(event);
let hash = 0;
for (let i = 0; i < base.length; i += 1) {
hash = (hash * 33 + base.charCodeAt(i)) % 0xffffffff;
}
return hash.toString(16).padStart(8, "0");
},
signEvent(event, secret) {
return `sig-${(secret || "").slice(0, 8)}-${event.kind}`;
},
nip19: {
decode(value) {
if (typeof value === "string" && value.startsWith("npub")) {
return { type: "npub", data: "f".repeat(64) };
}
return null;
},
},
SimplePool: class {
async list() {
return [];
}
sub() {
return { on() {}, unsub() {} };
}
async get() {
return null;
}
async ensureRelay() {
return { close() {}, url: "" };
}
},
};
window.NostrTools = nostrToolsStub;
globalThis.NostrTools = nostrToolsStub;
globalThis.fetch = async () => {
throw new Error("Unexpected fetch call during zap-requests tests.");
};
const {
buildZapRequestPayload,
signZapRequest,
__TESTING__,
} = await import("../js/payments/zapRequests.js");
const { normalizeRelayList, resolveLnurlTag } = __TESTING__;
const { encodeLnurlBech32 } = await import("../js/payments/lnurl.js");
// ---------------------------------------------------------------------------
// normalizeRelayList
// ---------------------------------------------------------------------------
(function testNormalizeRelayListDeduplicates() {
const result = normalizeRelayList([
"wss://relay.one",
"wss://relay.two",
"wss://relay.one",
"wss://relay.two",
]);
assert.deepEqual(result, ["wss://relay.one", "wss://relay.two"]);
})();
(function testNormalizeRelayListFiltersNonStrings() {
const result = normalizeRelayList(["wss://relay.one", 123, null, undefined, "", "wss://relay.two"]);
assert.deepEqual(result, ["wss://relay.one", "wss://relay.two"]);
})();
(function testNormalizeRelayListTrimsWhitespace() {
const result = normalizeRelayList([" wss://relay.one ", "wss://relay.two "]);
assert.deepEqual(result, ["wss://relay.one", "wss://relay.two"]);
})();
(function testNormalizeRelayListEmptyInput() {
assert.deepEqual(normalizeRelayList([]), []);
assert.deepEqual(normalizeRelayList(null), []);
assert.deepEqual(normalizeRelayList(undefined), []);
assert.deepEqual(normalizeRelayList("not-array"), []);
})();
// ---------------------------------------------------------------------------
// resolveLnurlTag
// ---------------------------------------------------------------------------
(function testResolveLnurlTagWithBech32Address() {
const lnurlAddress = encodeLnurlBech32("https://example.com/callback");
const resolved = { address: lnurlAddress, url: "https://example.com/callback" };
const tag = resolveLnurlTag(resolved);
assert.equal(tag, lnurlAddress.toLowerCase());
})();
(function testResolveLnurlTagWithUrlAddress() {
const resolved = { address: "[email protected]", url: "https://example.com/.well-known/lnurlp/alice" };
const tag = resolveLnurlTag(resolved);
assert(tag.startsWith("lnurl"), "should encode URL to bech32 lnurl");
})();
(function testResolveLnurlTagWithNoAddress() {
const tag = resolveLnurlTag({ url: "" });
assert.equal(tag, "");
})();
(function testResolveLnurlTagWithNull() {
const tag = resolveLnurlTag(null);
assert.equal(tag, "");
})();
// ---------------------------------------------------------------------------
// buildZapRequestPayload
// ---------------------------------------------------------------------------
(function testBuildZapRequestPayloadMinimal() {
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
amountSats: 100,
});
assert.equal(event.kind, 9734);
assert.equal(event.pubkey, "a".repeat(64));
assert(event.created_at > 0, "should have a timestamp");
const pTag = event.tags.find((t) => t[0] === "p");
assert(pTag, "should have a p tag");
assert.equal(pTag[1], "b".repeat(64));
const amountTag = event.tags.find((t) => t[0] === "amount");
assert(amountTag, "should have an amount tag");
assert.equal(amountTag[1], "100000");
})();
(function testBuildZapRequestPayloadAllParams() {
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
relays: ["wss://relay.one", "wss://relay.two"],
amountSats: 500,
comment: "Test zap",
lnurl: "lnurl1test",
eventId: "c".repeat(64),
coordinate: "30078:bbbb:pointer",
createdAt: 1700000000,
additionalTags: [["custom", "value"]],
});
assert.equal(event.kind, 9734);
assert.equal(event.pubkey, "a".repeat(64));
assert.equal(event.created_at, 1700000000);
assert.equal(event.content, "Test zap");
const relaysTag = event.tags.find((t) => t[0] === "relays");
assert(relaysTag, "should have relays tag");
assert(relaysTag.includes("wss://relay.one"));
assert(relaysTag.includes("wss://relay.two"));
const eTag = event.tags.find((t) => t[0] === "e");
assert(eTag, "should have an e tag for eventId");
assert.equal(eTag[1], "c".repeat(64));
const aTag = event.tags.find((t) => t[0] === "a");
assert(aTag, "should have an a tag for coordinate");
assert.equal(aTag[1], "30078:bbbb:pointer");
const lnurlTag = event.tags.find((t) => t[0] === "lnurl");
assert(lnurlTag, "should have lnurl tag");
assert.equal(lnurlTag[1], "lnurl1test");
const customTag = event.tags.find((t) => t[0] === "custom");
assert(customTag, "should include additionalTags");
assert.equal(customTag[1], "value");
})();
(function testBuildZapRequestPayloadCreatedAtDefault() {
const before = Math.floor(Date.now() / 1000);
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
amountSats: 10,
});
const after = Math.floor(Date.now() / 1000);
assert(event.created_at >= before && event.created_at <= after);
})();
(function testBuildZapRequestPayloadCustomCreatedAt() {
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
amountSats: 10,
createdAt: 1234567890,
});
assert.equal(event.created_at, 1234567890);
})();
// ---------------------------------------------------------------------------
// signZapRequest
// ---------------------------------------------------------------------------
async function testSignZapRequestRequiresSigner() {
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
amountSats: 10,
});
await assert.rejects(
() => signZapRequest(event, null),
/signer is required/i
);
await assert.rejects(
() => signZapRequest(event, {}),
/signer is required/i
);
await assert.rejects(
() => signZapRequest(event, { signEvent: "not-a-function" }),
/signer is required/i
);
}
async function testSignZapRequestCallsSigner() {
const event = buildZapRequestPayload({
senderPubkey: "a".repeat(64),
recipientPubkey: "b".repeat(64),
amountSats: 10,
});
let signedWith = null;
const signer = {
signEvent(eventToSign) {
signedWith = eventToSign;
return { ...eventToSign, sig: "test-signature" };
},
};
const signed = await signZapRequest(event, signer);
assert(signedWith, "signer should have been called");
assert.equal(signed.sig, "test-signature");
}
// ---------------------------------------------------------------------------
// Run async tests
// ---------------------------------------------------------------------------
await testSignZapRequestRequiresSigner();
await testSignZapRequestCallsSigner();
console.log("zap-requests tests passed");