mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 00:12:25 +00:00
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
763 lines
22 KiB
JavaScript
763 lines
22 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { bech32 } from "@scure/base";
|
|
|
|
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() {
|
|
return "0".repeat(64);
|
|
},
|
|
signEvent() {
|
|
return "sig-stub";
|
|
},
|
|
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 validator tests.");
|
|
};
|
|
|
|
const {
|
|
validateZapReceipt,
|
|
__TESTING__: validatorTesting,
|
|
} = await import("../js/payments/zapReceiptValidator.js");
|
|
|
|
const { computeZapRequestHash, extractDescriptionHashFromBolt11 } = validatorTesting;
|
|
|
|
const BOLT11_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
|
|
function buildTestBolt11Invoice({ amountCode = "10u", descriptionHashHex }) {
|
|
const hrp = `lnbc${amountCode}`;
|
|
const timestamp = 1_700_000_000;
|
|
|
|
const timestampWords = [];
|
|
let remaining = timestamp;
|
|
for (let i = 0; i < 7; i += 1) {
|
|
timestampWords.unshift(remaining & 31);
|
|
remaining >>= 5;
|
|
}
|
|
|
|
const words = [...timestampWords];
|
|
|
|
const pushTag = (tag, dataWords) => {
|
|
const code = BOLT11_CHARSET.indexOf(tag);
|
|
words.push(code);
|
|
const length = dataWords.length;
|
|
words.push((length >> 5) & 31);
|
|
words.push(length & 31);
|
|
words.push(...dataWords);
|
|
};
|
|
|
|
const paymentHashHex = "11".repeat(32);
|
|
pushTag("p", bech32.toWords(Buffer.from(paymentHashHex, "hex")));
|
|
|
|
if (descriptionHashHex) {
|
|
pushTag("h", bech32.toWords(Buffer.from(descriptionHashHex, "hex")));
|
|
}
|
|
|
|
const signatureWords = new Array(104).fill(0);
|
|
const data = [...words, ...signatureWords];
|
|
|
|
return bech32.encode(hrp, data, 2000);
|
|
}
|
|
|
|
function buildZapRequest({ relays = ["wss://relay.example"], pubkey = "f".repeat(64) } = {}) {
|
|
return {
|
|
kind: 9734,
|
|
pubkey,
|
|
content: "",
|
|
created_at: 1_700_000_000,
|
|
tags: [
|
|
["p", "a".repeat(64)],
|
|
["amount", String(1_000_000)],
|
|
["relays", ...relays],
|
|
],
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// computeZapRequestHash
|
|
// ---------------------------------------------------------------------------
|
|
|
|
(function testComputeZapRequestHashDeterministic() {
|
|
const input = JSON.stringify({ kind: 9734, content: "test" });
|
|
const hash1 = computeZapRequestHash(input);
|
|
const hash2 = computeZapRequestHash(input);
|
|
assert.equal(hash1, hash2);
|
|
assert.equal(typeof hash1, "string");
|
|
assert(hash1.length > 0, "hash should be non-empty");
|
|
})();
|
|
|
|
(function testComputeZapRequestHashDifferentInputs() {
|
|
const hash1 = computeZapRequestHash("input-a");
|
|
const hash2 = computeZapRequestHash("input-b");
|
|
assert.notEqual(hash1, hash2);
|
|
})();
|
|
|
|
(function testComputeZapRequestHashEmptyString() {
|
|
const hash = computeZapRequestHash("");
|
|
assert.equal(typeof hash, "string");
|
|
assert(hash.length > 0);
|
|
})();
|
|
|
|
(function testComputeZapRequestHashNonString() {
|
|
// Treats non-string as empty string
|
|
const hash = computeZapRequestHash(null);
|
|
const hashEmpty = computeZapRequestHash("");
|
|
assert.equal(hash, hashEmpty);
|
|
})();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// extractDescriptionHashFromBolt11
|
|
// ---------------------------------------------------------------------------
|
|
|
|
(function testExtractDescriptionHashFromValidBolt11() {
|
|
const descriptionHashHex = "bb".repeat(32);
|
|
const invoice = buildTestBolt11Invoice({ amountCode: "1m", descriptionHashHex });
|
|
const extracted = extractDescriptionHashFromBolt11(invoice);
|
|
assert.equal(extracted, descriptionHashHex);
|
|
})();
|
|
|
|
(function testExtractDescriptionHashFromBolt11WithoutHash() {
|
|
const invoice = buildTestBolt11Invoice({ amountCode: "1m" });
|
|
const extracted = extractDescriptionHashFromBolt11(invoice);
|
|
assert.equal(extracted, null);
|
|
})();
|
|
|
|
(function testExtractDescriptionHashFromEmptyString() {
|
|
assert.equal(extractDescriptionHashFromBolt11(""), null);
|
|
})();
|
|
|
|
(function testExtractDescriptionHashFromNull() {
|
|
assert.equal(extractDescriptionHashFromBolt11(null), null);
|
|
})();
|
|
|
|
(function testExtractDescriptionHashFromUndefined() {
|
|
assert.equal(extractDescriptionHashFromBolt11(undefined), null);
|
|
})();
|
|
|
|
(function testExtractDescriptionHashFromGarbage() {
|
|
assert.equal(extractDescriptionHashFromBolt11("not-a-bolt11-invoice"), null);
|
|
})();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — missing/invalid zap request
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateSkippedWhenNoZapRequest() {
|
|
const result = await validateZapReceipt({});
|
|
assert.equal(result.status, "skipped");
|
|
assert.match(result.reason, /not provided/i);
|
|
}
|
|
|
|
async function testValidateSkippedWhenZapRequestIsEmpty() {
|
|
const result = await validateZapReceipt({ zapRequest: "" });
|
|
assert.equal(result.status, "skipped");
|
|
assert.match(result.reason, /not provided/i);
|
|
}
|
|
|
|
async function testValidateFailsWhenZapRequestIsInvalidJson() {
|
|
const result = await validateZapReceipt({ zapRequest: "not-json{{{" });
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /could not be parsed/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — no relays
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenNoRelaysInZapRequest() {
|
|
const zapEvent = {
|
|
kind: 9734,
|
|
pubkey: "f".repeat(64),
|
|
tags: [["p", "a".repeat(64)]],
|
|
};
|
|
const result = await validateZapReceipt({
|
|
zapRequest: JSON.stringify(zapEvent),
|
|
});
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /relays/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — missing invoice
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenNoInvoice() {
|
|
const zapEvent = buildZapRequest();
|
|
const result = await validateZapReceipt({
|
|
zapRequest: JSON.stringify(zapEvent),
|
|
});
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /invoice.*not available/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — description hash mismatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsOnDescriptionHashMismatch() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const mismatchHash = "cc".repeat(32);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: mismatchHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /description hash/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — invoice has no description hash
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenInvoiceLacksDescriptionHash() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const bolt11 = buildTestBolt11Invoice({});
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /description hash/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — amount mismatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsOnAmountMismatch() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
amountSats: 500,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
getAmountFromBolt11: () => 999,
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /amount.*did not match/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — missing nostrPubkey in metadata
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenMetadataMissingPubkey() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
amountSats: 0,
|
|
metadata: {},
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /nostrPubkey/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — no receipt found on relays
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenNoReceiptOnRelays() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /no zap receipt/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — receipt with wrong pubkey
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenReceiptPubkeyDoesNotMatch() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const wrongPubkeyReceipt = {
|
|
kind: 9735,
|
|
pubkey: "d".repeat(64),
|
|
tags: [
|
|
["bolt11", bolt11],
|
|
["description", zapRequestString],
|
|
],
|
|
};
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [wrongPubkeyReceipt];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /no compliant zap receipt/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — receipt with invalid signature
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenReceiptSignatureInvalid() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const receipt = {
|
|
kind: 9735,
|
|
pubkey: "b".repeat(64),
|
|
tags: [
|
|
["bolt11", bolt11],
|
|
["description", zapRequestString],
|
|
],
|
|
};
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => false,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [receipt];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /no compliant zap receipt/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — successful validation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateReceiptSuccess() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const receipt = {
|
|
kind: 9735,
|
|
pubkey: "b".repeat(64),
|
|
tags: [
|
|
["bolt11", bolt11],
|
|
["description", zapRequestString],
|
|
],
|
|
};
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [receipt];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "passed");
|
|
assert.equal(result.event, receipt);
|
|
assert.deepEqual(result.checkedRelays, relays);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — override injection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateReceiptWithOverrides() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const receipt = {
|
|
kind: 9735,
|
|
pubkey: "b".repeat(64),
|
|
tags: [
|
|
["bolt11", bolt11],
|
|
["description", zapRequestString],
|
|
],
|
|
};
|
|
|
|
let listEventsCalled = false;
|
|
let createPoolCalled = false;
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
amountSats: 42,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
createPool(tools) {
|
|
createPoolCalled = true;
|
|
return {
|
|
list: async () => [],
|
|
close() {},
|
|
};
|
|
},
|
|
listEvents(pool, poolRelays, filters) {
|
|
listEventsCalled = true;
|
|
assert.deepEqual(poolRelays, relays);
|
|
return [receipt];
|
|
},
|
|
getAmountFromBolt11: () => 42,
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "passed");
|
|
assert(createPoolCalled, "createPool override should be called");
|
|
assert(listEventsCalled, "listEvents override should be called");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — nostrTools unavailable
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenNostrToolsUnavailable() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: 42, // truthy but not an object → triggers "unavailable"
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /unavailable/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — nostrTools missing validate/verify
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenToolsMissingValidators() {
|
|
const zapEvent = buildZapRequest();
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: { someOtherProp: true },
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /missing/i);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — invoice from payment object
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateExtractsInvoiceFromPayment() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const receipt = {
|
|
kind: 9735,
|
|
pubkey: "b".repeat(64),
|
|
tags: [
|
|
["bolt11", bolt11],
|
|
["description", zapRequestString],
|
|
],
|
|
};
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
payment: { bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [receipt];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "passed");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateZapReceipt — receipt missing description tag
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function testValidateFailsWhenReceiptMissingDescription() {
|
|
const relays = ["wss://relay.test"];
|
|
const zapEvent = buildZapRequest({ relays });
|
|
const zapRequestString = JSON.stringify(zapEvent);
|
|
const descriptionHash = computeZapRequestHash(zapRequestString);
|
|
const bolt11 = buildTestBolt11Invoice({ descriptionHashHex: descriptionHash });
|
|
|
|
const receiptNoDesc = {
|
|
kind: 9735,
|
|
pubkey: "b".repeat(64),
|
|
tags: [["bolt11", bolt11]],
|
|
};
|
|
|
|
const result = await validateZapReceipt(
|
|
{
|
|
zapRequest: zapRequestString,
|
|
metadata: { nostrPubkey: "b".repeat(64) },
|
|
invoice: { invoice: bolt11 },
|
|
},
|
|
{
|
|
nostrTools: {
|
|
validateEvent: () => true,
|
|
verifyEvent: () => true,
|
|
SimplePool: class {
|
|
async list() {
|
|
return [receiptNoDesc];
|
|
}
|
|
close() {}
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.equal(result.status, "failed");
|
|
assert.match(result.reason, /no compliant zap receipt/i);
|
|
}
|
|
|
|
// Run all async tests
|
|
await testValidateSkippedWhenNoZapRequest();
|
|
await testValidateSkippedWhenZapRequestIsEmpty();
|
|
await testValidateFailsWhenZapRequestIsInvalidJson();
|
|
await testValidateFailsWhenNoRelaysInZapRequest();
|
|
await testValidateFailsWhenNoInvoice();
|
|
await testValidateFailsOnDescriptionHashMismatch();
|
|
await testValidateFailsWhenInvoiceLacksDescriptionHash();
|
|
await testValidateFailsOnAmountMismatch();
|
|
await testValidateFailsWhenMetadataMissingPubkey();
|
|
await testValidateFailsWhenNoReceiptOnRelays();
|
|
await testValidateFailsWhenReceiptPubkeyDoesNotMatch();
|
|
await testValidateFailsWhenReceiptSignatureInvalid();
|
|
await testValidateReceiptSuccess();
|
|
await testValidateReceiptWithOverrides();
|
|
await testValidateFailsWhenNostrToolsUnavailable();
|
|
await testValidateFailsWhenToolsMissingValidators();
|
|
await testValidateExtractsInvoiceFromPayment();
|
|
await testValidateFailsWhenReceiptMissingDescription();
|
|
|
|
console.log("zap-receipt-validator tests passed");
|