@nostr-fetch/kernel
Version:
Kernel of nostr-fetch
439 lines (398 loc) • 12 kB
text/typescript
import { fail } from "node:assert";
import { describe, expect, test } from "vitest";
import {
FilterMatcher,
type NostrEvent,
isNoticeForReqError,
isValidTagQueryKey,
parseR2CMessage,
validateEvent,
} from "./nostr";
describe("isValidTagQueryKey", () => {
test("returns true for valid tag query keys", () => {
const validKeys = ["#p", "#e", "#a", "#I"];
for (const key of validKeys) {
expect(isValidTagQueryKey(key)).toBe(true);
}
});
test("returns false for invalid tag query keys", () => {
const invalidKeys = ["", "p", "#", "#client", "#emoji", "#expiration", "#relay", "#-"];
for (const key of invalidKeys) {
expect(isValidTagQueryKey(key)).toBe(false);
}
});
});
const validEventJSON = `{
"id": "381e2ea15a5b16f4ebdaa68ed3d9a112dc4ea6cc95641ef7eb57f1ec826f07e4",
"pubkey": "d1d1747115d16751a97c239f46ec1703292c3b7e9988b9ebdd4ec4705b15ed44",
"created_at": 1678892115,
"kind": 1,
"tags": [],
"content": "テスト用",
"sig": "644d8a7267d624bd861d4f53e97a637a6aea9c1f7e08e4f3cbe68087dbcd6aa9925c58fedd650815fa56e6d6733064e5bba1e58fc2210257bb30e717ba63b46c"
}`;
describe("parseR2CMessage", () => {
test("parses EVENT message", () => {
const eventJSON = `["EVENT", "sub_id", ${validEventJSON}]`;
const parsed = parseR2CMessage(eventJSON);
if (parsed === undefined) {
fail("parsing failed unexpectedly");
}
if (parsed[0] !== "EVENT") {
fail(`unexpected message type: ${parsed[0]}`);
}
const [, subId, event] = parsed;
expect(subId).toBe("sub_id");
expect(event).toStrictEqual(JSON.parse(validEventJSON));
});
test("parses EOSE message", () => {
const eoseJSON = `["EOSE", "sub_id"]`;
const parsed = parseR2CMessage(eoseJSON);
if (parsed === undefined) {
fail("parsing failed unexpectedly");
}
if (parsed[0] !== "EOSE") {
fail(`unexpected message type: ${parsed[0]}`);
}
const [, subId] = parsed;
expect(subId).toBe("sub_id");
});
test("parses CLOSED message", () => {
const closedJSON = `["CLOSED", "sub_id", "reason"]`;
const parsed = parseR2CMessage(closedJSON);
if (parsed === undefined) {
fail("parsing failed unexpectedly");
}
if (parsed[0] !== "CLOSED") {
fail(`unexpected message type: ${parsed[0]}`);
}
const [, subId, reason] = parsed;
expect(subId).toBe("sub_id");
expect(reason).toBe("reason");
});
test("parses NOTICE message", () => {
const noticeJSON = `["NOTICE", "error!"]`;
const parsed = parseR2CMessage(noticeJSON);
if (parsed === undefined) {
fail("parsing failed unexpecedly");
}
if (parsed[0] !== "NOTICE") {
fail(`unexpected message type: ${parsed[0]}`);
}
const [, msg] = parsed;
expect(msg).toBe("error!");
});
test("fails on malformed message", () => {
const malformedMsgJSON = ["", `{"type":"OK"}`, "[]", "[1]"];
for (const msgJSON of malformedMsgJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
test("fails on unknown/ignored message type", () => {
const unknownMsgJSON = [`["UNKNOWN"]`, `["OK", "event_id", true, ""]`, `["AUTH", "challenge"]`];
for (const msgJSON of unknownMsgJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
test("fails on malformed EVENT message", () => {
const malformedEventJSON = [
`["EVENT"]`,
`["EVENT", "sub_id"]`,
`["EVENT", "sub_id", 42]`,
`["EVENT", "sub_id", null]`,
`["EVENT", 100, ${validEventJSON}]`,
];
for (const msgJSON of malformedEventJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
test("fails on malformed EOSE message", () => {
const malformedEoseJSON = [`["EOSE"]`, `["EOSE", 42]`, `["EOSE", null]`];
for (const msgJSON of malformedEoseJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
test("fails on malformed CLOSED message", () => {
const malformedClosedJSON = [
`["CLOSED"]`,
`["CLOSED", 42]`,
`["CLOSED", null]`,
`["CLOSED", "sub_id", null]`,
];
for (const msgJSON of malformedClosedJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
test("fails on malformed NOTICE message", () => {
const malformedNoticeJSON = [`["NOTICE"]`];
for (const msgJSON of malformedNoticeJSON) {
expect(parseR2CMessage(msgJSON)).toBeUndefined();
}
});
});
const validEvent = JSON.parse(validEventJSON) as Record<keyof NostrEvent, unknown>;
const modifiedEvent = (
mod: Partial<Record<keyof NostrEvent, unknown>>,
): Record<string, unknown> => {
return { ...validEvent, ...mod };
};
const propDeletedEvent = (key: keyof NostrEvent): Record<string, unknown> => {
const copy = { ...validEvent };
delete copy[key];
return copy;
};
describe("validateEvent", () => {
test("fails on malformed id", () => {
const malformedEvs = [
propDeletedEvent("id"),
modifiedEvent({ id: 1 }),
modifiedEvent({ id: "not-sha256" }),
modifiedEvent({
id: "644d8a7267d624bd861d4f53e97a637a6aea9c1f7e08e4f3cbe68087dbcd6aa9925c58fedd650815fa56e6d6733064e5bba1e58fc2210257bb30e717ba63b46c", // invalid length
}),
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed pubkey", () => {
const malformedEvs = [
propDeletedEvent("pubkey"),
modifiedEvent({ pubkey: 1 }),
modifiedEvent({ pubkey: "not hex str" }),
modifiedEvent({
pubkey:
"644d8a7267d624bd861d4f53e97a637a6aea9c1f7e08e4f3cbe68087dbcd6aa9925c58fedd650815fa56e6d6733064e5bba1e58fc2210257bb30e717ba63b46c", // invalid length
}),
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed created_at", () => {
const malformedEvs = [
propDeletedEvent("created_at"),
modifiedEvent({ created_at: "2023-03-16T00:35:00" }),
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed kind", () => {
const malformedEvs = [propDeletedEvent("kind"), modifiedEvent({ kind: "text_note" })];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed tags", () => {
const malformedEvs = [
propDeletedEvent("tags"),
modifiedEvent({ tags: { non: "array" } }),
modifiedEvent({ tags: ["not array of array"] }),
modifiedEvent({ tags: [["contains non-string", 42]] }),
modifiedEvent({ tags: [["contains null", null]] }),
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed content", () => {
const malformedEvs = [
propDeletedEvent("content"),
modifiedEvent({ content: 42 }),
modifiedEvent({ content: null }),
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
test("fails on malformed sig", () => {
const malformedEvs = [
propDeletedEvent("sig"),
modifiedEvent({ sig: 1 }),
modifiedEvent({ sig: "not hex str" }),
modifiedEvent({
sig: "d1d1747115d16751a97c239f46ec1703292c3b7e9988b9ebdd4ec4705b15ed44",
}), // invalid length
];
for (const ev of malformedEvs) {
expect(validateEvent(ev)).toBe(false);
}
});
});
describe("FilterMatcher", () => {
const ev = {
id: "id1",
pubkey: "pk1",
created_at: 1629123456,
kind: 1,
tags: [
["e", "e1"],
["p", "p1"],
],
content: "",
sig: "sig",
};
test("should return true when event matches the filter", () => {
const filters = [
{},
{ ids: ["id1"] },
{ ids: ["id1", "id2"] },
{ kinds: [1] },
{ kinds: [1, 2] },
{ authors: ["pk1"] },
{ authors: ["pk1", "pk2"] },
{ "#e": ["e1"] },
{ "#e": ["e1", "e2"] },
{ "#p": ["p1"] },
{ "#p": ["p1", "p2"] },
{ since: 1629000000, until: 1630000000 },
{ since: 1629000000 },
{ since: 1629123456 }, // matches if since == created_at
{ until: 1630000000 },
{ until: 1629123456 }, // matches if until == created_at
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
since: 1629000000,
until: 1630000000,
},
];
for (const f of filters) {
const matcher = new FilterMatcher([f]);
expect(matcher.match(ev)).toBe(true);
}
});
test("should return false when event does not match the filter", () => {
const filters = [
{ ids: ["id2"] },
{ ids: ["id2", "id3"] },
{ kinds: [2] },
{ kinds: [2, 3] },
{ authors: ["pk2"] },
{ authors: ["pk2", "pk3"] },
{ "#e": ["e2"] },
{ "#e": ["e2", "e3"] },
{ "#p": ["p2"] },
{ "#p": ["p2", "p3"] },
{ "#a": ["a1"] }, // missing tag
{ since: 1630000000 },
{ until: 1629000000 },
// matches except one filter...
{
ids: ["id2"], // mismatch
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [2], // mismatch
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk2"], // mismatch
"#e": ["e1"],
"#p": ["p1"],
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e2"], // mismatch
"#p": ["p1"],
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p2"], // mismatch
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
"#a": ["a1"], // missing tag
since: 1629000000,
until: 1630000000,
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
since: 1630000000, // mismatch
},
{
ids: ["id1"],
kinds: [1],
authors: ["pk1"],
"#e": ["e1"],
"#p": ["p1"],
until: 1629000000, // mismatch
},
];
for (const f of filters) {
const matcher = new FilterMatcher([f]);
const matched = matcher.match(ev);
expect(matched).toBe(false);
}
});
test("should return true if at least one filter matches", () => {
const f = new FilterMatcher([
{ ids: ["id2"] },
{ kinds: [2] },
{ "#a": ["a1"] },
{ since: 1630000000 },
{ authors: ["pk1"] }, // matches
]);
expect(f.match(ev)).toBe(true);
});
test("should return false if no filters match", () => {
const f = new FilterMatcher([
{ ids: ["id2"] },
{ kinds: [2] },
{ "#a": ["a1"] },
{ since: 1630000000 },
{ authors: ["pk2"] },
]);
expect(f.match(ev)).toBe(false);
});
});
describe("isNoticeForReqError", () => {
test("returns true if notice message stands for REQ errors can be caused by nostr-fetch", () => {
const noticeForReqErr = [
"too many concurrent REQs",
"Subscription rejected: Too many subscriptions",
'invalid: "ids" must contain less than or equal to 1000',
'invalid: "[2].limit" must be less than or equal to 5000',
"message too large",
"Maximum concurrent subscription count reached",
];
for (const notice of noticeForReqErr) {
expect(isNoticeForReqError(notice)).toBe(true);
}
});
});