@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
823 lines (668 loc) • 34.5 kB
text/typescript
import { SignerGenerator, TestFixture } from "../../test";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { NDKEvent } from ".";
import { NDK } from "../ndk";
import { NDKRelay } from "../relay";
import { NDKRelaySet } from "../relay/sets";
import { NDKPrivateKeySigner } from "../signers/private-key";
import { NDKUser } from "../user";
import type { NIP73EntityType } from "./nip73";
import { NDKKind } from "./kinds";
const ndk = new NDK();
describe("NDKEvent", () => {
let event: NDKEvent;
let user1: NDKUser;
let user2: NDKUser;
beforeEach(() => {
user1 = new NDKUser({
npub: "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
});
user2 = new NDKUser({
npub: "npub1nnn379gxen6tn8erft6fh43q905g82q0jks4t3hf58pkl4l8srrsyjkzrt",
});
event = new NDKEvent(ndk, { kind: 1 });
});
describe("publish", () => {
it("stores the relays where the event was successfully published to", async () => {
const relay1 = new NDKRelay("wss://relay1.nos.dev", undefined, ndk);
const relay2 = new NDKRelay("wss://relay2.nos.dev", undefined, ndk);
const relay3 = new NDKRelay("wss://relay3.nos.dev", undefined, ndk);
const relaySet = new NDKRelaySet(new Set([relay1, relay2, relay3]), ndk);
relaySet.publish = vi.fn().mockResolvedValue(new Set([relay1, relay2]));
event.kind = 5;
await event.sign(NDKPrivateKeySigner.generate());
const result = await event.publish(relaySet);
expect(result).toEqual(new Set([relay1, relay2]));
});
// // Tests using RelayMock for publish behavior
// it('publish method waits for connecting relays before publishing using RelayMock', async () => {
// const relay1 = new RelayMock('wss://relay1.test', { connectionDelay: 50, autoConnect: true }) as unknown as NDKRelay;
// const relay2 = new RelayMock('wss://relay2.test', { connectionDelay: 100, autoConnect: true }) as unknown as NDKRelay;
// const relaySet = new NDKRelaySet(new Set([relay1, relay2]), ndk);
// const event = new NDKEvent(ndk);
// event.kind = 1;
// event.content = "Hello, world!";
// await event.sign(NDKPrivateKeySigner.generate());
// const publishSpy1 = vi.spyOn(relay1, 'publish');
// const publishSpy2 = vi.spyOn(relay2, 'publish');
// await event.publish(relaySet);
// expect(publishSpy1).toHaveBeenCalled();
// expect(publishSpy2).toHaveBeenCalled();
// });
// it('publish method respects timeout when waiting for relays to connect using RelayMock', async () => {
// const relay1 = new RelayMock('wss://relay1.test', { connectionDelay: 0, autoConnect: true }) as unknown as NDKRelay;
// const slowRelay = new RelayMock('wss://slow.test', { connectionDelay: 500, autoConnect: true }) as unknown as NDKRelay;
// const relaySet = new NDKRelaySet(new Set([relay1, slowRelay]), ndk);
// const event = new NDKEvent(ndk);
// event.kind = 1;
// event.content = "Hello, world!";
// await event.sign(NDKPrivateKeySigner.generate());
// const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// const publishSpy1 = vi.spyOn(relay1, 'publish');
// const publishSpySlow = vi.spyOn(slowRelay, 'publish');
// await event.publish(relaySet, 100);
// expect(consoleWarnSpy).toHaveBeenCalledWith(
// expect.stringContaining("Timeout waiting for relays to connect"),
// expect.any(Error)
// );
// expect(publishSpy1).toHaveBeenCalled();
// expect(publishSpySlow).not.toHaveBeenCalled();
// consoleWarnSpy.mockRestore();
// });
});
describe("deduplicationKey", () => {
it("returns <kind>:<pubkey> for kinds 0", () => {
event.pubkey = user1.pubkey;
event.kind = 0;
const result = event.deduplicationKey();
expect(result).toEqual(`0:${user1.pubkey}`);
});
it("returns <kind>:<pubkey> for kinds 3", () => {
event.pubkey = user1.pubkey;
event.kind = 3;
const result = event.deduplicationKey();
expect(result).toEqual(`3:${user1.pubkey}`);
});
it("returns tagId for other kinds", () => {
event.kind = 2;
const spy = vi.spyOn(event, "tagId").mockReturnValue("mockTagId");
const result = event.deduplicationKey();
expect(result).toEqual("mockTagId");
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it("returns parameterized tagId for kinds between 30k and 40k", () => {
event.kind = 35000;
const spy = vi.spyOn(event, "tagId").mockReturnValue("parameterizedTagId");
const result = event.deduplicationKey();
expect(result).toEqual("parameterizedTagId");
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
describe("tag", () => {
it("tags a user without a marker", () => {
event.tag(user2);
expect(event.tags).toEqual([["p", user2.pubkey]]);
});
it("tags a user with a marker", () => {
event.tag(user2, "author");
expect(event.tags).toEqual([["p", user2.pubkey, "", "author"]]);
});
it("tags an event without a marker", () => {
const otherEvent = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
otherEvent.id = "123";
event.tag(otherEvent);
expect(event.tags).toEqual([
["e", otherEvent.id, "", "", otherEvent.pubkey],
["p", user1.pubkey],
]);
});
it("tags an event with a marker", () => {
const event = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
const otherEvent = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
otherEvent.id = "123";
event.tag(otherEvent, "marker");
expect(event.tags).toEqual([["e", otherEvent.id, "", "marker", otherEvent.pubkey]]);
});
it("tags an event author when it's different from the signing user", () => {
const event = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
const otherEvent = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user2.pubkey });
event.tag(otherEvent);
expect(event.tags).toEqual([
["e", otherEvent.id, "", "", otherEvent.pubkey],
["p", user2.pubkey],
]);
});
it("does not tag an event author when it's the same as the signing user", () => {
const event = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
const otherEvent = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user1.pubkey });
otherEvent.id = "abc";
event.tag(otherEvent);
expect(event.tags).toEqual([["e", otherEvent.id, "", "", otherEvent.pubkey]]);
});
it("does not re-tag the same user", () => {
const otherEvent = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user2.pubkey });
otherEvent.id = "abc";
const otherEvent2 = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user2.pubkey });
otherEvent2.id = "def";
event.tag(otherEvent);
event.tag(otherEvent2);
expect(event.tags).toEqual([
["e", otherEvent.id, "", "", otherEvent.pubkey],
["p", user2.pubkey],
["e", otherEvent2.id, "", "", otherEvent2.pubkey],
]);
});
});
describe("fetchEvents", () => {
it("correctly handles a relay sending old replaced events", () => {
// Create a dedupEvent function similar to what the NDK uses
const dedupEvent = (existingEvent: NDKEvent, newEvent: NDKEvent) => {
// Keep the newer event based on created_at
if (newEvent.created_at! > existingEvent.created_at!) {
return newEvent;
}
return existingEvent;
};
// Create events with the same kind/pubkey but different timestamps
const eventData = {
kind: 30001,
tags: [["d", "test"]],
content: "content",
pubkey: user1.pubkey,
};
const event1 = new NDKEvent(ndk, {
kind: 30000,
content: eventData.content,
pubkey: eventData.pubkey,
});
event1.tags = eventData.tags;
event1.created_at = Math.floor(Date.now() / 1000 - 3600);
event1.id = "id1";
event1.sig = "sig1";
const event2 = new NDKEvent(ndk, {
kind: 30000,
content: eventData.content,
pubkey: eventData.pubkey,
});
event2.tags = eventData.tags;
event2.created_at = Math.floor(Date.now() / 1000);
event2.id = "id2";
event2.sig = "sig2";
// Test the deduplication logic directly
const events = new Map<string, NDKEvent>();
// Add the older event first
const dedupKey1 = event1.deduplicationKey();
events.set(dedupKey1, event1);
// Then add the newer event
const dedupKey2 = event2.deduplicationKey();
const existingEvent = events.get(dedupKey2);
if (existingEvent) {
events.set(dedupKey2, dedupEvent(existingEvent, event2));
} else {
events.set(dedupKey2, event2);
}
// Verify that only the newest event was kept (deduplication)
expect(events.size).toBe(1);
const dedupedEvent = events.values().next().value;
expect(dedupedEvent).toBeDefined();
expect(dedupedEvent?.id).toEqual(event2.id);
});
});
describe("toNostrEvent", () => {
it("returns a NostrEvent object", async () => {
const nostrEvent = await event.toNostrEvent();
expect(nostrEvent).toHaveProperty("created_at");
expect(nostrEvent).toHaveProperty("content");
expect(nostrEvent).toHaveProperty("tags");
expect(nostrEvent).toHaveProperty("kind");
expect(nostrEvent).toHaveProperty("pubkey");
expect(nostrEvent).toHaveProperty("id");
});
describe("mentions", () => {
it("handles NIP-27 mentions", async () => {
event.content =
"hello nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft!";
const nostrEvent = await event.toNostrEvent();
const mentionTag = nostrEvent.tags.find(
(t) =>
t[0] === "p" &&
t[1] === "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"
);
expect(mentionTag).toBeTruthy();
});
});
});
describe("referenceTags", () => {
it("returns the correct tag for referencing the event", () => {
const event1 = new NDKEvent(ndk, { kind: 30000, content: "", pubkey: user1.pubkey });
event1.tags.push(["d", "d-code"]);
event1.id = "eventid1";
const event2 = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user2.pubkey });
event2.id = "eventid2";
expect(event1.referenceTags()).toEqual([
["a", `30000:${user1.pubkey}:d-code`],
["e", "eventid1", "", "", user1.pubkey],
["p", user1.pubkey],
]);
expect(event2.referenceTags()).toEqual([
["e", "eventid2", "", "", user2.pubkey],
["p", user2.pubkey],
]);
});
it("adds a marker to the reference tag if provided", () => {
const nip33event = new NDKEvent(ndk, { kind: 30000, pubkey: user1.pubkey });
nip33event.tags.push(["d", "d-code"]);
nip33event.id = "eventid1";
const event = new NDKEvent(ndk, { kind: 1, pubkey: user2.pubkey });
event.id = "eventid2";
expect(nip33event.referenceTags("marker")).toEqual([
["a", `30000:${user1.pubkey}:d-code`, "", "marker"],
["e", "eventid1", "", "marker", user1.pubkey],
["p", user1.pubkey],
]);
expect(event.referenceTags("marker")).toEqual([
["e", "eventid2", "", "marker", user2.pubkey],
["p", user2.pubkey],
]);
});
it("adds a marker to the reference tag if provided with relay if its set", () => {
const relay = new NDKRelay("wss://relay.nos.dev/", undefined, ndk);
const nip33event = new NDKEvent(ndk, {
kind: 30000,
content: "",
pubkey: user1.pubkey,
});
nip33event.tags.push(["d", "d-code"]);
nip33event.id = "eventid1";
nip33event.relay = relay;
const event = new NDKEvent(ndk, { kind: 1, content: "", pubkey: user2.pubkey });
event.id = "eventid2";
expect(nip33event.referenceTags("marker")).toEqual([
["a", `30000:${user1.pubkey}:d-code`, "wss://relay.nos.dev/", "marker"],
["e", "eventid1", "wss://relay.nos.dev/", "marker", user1.pubkey],
["p", user1.pubkey],
]);
expect(event.referenceTags("marker")).toEqual([
["e", "eventid2", "", "marker", user2.pubkey],
["p", user2.pubkey],
]);
});
it("returns h tags if they are present", () => {
const event = new NDKEvent(ndk, { kind: 1, pubkey: user1.pubkey });
event.id = "eventid";
event.tags.push(["h", "group-id"]);
expect(event.referenceTags()).toEqual([
["e", "eventid", "", "", user1.pubkey],
["h", "group-id"],
["p", user1.pubkey],
]);
});
});
describe("tagId", () => {
it("returns the correct tagId for a given event", () => {
const event1 = new NDKEvent(ndk, { kind: 30000, content: "", pubkey: "pubkey" });
event1.tags.push(["d", "d-code"]);
const event2 = new NDKEvent(ndk, { kind: 1, content: "" });
event2.id = "eventid";
expect(event1.tagId()).toEqual("30000:pubkey:d-code");
expect(event2.tagId()).toEqual("eventid");
});
});
describe("replacableDTag", () => {
it("returns the correct tagId for a given event", () => {
const event1 = new NDKEvent(ndk, { kind: 30000, content: "", pubkey: "" });
event1.tags.push(["d", "d-code"]);
const event2 = new NDKEvent(ndk, { kind: 1, content: "" });
expect(event1.replaceableDTag()).toEqual("d-code");
expect(() => event2.replaceableDTag()).toThrowError(
"Event is not a parameterized replaceable event"
);
});
});
describe("tagExternal", () => {
it("correctly tags a URL", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("https://example.com/article/123#nostr", "url");
expect(event.tags).toContainEqual(["i", "https://example.com/article/123"]);
expect(event.tags).toContainEqual(["k", "https://example.com"]);
});
it("correctly tags a hashtag", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("NostrTest", "hashtag");
expect(event.tags).toContainEqual(["i", "#nostrtest"]);
expect(event.tags).toContainEqual(["k", "#"]);
});
it("correctly tags a geohash", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("u4pruydqqvj", "geohash");
expect(event.tags).toContainEqual(["i", "geo:u4pruydqqvj"]);
expect(event.tags).toContainEqual(["k", "geo"]);
});
it("correctly tags an ISBN", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("978-3-16-148410-0", "isbn");
expect(event.tags).toContainEqual(["i", "isbn:9783161484100"]);
expect(event.tags).toContainEqual(["k", "isbn"]);
});
it("correctly tags a podcast GUID", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("e32b4890-b9ea-4aef-a0bf-54b787833dc5", "podcast:guid");
expect(event.tags).toContainEqual([
"i",
"podcast:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5",
]);
expect(event.tags).toContainEqual(["k", "podcast:guid"]);
});
it("correctly tags an ISAN", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("1881-66C7-3420-0000-7-9F3A-0245-U", "isan");
expect(event.tags).toContainEqual(["i", "isan:1881-66C7-3420-0000"]);
expect(event.tags).toContainEqual(["k", "isan"]);
});
it("correctly tags a DOI", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal("10.1000/182", "doi");
expect(event.tags).toContainEqual(["i", "doi:10.1000/182"]);
expect(event.tags).toContainEqual(["k", "doi"]);
});
it("adds a marker URL when provided", () => {
const event = new NDKEvent(ndk, { kind: 1 });
event.tagExternal(
"e32b4890-b9ea-4aef-a0bf-54b787833dc5",
"podcast:guid",
"https://example.com/marker"
);
expect(event.tags).toContainEqual([
"i",
"podcast:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5",
"https://example.com/marker",
]);
expect(event.tags).toContainEqual(["k", "podcast:guid"]);
});
it("throws an error for unsupported entity types", () => {
const event = new NDKEvent(ndk, { kind: 1 });
expect(() => {
event.tagExternal("test", "unsupported" as NIP73EntityType);
}).toThrow("Unsupported NIP-73 entity type: unsupported");
});
});
describe("reply", () => {
let fixture: TestFixture;
let _alice: NDKUser;
let _bob: NDKUser;
let _carol: NDKUser;
beforeEach(async () => {
fixture = new TestFixture();
_alice = await fixture.getUser("alice");
_bob = await fixture.getUser("bob");
_carol = await fixture.getUser("carol");
// Set up signers
fixture.setupSigner("alice");
});
describe("replies to kind:1 events", () => {
it("creates a reply using a kind 1 event", async () => {
// Create root event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply
const reply = op.reply();
expect(reply.kind).toBe(1);
});
it("carries over the root event of the OP", async () => {
// Create thread with root and one reply
const [root, reply1] = await fixture.eventFactory.createEventChain(
"Hello world",
"alice",
[{ content: "First reply", author: "bob" }]
);
// Create a second reply to the first reply
fixture.setupSigner("carol");
const reply2 = reply1.reply();
// Verify it has the root tag
expect(reply2.tags).toContainEqual(["e", root.id, "", "root", root.pubkey]);
expect(reply2.tags).toContainEqual(["p", root.pubkey]);
});
it("adds a root marker for root events", async () => {
// Create root event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply
fixture.setupSigner("bob");
const reply = op.reply();
// Verify it has the root tag
expect(reply.tags).toContainEqual(["e", op.id, "", "root", op.pubkey]);
expect(reply.tags).toContainEqual(["p", op.pubkey]);
});
it("adds a reply marker for non-root events", async () => {
// Create thread with root and one reply
const [_root, reply1] = await fixture.eventFactory.createEventChain(
"Hello world",
"alice",
[{ content: "First reply", author: "bob" }]
);
// Create a second reply to the first reply
fixture.setupSigner("carol");
const reply2 = reply1.reply();
// Verify it has the proper reply tag for the parent
expect(reply2.tags).toContainEqual(["e", reply1.id, "", "reply", reply1.pubkey]);
expect(reply2.tags).toContainEqual(["p", reply1.pubkey]);
});
it("p-tags the author of the event", async () => {
// Create root event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply
fixture.setupSigner("bob");
const reply = op.reply();
// Verify it tags the author
expect(reply.tags).toContainEqual(["p", op.pubkey]);
});
it("carries over the p-tags from the root event", async () => {
// Create thread with root and one reply
const [root, reply1] = await fixture.eventFactory.createEventChain(
"Hello world",
"alice",
[{ content: "First reply", author: "bob" }]
);
// Create a second reply to the first reply
fixture.setupSigner("carol");
const reply2 = reply1.reply();
// Verify it has tags for both authors
expect(reply2.tags).toContainEqual(["p", root.pubkey]);
expect(reply2.tags).toContainEqual(["p", reply1.pubkey]);
});
});
describe("replies to other kinds", () => {
let root: NDKEvent;
beforeEach(async () => {
root = new NDKEvent(ndk, { kind: NDKKind.Article, content: "Hello world" });
root.tags.push(["d", "test"]);
await SignerGenerator.sign(root, "alice");
});
it("creates a reply using a kind 1111 event", async () => {
const reply1 = root.reply();
expect(reply1.kind).toBe(1111); // GenericReply kind
});
it("tags the root event or scope using an appropriate uppercase tag (e.g., 'A', 'E', 'I')", async () => {
const reply1 = root.reply();
expect(reply1.tags).toContainEqual(["A", root.tagId(), ""]);
});
it("tags the root event with an 'a' for addressable events when it's a top level reply", async () => {
const reply1 = root.reply();
expect(reply1.tags).toContainEqual(["A", root.tagId(), ""]);
expect(reply1.tags).toContainEqual(["a", root.tagId(), ""]);
});
it("p-tags the author of the root event", async () => {
const reply1 = root.reply();
expect(reply1.tags).toContainEqual(["P", root.pubkey]);
});
it("p-tags the author of the reply event", async () => {
const reply1 = root.reply();
// Create a second reply to the first reply
const reply2 = reply1.reply();
expect(reply2.tags).toContainEqual(["p", reply1.pubkey]);
});
it("p-tags the author of the root event only once when it's the root reply", async () => {
const reply1 = root.reply();
expect(reply1.tags).toContainEqual(["p", root.pubkey]);
expect(reply1.tags.filter((t) => t[0] === "p")).toHaveLength(1);
});
it("p-tags the author of the root and reply events", async () => {
const reply1 = root.reply();
// Create a second reply to the first reply
const reply2 = reply1.reply();
expect(reply2.tags).toContainEqual(["P", root.pubkey]);
expect(reply2.tags).toContainEqual(["p", reply1.pubkey]);
});
it("tags the root event or scope using an appropriate uppercase tag with the pubkey when it's an E tag", async () => {
const k20event = new NDKEvent(ndk, { kind: 20, content: "Hello world" });
// Create a reply to the kind 20 event
const reply1 = k20event.reply();
expect(reply1.tags).toContainEqual(["E", k20event.tagId(), "", k20event.pubkey]);
});
it("tags the parent item using an appropriate lowercase tag (e.g., 'a', 'e', 'i')", async () => {
const reply1 = root.reply();
const reply2 = reply1.reply();
expect(reply2.tags).toContainEqual(["A", root.tagId(), ""]);
expect(reply2.tags).toContainEqual(["e", reply1.tagId(), "", reply1.pubkey]);
});
it("adds a 'K' tag to specify the root kind", async () => {
const reply1 = root.reply();
expect(reply1.tags).toContainEqual(["K", root.kind?.toString()]);
});
it("adds a 'k' tag to specify the parent kind", async () => {
const reply1 = root.reply();
const reply2 = reply1.reply();
expect(reply2.tags).toContainEqual(["k", reply1.kind?.toString()]);
});
});
describe("forceNip22 parameter", () => {
it("maintains backward compatibility when forceNip22 is not provided", async () => {
// Create a kind 1 event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply without forceNip22 parameter
fixture.setupSigner("bob");
const reply = op.reply();
// Should behave as before - kind 1 reply to kind 1 event
expect(reply.kind).toBe(1);
expect(reply.tags).toContainEqual(["e", op.id, "", "root", op.pubkey]);
expect(reply.tags).toContainEqual(["p", op.pubkey]);
});
it("maintains backward compatibility when forceNip22 is explicitly false", async () => {
// Create a kind 1 event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply with forceNip22: false
fixture.setupSigner("bob");
const reply = op.reply(false);
// Should behave as before - kind 1 reply to kind 1 event
expect(reply.kind).toBe(1);
expect(reply.tags).toContainEqual(["e", op.id, "", "root", op.pubkey]);
expect(reply.tags).toContainEqual(["p", op.pubkey]);
});
it("forces kind 1111 when replying to kind 1 event with forceNip22: true", async () => {
// Create a kind 1 event
const op = await fixture.eventFactory.createSignedTextNote("Hello world", "alice");
// Create reply with forceNip22: true
fixture.setupSigner("bob");
const reply = op.reply(true);
// Should create kind 1111 reply instead of kind 1
expect(reply.kind).toBe(1111); // GenericReply kind
// Should have proper NIP-22 tagging
expect(reply.tags).toContainEqual(["e", op.id, "", op.pubkey]);
expect(reply.tags).toContainEqual(["E", op.id, "", op.pubkey]);
expect(reply.tags).toContainEqual(["K", "1"]);
expect(reply.tags).toContainEqual(["k", "1"]);
expect(reply.tags).toContainEqual(["P", op.pubkey]);
expect(reply.tags).toContainEqual(["p", op.pubkey]);
});
it("forces kind 1111 when replying to non-kind 1 event with forceNip22: true", async () => {
// Create a non-kind 1 event (Article)
const article = new NDKEvent(ndk, {
kind: NDKKind.Article,
content: "Article content",
});
article.tags.push(["d", "test-article"]);
await SignerGenerator.sign(article, "alice");
// Create reply with forceNip22: true
fixture.setupSigner("bob");
const reply = article.reply(true);
// Should create kind 1111 reply (same as default behavior for non-kind 1)
expect(reply.kind).toBe(1111); // GenericReply kind
// Should have proper NIP-22 tagging for addressable event
expect(reply.tags).toContainEqual(["a", article.tagId(), ""]);
expect(reply.tags).toContainEqual(["A", article.tagId(), ""]);
expect(reply.tags).toContainEqual(["K", article.kind?.toString()]);
expect(reply.tags).toContainEqual(["k", article.kind?.toString()]);
expect(reply.tags).toContainEqual(["P", article.pubkey]);
expect(reply.tags).toContainEqual(["p", article.pubkey]);
});
it("preserves existing thread structure when using forceNip22 on kind 1 events", async () => {
// Create thread with root and one reply
const [root, reply1] = await fixture.eventFactory.createEventChain(
"Hello world",
"alice",
[{ content: "First reply", author: "bob" }]
);
// Create a second reply to the first reply with forceNip22: true
fixture.setupSigner("carol");
const reply2 = reply1.reply(true);
// Should create kind 1111 reply
expect(reply2.kind).toBe(1111);
// Should preserve thread structure with proper tagging
// When forceNip22 is true, it doesn't preserve the traditional thread structure
// Instead it uses NIP-22 style tagging
expect(reply2.tags).toContainEqual(["e", reply1.id, "", reply1.pubkey]);
expect(reply2.tags).toContainEqual(["E", reply1.id, "", reply1.pubkey]);
expect(reply2.tags).toContainEqual(["p", root.pubkey]);
expect(reply2.tags).toContainEqual(["p", reply1.pubkey]);
// Should have NIP-22 specific tags
expect(reply2.tags).toContainEqual(["k", "1"]);
});
it("handles mixed thread with both kind 1 and kind 1111 replies", async () => {
// Create root kind 1 event
const root = await fixture.eventFactory.createSignedTextNote("Root post", "alice");
// Create normal kind 1 reply
fixture.setupSigner("bob");
const normalReply = root.reply();
expect(normalReply.kind).toBe(1);
// Create forced NIP-22 reply to the same root
fixture.setupSigner("carol");
const forcedReply = root.reply(true);
expect(forcedReply.kind).toBe(1111);
// Both should reference the same root but with different tagging styles
expect(normalReply.tags).toContainEqual(["e", root.id, "", "root", root.pubkey]);
expect(forcedReply.tags).toContainEqual(["e", root.id, "", root.pubkey]);
expect(forcedReply.tags).toContainEqual(["E", root.id, "", root.pubkey]);
});
});
});
describe("react", () => {
it("adds a k-tag when reacting to non-kind-1 events", async () => {
const targetEvent = new NDKEvent(ndk, { kind: 6, content: "repost content" });
targetEvent.id = "target-event-id";
targetEvent.pubkey = user1.pubkey;
const mockSigner = {
user: vi.fn().mockResolvedValue(user2),
sign: vi.fn().mockResolvedValue(undefined),
};
ndk.signer = mockSigner as any;
const reaction = await targetEvent.react("👍", false);
expect(reaction.kind).toBe(NDKKind.Reaction);
expect(reaction.content).toBe("👍");
expect(reaction.tags).toContainEqual(["k", "6"]);
});
it("does not add a k-tag when reacting to kind-1 events", async () => {
const targetEvent = new NDKEvent(ndk, { kind: 1, content: "text note" });
targetEvent.id = "target-event-id";
targetEvent.pubkey = user1.pubkey;
const mockSigner = {
user: vi.fn().mockResolvedValue(user2),
sign: vi.fn().mockResolvedValue(undefined),
};
ndk.signer = mockSigner as any;
const reaction = await targetEvent.react("👍", false);
expect(reaction.kind).toBe(NDKKind.Reaction);
expect(reaction.content).toBe("👍");
expect(reaction.tags.find((tag) => tag[0] === "k")).toBeUndefined();
});
});
});