Move domain stuff to sub directory, clean up base classes
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, BLOCKED_RELAYS, NOTE, getTagValues, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {BlockedRelayList, BlockedRelayListBuilder} from "../src/kinds/BlockedRelayList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const r1 = "wss://relay.one.example/"
|
||||
const r2 = "wss://relay.two.example/"
|
||||
const r3 = "wss://relay.three.example/"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: BLOCKED_RELAYS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("BlockedRelayList", () => {
|
||||
it("reads relay urls from relay tags", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["relay", r1],
|
||||
["relay", r2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BlockedRelayList.fromEvent(event)
|
||||
|
||||
expect(list.urls().sort()).toEqual([r1, r2].sort())
|
||||
expect(list.includes(r1)).toBe(true)
|
||||
expect(list.includes(r3)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["relay", r1],
|
||||
["relay", r2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BlockedRelayList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(BLOCKED_RELAYS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relay").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder and normalizes urls", async () => {
|
||||
const tmpl = await new BlockedRelayListBuilder()
|
||||
.addRelay("wss://relay.one.example")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getTagValues("relay", tmpl.tags)).toEqual([normalizeRelayUrl("wss://relay.one.example")])
|
||||
})
|
||||
|
||||
it("setRelays replaces existing relays", async () => {
|
||||
const event = makeEvent({tags: [["relay", r1]]})
|
||||
const list = await BlockedRelayList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().setRelays([r2, r3]).toTemplate(signer)
|
||||
|
||||
expect(getTagValues("relay", tmpl.tags).sort()).toEqual([r2, r3].sort())
|
||||
})
|
||||
|
||||
it("round-trips public and private entries through encryption", async () => {
|
||||
const event = await new BlockedRelayListBuilder()
|
||||
.addRelay(r1)
|
||||
.addPrivate(["relay", r2])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(event.kind).toBe(BLOCKED_RELAYS)
|
||||
expect(getTagValues("relay", event.tags)).toEqual([r1])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await BlockedRelayList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.urls().sort()).toEqual([r1, r2].sort())
|
||||
|
||||
const publicOnly = await BlockedRelayList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.urls()).toEqual([r1])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through", async () => {
|
||||
const event = await new BlockedRelayListBuilder().addPrivate(["relay", r2]).toEvent(signer)
|
||||
const undecrypted = await BlockedRelayList.fromEvent(event)
|
||||
|
||||
const tmpl = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(BlockedRelayList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, BLOSSOM_SERVERS, NOTE, getTagValues, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {BlossomServerList, BlossomServerListBuilder} from "../src/kinds/BlossomServerList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const s1 = "https://blossom.one.example/"
|
||||
const s2 = "https://blossom.two.example/"
|
||||
const s3 = "https://blossom.three.example/"
|
||||
|
||||
const norm = (url: string) => normalizeRelayUrl(url)
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: BLOSSOM_SERVERS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("BlossomServerList", () => {
|
||||
it("reads server urls from server tags", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["server", s1],
|
||||
["server", s2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BlossomServerList.fromEvent(event)
|
||||
|
||||
expect(list.servers().sort()).toEqual([norm(s1), norm(s2)].sort())
|
||||
expect(list.includes(s1)).toBe(true)
|
||||
expect(list.includes(s3)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["server", s1],
|
||||
["server", s2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BlossomServerList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(BLOSSOM_SERVERS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "server").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder and normalizes urls", async () => {
|
||||
const tmpl = await new BlossomServerListBuilder().addServer(s1).toTemplate(signer)
|
||||
|
||||
expect(getTagValues("server", tmpl.tags)).toEqual([norm(s1)])
|
||||
})
|
||||
|
||||
it("setServers replaces existing servers", async () => {
|
||||
const event = makeEvent({tags: [["server", s1]]})
|
||||
const list = await BlossomServerList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().setServers([s2, s3]).toTemplate(signer)
|
||||
|
||||
expect(getTagValues("server", tmpl.tags).sort()).toEqual([norm(s2), norm(s3)].sort())
|
||||
})
|
||||
|
||||
it("round-trips public and private entries through encryption", async () => {
|
||||
const event = await new BlossomServerListBuilder()
|
||||
.addServer(s1)
|
||||
.addPrivate(["server", norm(s2)])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getTagValues("server", event.tags)).toEqual([norm(s1)])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await BlossomServerList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.servers().sort()).toEqual([norm(s1), norm(s2)].sort())
|
||||
|
||||
const publicOnly = await BlossomServerList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.servers()).toEqual([norm(s1)])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(BlossomServerList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {
|
||||
makeSecret,
|
||||
BOOKMARKS,
|
||||
NOTE,
|
||||
getEventTagValues,
|
||||
getTopicTagValues,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {BookmarkList, BookmarkListBuilder} from "../src/kinds/BookmarkList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const noteId = "11".repeat(32)
|
||||
const noteId2 = "22".repeat(32)
|
||||
const address = `30023:${"aa".repeat(32)}:article-1`
|
||||
const url = "https://example.com/post"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: BOOKMARKS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("BookmarkList", () => {
|
||||
it("reads mixed bookmark entries", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["e", noteId],
|
||||
["a", address],
|
||||
["t", "nostr"],
|
||||
["r", url],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BookmarkList.fromEvent(event)
|
||||
|
||||
expect(list.ids()).toEqual([noteId])
|
||||
expect(list.addresses()).toEqual([address])
|
||||
expect(list.topics()).toEqual(["nostr"])
|
||||
expect(list.urls()).toEqual([url])
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["e", noteId],
|
||||
["a", address],
|
||||
["t", "nostr"],
|
||||
["r", url],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await BookmarkList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(BOOKMARKS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "t").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "r").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new BookmarkListBuilder()
|
||||
.bookmarkPublicly(["e", noteId])
|
||||
.bookmarkPublicly(["t", "nostr"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getEventTagValues(tmpl.tags)).toEqual([noteId])
|
||||
expect(getTopicTagValues(tmpl.tags)).toEqual(["nostr"])
|
||||
})
|
||||
|
||||
it("removeBookmark removes by value", async () => {
|
||||
const event = makeEvent({tags: [["e", noteId], ["e", noteId2]]})
|
||||
const list = await BookmarkList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().removeBookmark(noteId).toTemplate(signer)
|
||||
|
||||
expect(getEventTagValues(tmpl.tags)).toEqual([noteId2])
|
||||
})
|
||||
|
||||
it("round-trips public and private bookmarks through encryption", async () => {
|
||||
const event = await new BookmarkListBuilder()
|
||||
.bookmarkPublicly(["e", noteId])
|
||||
.bookmarkPrivately(["e", noteId2])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getEventTagValues(event.tags)).toEqual([noteId])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await BookmarkList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.ids().sort()).toEqual([noteId, noteId2].sort())
|
||||
|
||||
const publicOnly = await BookmarkList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.ids()).toEqual([noteId])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through", async () => {
|
||||
const event = await new BookmarkListBuilder().bookmarkPrivately(["e", noteId2]).toEvent(signer)
|
||||
const undecrypted = await BookmarkList.fromEvent(event)
|
||||
|
||||
const tmpl = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("refuses private mutation when undecrypted", async () => {
|
||||
const event = await new BookmarkListBuilder().bookmarkPrivately(["e", noteId2]).toEvent(signer)
|
||||
const undecrypted = await BookmarkList.fromEvent(event)
|
||||
|
||||
await expect(
|
||||
undecrypted.builder().bookmarkPrivately(["e", noteId]).toEvent(signer),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(BookmarkList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, CLASSIFIED, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Classified, ClassifiedBuilder} from "../src/kinds/Classified"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: CLASSIFIED,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Classified", () => {
|
||||
it("reads represented tags and content", async () => {
|
||||
const event = makeEvent({
|
||||
content: "for sale",
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "Bike"],
|
||||
["summary", "A good bike"],
|
||||
["price", "100", "USD"],
|
||||
["status", "active"],
|
||||
["image", "https://example.com/a.jpg"],
|
||||
["image", "https://example.com/b.jpg"],
|
||||
["t", "cycling"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const c = await Classified.fromEvent(event)
|
||||
|
||||
expect(c.identifier()).toBe("abc")
|
||||
expect(c.title()).toBe("Bike")
|
||||
expect(c.summary()).toBe("A good bike")
|
||||
expect(c.price()).toEqual({amount: 100, currency: "USD"})
|
||||
expect(c.status()).toBe("active")
|
||||
expect(c.images()).toEqual(["https://example.com/a.jpg", "https://example.com/b.jpg"])
|
||||
expect(c.topics()).toEqual(["cycling"])
|
||||
expect(c.content()).toBe("for sale")
|
||||
})
|
||||
|
||||
it("defaults the price currency to SAT", async () => {
|
||||
const c = await Classified.fromEvent(makeEvent({tags: [["d", "x"], ["price", "50"]]}))
|
||||
|
||||
expect(c.price()).toEqual({amount: 50, currency: "SAT"})
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate represented tags", async () => {
|
||||
const event = makeEvent({
|
||||
content: "for sale",
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "Bike"],
|
||||
["summary", "A good bike"],
|
||||
["price", "100", "USD"],
|
||||
["status", "active"],
|
||||
["image", "https://example.com/a.jpg"],
|
||||
["image", "https://example.com/b.jpg"],
|
||||
["t", "cycling"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Classified.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
for (const key of ["d", "title", "summary", "price", "status"]) {
|
||||
expect(tmpl.tags.filter(t => t[0] === key).length).toBe(1)
|
||||
}
|
||||
expect(tmpl.tags.filter(t => t[0] === "image").length).toBe(2)
|
||||
expect(tmpl.tags.filter(t => t[0] === "t").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["d", "abc"])
|
||||
expect(tmpl.tags).toContainEqual(["price", "100", "USD"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("for sale")
|
||||
})
|
||||
|
||||
it("builds from a fresh builder with an auto-generated d", async () => {
|
||||
const tmpl = await new ClassifiedBuilder()
|
||||
.setTitle("Fresh")
|
||||
.setContent("desc")
|
||||
.setPrice(25)
|
||||
.setImages(["https://example.com/c.jpg"])
|
||||
.setTopics(["misc"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(CLASSIFIED)
|
||||
expect(tmpl.tags.find(t => t[0] === "d")?.[1]).toBeTruthy()
|
||||
expect(tmpl.tags).toContainEqual(["title", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["price", "25", "SAT"])
|
||||
expect(tmpl.tags).toContainEqual(["image", "https://example.com/c.jpg"])
|
||||
expect(tmpl.tags).toContainEqual(["t", "misc"])
|
||||
expect(tmpl.content).toBe("desc")
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Classified.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, COMMENT, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Comment, CommentBuilder} from "../src/kinds/Comment"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const rootId = "aa".repeat(32)
|
||||
const rootPubkey = "bb".repeat(32)
|
||||
const parentId = "cc".repeat(32)
|
||||
const parentPubkey = "dd".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: COMMENT,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Comment", () => {
|
||||
it("reads root and parent references", async () => {
|
||||
const event = makeEvent({
|
||||
content: "nice thread",
|
||||
tags: [
|
||||
["E", rootId],
|
||||
["K", "11"],
|
||||
["P", rootPubkey],
|
||||
["e", parentId],
|
||||
["k", "1111"],
|
||||
["p", parentPubkey],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const comment = await Comment.fromEvent(event)
|
||||
|
||||
expect(comment.content()).toBe("nice thread")
|
||||
expect(comment.rootId()).toBe(rootId)
|
||||
expect(comment.rootKind()).toBe("11")
|
||||
expect(comment.rootPubkey()).toBe(rootPubkey)
|
||||
expect(comment.parentId()).toBe(parentId)
|
||||
expect(comment.parentKind()).toBe("1111")
|
||||
expect(comment.parentPubkey()).toBe(parentPubkey)
|
||||
expect(comment.root()).toEqual({id: rootId, address: undefined, kind: "11", pubkey: rootPubkey})
|
||||
expect(comment.parent()).toEqual({
|
||||
id: parentId,
|
||||
address: undefined,
|
||||
kind: "1111",
|
||||
pubkey: parentPubkey,
|
||||
})
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate reference tags", async () => {
|
||||
const event = makeEvent({
|
||||
content: "nice thread",
|
||||
tags: [
|
||||
["E", rootId],
|
||||
["K", "11"],
|
||||
["P", rootPubkey],
|
||||
["e", parentId],
|
||||
["k", "1111"],
|
||||
["p", parentPubkey],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Comment.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
// Each represented reference key emits exactly once.
|
||||
for (const key of ["E", "K", "P", "e", "k", "p"]) {
|
||||
expect(tmpl.tags.filter(t => t[0] === key).length).toBe(1)
|
||||
}
|
||||
expect(tmpl.tags).toContainEqual(["E", rootId])
|
||||
expect(tmpl.tags).toContainEqual(["e", parentId])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("nice thread")
|
||||
})
|
||||
|
||||
it("builds references from full events", async () => {
|
||||
const root = makeEvent({id: rootId, pubkey: rootPubkey, kind: 11})
|
||||
const parent = makeEvent({id: parentId, pubkey: parentPubkey, kind: 1111})
|
||||
|
||||
const tmpl = await new CommentBuilder()
|
||||
.setContent("reply")
|
||||
.setRootFromEvent(root)
|
||||
.setParentFromEvent(parent)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(COMMENT)
|
||||
expect(tmpl.tags).toContainEqual(["E", rootId])
|
||||
expect(tmpl.tags).toContainEqual(["K", "11"])
|
||||
expect(tmpl.tags).toContainEqual(["P", rootPubkey])
|
||||
expect(tmpl.tags).toContainEqual(["e", parentId])
|
||||
expect(tmpl.tags).toContainEqual(["k", "1111"])
|
||||
expect(tmpl.tags).toContainEqual(["p", parentPubkey])
|
||||
expect(tmpl.content).toBe("reply")
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Comment.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, EMOJIS, NOTE, getAddressTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {EmojiList, EmojiListBuilder} from "../src/kinds/EmojiList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const setAddress = `30030:${"aa".repeat(32)}:my-emojis`
|
||||
const setAddress2 = `30030:${"bb".repeat(32)}:more-emojis`
|
||||
const emojiTag = ["emoji", "soapbox", "https://example.com/soapbox.png"]
|
||||
const emojiTag2 = ["emoji", "ostrich", "https://example.com/ostrich.png"]
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: EMOJIS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("EmojiList", () => {
|
||||
it("reads emoji-set addresses and inline emoji tags", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [["a", setAddress], emojiTag, ["alt", "x"]],
|
||||
})
|
||||
|
||||
const list = await EmojiList.fromEvent(event)
|
||||
|
||||
expect(list.addresses()).toEqual([setAddress])
|
||||
expect(list.emojis()).toEqual([emojiTag])
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [["a", setAddress], emojiTag, ["alt", "x"]],
|
||||
})
|
||||
|
||||
const list = await EmojiList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(EMOJIS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "emoji").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new EmojiListBuilder()
|
||||
.addEmojiSet(setAddress)
|
||||
.addEmoji("soapbox", "https://example.com/soapbox.png")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getAddressTagValues(tmpl.tags)).toEqual([setAddress])
|
||||
expect(tmpl.tags).toContainEqual(emojiTag)
|
||||
})
|
||||
|
||||
it("removeEmoji removes by value", async () => {
|
||||
const event = makeEvent({tags: [emojiTag, emojiTag2]})
|
||||
const list = await EmojiList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().removeEmoji("soapbox").toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "emoji")).toEqual([emojiTag2])
|
||||
})
|
||||
|
||||
it("round-trips public and private entries through encryption", async () => {
|
||||
const event = await new EmojiListBuilder()
|
||||
.addEmojiSet(setAddress)
|
||||
.addPrivate(["a", setAddress2])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getAddressTagValues(event.tags)).toEqual([setAddress])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await EmojiList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.addresses().sort()).toEqual([setAddress, setAddress2].sort())
|
||||
|
||||
const publicOnly = await EmojiList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.addresses()).toEqual([setAddress])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through", async () => {
|
||||
const event = await new EmojiListBuilder().addPrivate(["a", setAddress2]).toEvent(signer)
|
||||
const undecrypted = await EmojiList.fromEvent(event)
|
||||
|
||||
const tmpl = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(EmojiList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, FEED, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Feed, FeedBuilder} from "../src/kinds/Feed"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const definition = ["union", ["search", "nostr"]]
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: FEED,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Feed", () => {
|
||||
it("reads represented tags", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "My Feed"],
|
||||
["description", "all the things"],
|
||||
["feed", JSON.stringify(definition)],
|
||||
["alt", "My Feed"],
|
||||
],
|
||||
})
|
||||
|
||||
const feed = await Feed.fromEvent(event)
|
||||
|
||||
expect(feed.identifier()).toBe("abc")
|
||||
expect(feed.title()).toBe("My Feed")
|
||||
expect(feed.description()).toBe("all the things")
|
||||
expect(feed.definition()).toEqual(definition)
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate represented tags", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "My Feed"],
|
||||
["description", "all the things"],
|
||||
["feed", JSON.stringify(definition)],
|
||||
["alt", "My Feed"],
|
||||
// "alt" is a represented/derived tag (mirrors title), so use a distinct
|
||||
// unknown key for the passthrough assertion.
|
||||
["zzz", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Feed.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
for (const key of ["d", "title", "description", "feed", "alt"]) {
|
||||
expect(tmpl.tags.filter(t => t[0] === key).length).toBe(1)
|
||||
}
|
||||
expect(tmpl.tags).toContainEqual(["d", "abc"])
|
||||
expect(tmpl.tags).toContainEqual(["title", "My Feed"])
|
||||
expect(tmpl.tags).toContainEqual(["feed", JSON.stringify(definition)])
|
||||
// The derived "alt" tag mirrors the title.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "My Feed"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["zzz", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder with an auto-generated d", async () => {
|
||||
const tmpl = await new FeedBuilder()
|
||||
.setTitle("Fresh")
|
||||
.setDescription("desc")
|
||||
.setDefinition(definition)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(FEED)
|
||||
expect(tmpl.tags.find(t => t[0] === "d")?.[1]).toBeTruthy()
|
||||
expect(tmpl.tags).toContainEqual(["title", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["description", "desc"])
|
||||
expect(tmpl.tags).toContainEqual(["feed", JSON.stringify(definition)])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Feed.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, FEEDS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {FeedList, FeedListBuilder} from "../src/kinds/FeedList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const addressA = `31890:${"22".repeat(32)}:feeda`
|
||||
const addressB = `31890:${"33".repeat(32)}:feedb`
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: FEEDS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("FeedList", () => {
|
||||
it("reads saved feed addresses", async () => {
|
||||
const reader = await FeedList.fromEvent(
|
||||
makeEvent({tags: [["a", addressA], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(reader.addresses()).toEqual([addressA])
|
||||
expect(reader.includes(addressA)).toBe(true)
|
||||
expect(reader.includes(addressB)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await FeedList.fromEvent(
|
||||
makeEvent({tags: [["a", addressA], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("adds and removes feeds via a fresh builder", async () => {
|
||||
const tmpl = await new FeedListBuilder()
|
||||
.addFeed(addressA, "wss://relay.example.com/")
|
||||
.addFeed(addressB)
|
||||
.removeFeed(addressA)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(FEEDS)
|
||||
expect(tmpl.tags).toContainEqual(["a", addressB, ""])
|
||||
expect(tmpl.tags.some(t => t[1] === addressA)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips public and private feeds through encryption", async () => {
|
||||
const event = await new FeedListBuilder()
|
||||
.addFeed(addressA)
|
||||
.addFeedPrivately(addressB)
|
||||
.toEvent(signer)
|
||||
|
||||
const decrypted = await FeedList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.addresses().sort()).toEqual([addressA, addressB].sort())
|
||||
|
||||
const publicOnly = await FeedList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.addresses()).toEqual([addressA])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(FeedList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, FOLLOWS, NOTE, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {FollowList, FollowListBuilder} from "../src/kinds/FollowList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
const c = "cc".repeat(32)
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: FOLLOWS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("FollowList", () => {
|
||||
it("reads followed pubkeys", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["p", a],
|
||||
["p", b],
|
||||
["t", "nostr"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await FollowList.fromEvent(event)
|
||||
|
||||
expect(list.pubkeys().sort()).toEqual([a, b].sort())
|
||||
expect(list.includes(a)).toBe(true)
|
||||
expect(list.includes(c)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["p", a],
|
||||
["p", b],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await FollowList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(FOLLOWS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder via addFollow", async () => {
|
||||
const tmpl = await new FollowListBuilder()
|
||||
.addFollow(["p", a])
|
||||
.addFollow(["t", "nostr"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getPubkeyTagValues(tmpl.tags)).toEqual([a])
|
||||
expect(tmpl.tags).toContainEqual(["t", "nostr"])
|
||||
})
|
||||
|
||||
it("removeFollow removes by value", async () => {
|
||||
const event = makeEvent({tags: [["p", a], ["p", b]]})
|
||||
const list = await FollowList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().removeFollow(a).toTemplate(signer)
|
||||
|
||||
expect(getPubkeyTagValues(tmpl.tags)).toEqual([b])
|
||||
})
|
||||
|
||||
it("round-trips public and private follows through encryption", async () => {
|
||||
const event = await new FollowListBuilder()
|
||||
.addFollow(["p", a])
|
||||
.addPrivate(["p", b])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getPubkeyTagValues(event.tags)).toEqual([a])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await FollowList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.pubkeys().sort()).toEqual([a, b].sort())
|
||||
|
||||
const publicOnly = await FollowList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.pubkeys()).toEqual([a])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(FollowList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, COMMUNITIES, NOTE, getAddressTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {GroupList, GroupListBuilder} from "../src/kinds/GroupList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const g1 = `34550:${"aa".repeat(32)}:dev`
|
||||
const g2 = `34550:${"bb".repeat(32)}:art`
|
||||
const g3 = `34550:${"cc".repeat(32)}:music`
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: COMMUNITIES,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("GroupList", () => {
|
||||
it("reads community addresses", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["a", g1, "wss://relay.example/"],
|
||||
["a", g2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await GroupList.fromEvent(event)
|
||||
|
||||
expect(list.addresses().sort()).toEqual([g1, g2].sort())
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["a", g1, "wss://relay.example/"],
|
||||
["a", g2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await GroupList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(COMMUNITIES)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder with relay hint", async () => {
|
||||
const tmpl = await new GroupListBuilder()
|
||||
.addGroup(g1, "wss://relay.example/")
|
||||
.addGroup(g2)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getAddressTagValues(tmpl.tags).sort()).toEqual([g1, g2].sort())
|
||||
expect(tmpl.tags).toContainEqual(["a", g1, "wss://relay.example/"])
|
||||
expect(tmpl.tags).toContainEqual(["a", g2, ""])
|
||||
})
|
||||
|
||||
it("removeGroup removes by address", async () => {
|
||||
const event = makeEvent({tags: [["a", g1], ["a", g2]]})
|
||||
const list = await GroupList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().removeGroup(g1).toTemplate(signer)
|
||||
|
||||
expect(getAddressTagValues(tmpl.tags)).toEqual([g2])
|
||||
})
|
||||
|
||||
it("round-trips public and private entries through encryption", async () => {
|
||||
const event = await new GroupListBuilder()
|
||||
.addGroup(g1)
|
||||
.addPrivate(["a", g2, ""])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getAddressTagValues(event.tags)).toEqual([g1])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await GroupList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.addresses().sort()).toEqual([g1, g2].sort())
|
||||
|
||||
const publicOnly = await GroupList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.addresses()).toEqual([g1])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through", async () => {
|
||||
const event = await new GroupListBuilder().addPrivate(["a", g2, ""]).toEvent(signer)
|
||||
const undecrypted = await GroupList.fromEvent(event)
|
||||
|
||||
const tmpl = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(GroupList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, HANDLER_INFORMATION, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Handler, HandlerBuilder} from "../src/kinds/Handler"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: HANDLER_INFORMATION,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Handler", () => {
|
||||
it("parses JSON metadata content and k tags", async () => {
|
||||
const event = makeEvent({
|
||||
content: JSON.stringify({
|
||||
name: "Coracle",
|
||||
about: "a client",
|
||||
image: "https://example.com/i.png",
|
||||
website: "https://example.com",
|
||||
lud16: "a@example.com",
|
||||
nip05: "a@example.com",
|
||||
}),
|
||||
tags: [
|
||||
["d", "myhandler"],
|
||||
["k", "1"],
|
||||
["k", "30023"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const handler = await Handler.fromEvent(event)
|
||||
|
||||
expect(handler.values.name).toBe("Coracle")
|
||||
expect(handler.name()).toBe("Coracle")
|
||||
expect(handler.about()).toBe("a client")
|
||||
expect(handler.image()).toBe("https://example.com/i.png")
|
||||
expect(handler.website()).toBe("https://example.com")
|
||||
expect(handler.lud16()).toBe("a@example.com")
|
||||
expect(handler.nip05()).toBe("a@example.com")
|
||||
expect(handler.kinds()).toEqual([1, 30023])
|
||||
})
|
||||
|
||||
it("maps display_name and picture aliases to canonical accessors", async () => {
|
||||
const handler = await Handler.fromEvent(
|
||||
makeEvent({
|
||||
content: JSON.stringify({display_name: "Alias", picture: "https://example.com/p.png"}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(handler.name()).toBe("Alias")
|
||||
expect(handler.image()).toBe("https://example.com/p.png")
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
content: JSON.stringify({name: "Coracle", about: "a client"}),
|
||||
tags: [
|
||||
["d", "myhandler"],
|
||||
["k", "1"],
|
||||
["k", "30023"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Handler.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "k").length).toBe(2)
|
||||
// The d identifier is passed through untouched.
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
|
||||
// Content re-serializes the parsed metadata.
|
||||
const parsed = JSON.parse(tmpl.content)
|
||||
expect(parsed.name).toBe("Coracle")
|
||||
expect(parsed.about).toBe("a client")
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new HandlerBuilder()
|
||||
.setName("MyApp")
|
||||
.setAbout("does things")
|
||||
.setWebsite("https://my.app")
|
||||
.setKinds([1, 7])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(HANDLER_INFORMATION)
|
||||
const parsed = JSON.parse(tmpl.content)
|
||||
expect(parsed.name).toBe("MyApp")
|
||||
expect(parsed.about).toBe("does things")
|
||||
expect(parsed.website).toBe("https://my.app")
|
||||
expect(tmpl.tags).toContainEqual(["k", "1"])
|
||||
expect(tmpl.tags).toContainEqual(["k", "7"])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Handler.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, HANDLER_RECOMMENDATION, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {
|
||||
HandlerRecommendation,
|
||||
HandlerRecommendationBuilder,
|
||||
} from "../src/kinds/HandlerRecommendation"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const webAddress = `31990:${"aa".repeat(32)}:web-handler`
|
||||
const otherAddress = `31990:${"bb".repeat(32)}:other-handler`
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: HANDLER_RECOMMENDATION,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("HandlerRecommendation", () => {
|
||||
it("parses address tags and prefers the web handler", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["d", "1"],
|
||||
["a", otherAddress, "wss://relay.one", "android"],
|
||||
["a", webAddress, "wss://relay.two", "web"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const rec = await HandlerRecommendation.fromEvent(event)
|
||||
|
||||
expect(rec.addresses()).toEqual([otherAddress, webAddress])
|
||||
expect(rec.addressTags().length).toBe(2)
|
||||
// Prefers the recommendation marked "web".
|
||||
expect(rec.handlerAddress()).toBe(webAddress)
|
||||
})
|
||||
|
||||
it("falls back to the first recommendation without a web marker", async () => {
|
||||
const rec = await HandlerRecommendation.fromEvent(
|
||||
makeEvent({tags: [["d", "1"], ["a", otherAddress, "", "android"]]}),
|
||||
)
|
||||
|
||||
expect(rec.handlerAddress()).toBe(otherAddress)
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["d", "1"],
|
||||
["a", otherAddress, "wss://relay.one", "android"],
|
||||
["a", webAddress, "wss://relay.two", "web"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await HandlerRecommendation.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
// The d identifier round-trips its value.
|
||||
expect(tmpl.tags.find(t => t[0] === "d")![1]).toBe("1")
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const builder = new HandlerRecommendationBuilder()
|
||||
// The d identifier holds the recommended kind; it has no setter, so set it
|
||||
// directly on the public field.
|
||||
builder.identifier = "1"
|
||||
|
||||
const tmpl = await builder
|
||||
.addRecommendation(webAddress, "wss://relay.one", "web")
|
||||
// Duplicate addresses are ignored.
|
||||
.addRecommendation(webAddress, "wss://relay.one", "web")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(HANDLER_RECOMMENDATION)
|
||||
expect(tmpl.tags).toContainEqual(["d", "1"])
|
||||
expect(tmpl.tags.filter(t => t[0] === "a")).toEqual([
|
||||
["a", webAddress, "wss://relay.one", "web"],
|
||||
])
|
||||
})
|
||||
|
||||
it("requires a d identifier", async () => {
|
||||
await expect(
|
||||
new HandlerRecommendationBuilder().addRecommendation(webAddress).toTemplate(signer),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(HandlerRecommendation.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, MESSAGING_RELAYS, NOTE, getTagValues, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {MessagingRelayList, MessagingRelayListBuilder} from "../src/kinds/MessagingRelayList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const r1 = "wss://inbox.one.example/"
|
||||
const r2 = "wss://inbox.two.example/"
|
||||
const r3 = "wss://inbox.three.example/"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: MESSAGING_RELAYS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("MessagingRelayList", () => {
|
||||
it("reads messaging relay urls", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["relay", r1],
|
||||
["relay", r2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await MessagingRelayList.fromEvent(event)
|
||||
|
||||
expect(list.urls().sort()).toEqual([r1, r2].sort())
|
||||
})
|
||||
|
||||
it("round-trips without duplicating tags and preserves passthrough", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["relay", r1],
|
||||
["relay", r2],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const list = await MessagingRelayList.fromEvent(event)
|
||||
const tmpl = await list.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(MESSAGING_RELAYS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relay").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder and normalizes urls", async () => {
|
||||
const tmpl = await new MessagingRelayListBuilder()
|
||||
.addRelay("wss://inbox.one.example")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getTagValues("relay", tmpl.tags)).toEqual([normalizeRelayUrl("wss://inbox.one.example")])
|
||||
})
|
||||
|
||||
it("setRelays replaces existing relays", async () => {
|
||||
const event = makeEvent({tags: [["relay", r1]]})
|
||||
const list = await MessagingRelayList.fromEvent(event)
|
||||
|
||||
const tmpl = await list.builder().setRelays([r2, r3]).toTemplate(signer)
|
||||
|
||||
expect(getTagValues("relay", tmpl.tags).sort()).toEqual([r2, r3].sort())
|
||||
})
|
||||
|
||||
it("round-trips public and private entries through encryption", async () => {
|
||||
const event = await new MessagingRelayListBuilder()
|
||||
.addRelay(r1)
|
||||
.addPrivate(["relay", r2])
|
||||
.toEvent(signer)
|
||||
|
||||
expect(getTagValues("relay", event.tags)).toEqual([r1])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
const decrypted = await MessagingRelayList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.urls().sort()).toEqual([r1, r2].sort())
|
||||
|
||||
const publicOnly = await MessagingRelayList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.urls()).toEqual([r1])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through", async () => {
|
||||
const event = await new MessagingRelayListBuilder().addPrivate(["relay", r2]).toEvent(signer)
|
||||
const undecrypted = await MessagingRelayList.fromEvent(event)
|
||||
|
||||
const tmpl = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(MessagingRelayList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, MUTES, FOLLOWS, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {MuteList} from "../src/MuteList"
|
||||
import {MuteList, MuteListBuilder} from "../src/kinds/MuteList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
|
||||
@@ -12,14 +12,7 @@ const c = "cc".repeat(32)
|
||||
|
||||
describe("MuteList", () => {
|
||||
it("round-trips public and private mutes through encryption", async () => {
|
||||
const list = MuteList.init().addPublicly(a).addPrivately(b)
|
||||
|
||||
expect(list.pubkeys.sort()).toEqual([a, b].sort())
|
||||
expect(list.includes(a)).toBe(true)
|
||||
expect(list.includes(b)).toBe(true)
|
||||
expect(list.includes(c)).toBe(false)
|
||||
|
||||
const event = await list.toEvent(signer)
|
||||
const event = await new MuteListBuilder().mutePublicly(a).mutePrivately(b).toEvent(signer)
|
||||
|
||||
expect(event.kind).toBe(MUTES)
|
||||
expect(event.sig).toBeTruthy()
|
||||
@@ -28,46 +21,55 @@ describe("MuteList", () => {
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
// Re-parsing with a capable signer recovers the private entries.
|
||||
const decrypted = await MuteList.parse(event, signer)
|
||||
const decrypted = await MuteList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.isDecrypted).toBe(true)
|
||||
expect(decrypted.pubkeys.sort()).toEqual([a, b].sort())
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.pubkeys().sort()).toEqual([a, b].sort())
|
||||
expect(decrypted.includes(a)).toBe(true)
|
||||
expect(decrypted.includes(b)).toBe(true)
|
||||
expect(decrypted.includes(c)).toBe(false)
|
||||
|
||||
// Parsing without a signer exposes only the public entries.
|
||||
const publicOnly = await MuteList.parse(event)
|
||||
const publicOnly = await MuteList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.isDecrypted).toBe(false)
|
||||
expect(publicOnly.pubkeys).toEqual([a])
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.pubkeys()).toEqual([a])
|
||||
})
|
||||
|
||||
it("removes from both public and private entries", async () => {
|
||||
const list = MuteList.init().addPublicly(a).addPrivately(b)
|
||||
const event = await new MuteListBuilder()
|
||||
.mutePublicly(a)
|
||||
.mutePrivately(b)
|
||||
.unmute(a)
|
||||
.unmute(b)
|
||||
.toEvent(signer)
|
||||
|
||||
list.remove(a)
|
||||
list.remove(b)
|
||||
const parsed = await MuteList.fromEvent(event, signer)
|
||||
|
||||
expect(list.pubkeys).toEqual([])
|
||||
expect(parsed.pubkeys()).toEqual([])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through serialization", async () => {
|
||||
const event = await MuteList.init().addPrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.parse(event)
|
||||
const event = await new MuteListBuilder().mutePrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.fromEvent(event)
|
||||
|
||||
// We never decrypted, so the original ciphertext must survive untouched.
|
||||
const template = await undecrypted.toTemplate(signer)
|
||||
const template = await undecrypted.builder().toTemplate(signer)
|
||||
|
||||
expect(template.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("refuses private mutation when undecrypted", async () => {
|
||||
const event = await MuteList.init().addPrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.parse(event)
|
||||
const event = await new MuteListBuilder().mutePrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.fromEvent(event)
|
||||
|
||||
expect(() => undecrypted.addPrivately(c)).toThrow()
|
||||
// Mutation is now deferred-validated: adding a private entry to a list we
|
||||
// couldn't decrypt throws at emit time, not on the mutating call.
|
||||
await expect(undecrypted.builder().mutePrivately(c).toEvent(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("toRumor encrypts but does not sign", async () => {
|
||||
const rumor = await MuteList.init().addPrivately(b).toRumor(signer)
|
||||
const rumor = await new MuteListBuilder().mutePrivately(b).toRumor(signer)
|
||||
|
||||
expect(rumor.id).toBeTruthy()
|
||||
expect((rumor as TrustedEvent).sig).toBeUndefined()
|
||||
@@ -77,6 +79,6 @@ describe("MuteList", () => {
|
||||
it("throws on the wrong kind", async () => {
|
||||
const event = {kind: FOLLOWS, tags: [], content: "", pubkey: a} as TrustedEvent
|
||||
|
||||
await expect(MuteList.parse(event)).rejects.toThrow()
|
||||
await expect(MuteList.fromEvent(event)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, PINS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {PinList, PinListBuilder} from "../src/kinds/PinList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const eventId = "11".repeat(32)
|
||||
const address = `31890:${"22".repeat(32)}:feed`
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: PINS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("PinList", () => {
|
||||
it("reads pinned event ids and addresses", async () => {
|
||||
const reader = await PinList.fromEvent(
|
||||
makeEvent({tags: [["e", eventId], ["a", address], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(reader.ids()).toEqual([eventId])
|
||||
expect(reader.addresses()).toEqual([address])
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await PinList.fromEvent(
|
||||
makeEvent({tags: [["e", eventId], ["a", address], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new PinListBuilder().pinPublicly(["e", eventId]).toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(PINS)
|
||||
expect(tmpl.tags).toContainEqual(["e", eventId])
|
||||
})
|
||||
|
||||
it("round-trips public and private pins through encryption", async () => {
|
||||
const event = await new PinListBuilder()
|
||||
.pinPublicly(["e", eventId])
|
||||
.pinPrivately(["a", address])
|
||||
.toEvent(signer)
|
||||
|
||||
const decrypted = await PinList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.ids()).toEqual([eventId])
|
||||
expect(decrypted.addresses()).toEqual([address])
|
||||
|
||||
const publicOnly = await PinList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.ids()).toEqual([eventId])
|
||||
expect(publicOnly.addresses()).toEqual([])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(PinList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, POLL, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Poll, PollBuilder} from "../src/kinds/Poll"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: POLL,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Poll", () => {
|
||||
it("parses the represented tags and plain-text title", async () => {
|
||||
const event = makeEvent({
|
||||
content: "Favorite color?",
|
||||
tags: [
|
||||
["option", "1", "Red"],
|
||||
["option", "2", "Blue"],
|
||||
["polltype", "multiplechoice"],
|
||||
["endsAt", "1234"],
|
||||
["relay", "wss://relay.one"],
|
||||
["relay", "wss://relay.two"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const poll = await Poll.fromEvent(event)
|
||||
|
||||
expect(poll.title()).toBe("Favorite color?")
|
||||
expect(poll.options()).toEqual([
|
||||
{id: "1", label: "Red"},
|
||||
{id: "2", label: "Blue"},
|
||||
])
|
||||
expect(poll.pollType()).toBe("multiplechoice")
|
||||
expect(poll.endsAt()).toBe(1234)
|
||||
expect(poll.relays()).toEqual(["wss://relay.one", "wss://relay.two"])
|
||||
})
|
||||
|
||||
it("tallies results from response events", async () => {
|
||||
const poll = await Poll.fromEvent(
|
||||
makeEvent({
|
||||
content: "Pick one",
|
||||
tags: [
|
||||
["option", "1", "Red"],
|
||||
["option", "2", "Blue"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const responses = [
|
||||
{pubkey: "a", created_at: 1, tags: [["response", "1"]]},
|
||||
{pubkey: "b", created_at: 1, tags: [["response", "2"]]},
|
||||
// Latest response per pubkey wins.
|
||||
{pubkey: "a", created_at: 2, tags: [["response", "2"]]},
|
||||
] as TrustedEvent[]
|
||||
|
||||
const result = poll.results(responses)
|
||||
|
||||
expect(result.voters).toBe(2)
|
||||
expect(result.options.find(o => o.id === "1")!.votes).toBe(0)
|
||||
expect(result.options.find(o => o.id === "2")!.votes).toBe(2)
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
content: "Favorite color?",
|
||||
tags: [
|
||||
["option", "1", "Red"],
|
||||
["option", "2", "Blue"],
|
||||
["polltype", "multiplechoice"],
|
||||
["endsAt", "1234"],
|
||||
["relay", "wss://relay.one"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Poll.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe("Favorite color?")
|
||||
expect(tmpl.tags.filter(t => t[0] === "option").length).toBe(2)
|
||||
expect(tmpl.tags.filter(t => t[0] === "polltype").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "endsAt").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relay").length).toBe(1)
|
||||
// Unknown tag survives the round-trip.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new PollBuilder()
|
||||
.setTitle("Q?")
|
||||
.addOption("Red", "1")
|
||||
.addOption("Blue", "2")
|
||||
.setPollType("multiplechoice")
|
||||
.setEndsAt(9999)
|
||||
.setRelays(["wss://relay.one"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(POLL)
|
||||
expect(tmpl.content).toBe("Q?")
|
||||
expect(tmpl.tags).toContainEqual(["option", "1", "Red"])
|
||||
expect(tmpl.tags).toContainEqual(["polltype", "multiplechoice"])
|
||||
expect(tmpl.tags).toContainEqual(["endsAt", "9999"])
|
||||
expect(tmpl.tags).toContainEqual(["relay", "wss://relay.one"])
|
||||
})
|
||||
|
||||
it("requires at least one option", async () => {
|
||||
await expect(new PollBuilder().setTitle("Q?").toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Poll.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, POLL_RESPONSE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {PollResponse, PollResponseBuilder} from "../src/kinds/PollResponse"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const poll = "aa".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: POLL_RESPONSE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("PollResponse", () => {
|
||||
it("parses the poll id and deduped selections", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["e", poll],
|
||||
["response", "1"],
|
||||
["response", "2"],
|
||||
["response", "1"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const response = await PollResponse.fromEvent(event)
|
||||
|
||||
expect(response.pollId()).toBe(poll)
|
||||
expect(response.selections()).toEqual(["1", "2"])
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["e", poll],
|
||||
["response", "1"],
|
||||
["response", "2"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await PollResponse.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "response").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new PollResponseBuilder()
|
||||
.setPollId(poll)
|
||||
.addSelection("1")
|
||||
.addSelection("1")
|
||||
.addSelection("2")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(POLL_RESPONSE)
|
||||
expect(tmpl.tags).toContainEqual(["e", poll])
|
||||
expect(tmpl.tags.filter(t => t[0] === "response")).toEqual([
|
||||
["response", "1"],
|
||||
["response", "2"],
|
||||
])
|
||||
})
|
||||
|
||||
it("requires a poll id", async () => {
|
||||
await expect(new PollResponseBuilder().addSelection("1").toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(PollResponse.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, PROFILE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Profile, displayPubkey} from "../src/Profile"
|
||||
import {Profile, displayPubkey} from "../src/kinds/Profile"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
@@ -26,13 +26,13 @@ describe("Profile", () => {
|
||||
tags: [["alt", "profile"]],
|
||||
})
|
||||
|
||||
const profile = await Profile.parse(event)
|
||||
const profile = await Profile.fromEvent(event)
|
||||
|
||||
expect(profile.values.name).toBe("alice")
|
||||
expect(profile.hasName()).toBe(true)
|
||||
expect(profile.name()).toBe("alice")
|
||||
expect(profile.display()).toBe("alice")
|
||||
|
||||
const signed = await profile.toEvent(signer)
|
||||
const signed = await profile.builder().toEvent(signer)
|
||||
|
||||
expect(signed.kind).toBe(PROFILE)
|
||||
expect(JSON.parse(signed.content).name).toBe("alice")
|
||||
@@ -40,28 +40,31 @@ describe("Profile", () => {
|
||||
expect(signed.tags).toEqual([["alt", "profile"]])
|
||||
})
|
||||
|
||||
it("derives lnurl from a lud16 address", () => {
|
||||
const profile = Profile.init({lud16: "alice@example.com"})
|
||||
it("derives lnurl from a lud16 address", async () => {
|
||||
const profile = await Profile.fromEvent(
|
||||
makeEvent({content: JSON.stringify({lud16: "alice@example.com"})}),
|
||||
)
|
||||
|
||||
expect(profile.values.lnurl).toBeTruthy()
|
||||
expect(profile.lnurl()).toBeTruthy()
|
||||
})
|
||||
|
||||
it("gets and sets values by key", () => {
|
||||
const profile = Profile.init({name: "alice"})
|
||||
it("seeds the builder from the reader and edits values", async () => {
|
||||
const profile = await Profile.fromEvent(makeEvent({content: JSON.stringify({name: "alice"})}))
|
||||
|
||||
profile.set("about", "hello")
|
||||
const event = await profile.builder().about("hello").toEvent(signer)
|
||||
const updated = await Profile.fromEvent(event)
|
||||
|
||||
expect(profile.get("name")).toBe("alice")
|
||||
expect(profile.get("about")).toBe("hello")
|
||||
expect(updated.name()).toBe("alice")
|
||||
expect(updated.about()).toBe("hello")
|
||||
})
|
||||
|
||||
it("display falls back to a shortened npub", async () => {
|
||||
const profile = await Profile.parse(makeEvent({content: "{}"}))
|
||||
const profile = await Profile.fromEvent(makeEvent({content: "{}"}))
|
||||
|
||||
expect(profile.display()).toBe(displayPubkey(pubkey))
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Profile.parse(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
await expect(Profile.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAY_INVITE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelayInvite, RelayInviteBuilder} from "../src/kinds/RelayInvite"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAY_INVITE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayInvite", () => {
|
||||
it("reads the claim tag", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["claim", "secret-code"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const invite = await RelayInvite.fromEvent(event)
|
||||
|
||||
expect(invite.claim()).toBe("secret-code")
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate claim tag", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["claim", "secret-code"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await RelayInvite.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "claim").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["claim", "secret-code"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RelayInviteBuilder().setClaim("fresh-code").toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_INVITE)
|
||||
expect(tmpl.tags).toContainEqual(["claim", "fresh-code"])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayInvite.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAY_JOIN, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelayJoin, RelayJoinBuilder} from "../src/kinds/RelayJoin"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAY_JOIN,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayJoin", () => {
|
||||
it("reads claim tag and reason content", async () => {
|
||||
const join = await RelayJoin.fromEvent(
|
||||
makeEvent({tags: [["claim", "abc123"]], content: "please let me in"}),
|
||||
)
|
||||
|
||||
expect(join.claim()).toBe("abc123")
|
||||
expect(join.reason()).toBe("please let me in")
|
||||
})
|
||||
|
||||
it("returns undefined for missing claim/reason", async () => {
|
||||
const join = await RelayJoin.fromEvent(makeEvent())
|
||||
|
||||
expect(join.claim()).toBeUndefined()
|
||||
expect(join.reason()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate tags and preserves passthrough/content", async () => {
|
||||
const join = await RelayJoin.fromEvent(
|
||||
makeEvent({tags: [["claim", "abc123"], ["alt", "x"]], content: "let me in"}),
|
||||
)
|
||||
|
||||
const tmpl = await join.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_JOIN)
|
||||
expect(tmpl.tags.filter(t => t[0] === "claim").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("let me in")
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RelayJoinBuilder()
|
||||
.setClaim("invite42")
|
||||
.setReason("hello")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["claim", "invite42"])
|
||||
expect(tmpl.content).toBe("hello")
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayJoin.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAY_LEAVE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelayLeave, RelayLeaveBuilder} from "../src/kinds/RelayLeave"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const group = "wss://relay.example.com"
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAY_LEAVE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayLeave", () => {
|
||||
it("round-trips the group behavior tag without duplication", async () => {
|
||||
const leave = await RelayLeave.fromEvent(makeEvent({tags: [["h", group], ["alt", "x"]]}))
|
||||
|
||||
expect(leave.group()).toBe(group)
|
||||
|
||||
const tmpl = await leave.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_LEAVE)
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("sets the group via a fresh builder", async () => {
|
||||
const tmpl = await new RelayLeaveBuilder().group(group).toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayLeave.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAYS, NOTE, RelayMode, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelayList, RelayListBuilder} from "../src/kinds/RelayList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const both = "wss://both.example.com/"
|
||||
const read = "wss://read.example.com/"
|
||||
const write = "wss://write.example.com/"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAYS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayList", () => {
|
||||
it("reads relay urls split by read/write mode", async () => {
|
||||
const reader = await RelayList.fromEvent(
|
||||
makeEvent({
|
||||
tags: [
|
||||
["r", both],
|
||||
["r", read, RelayMode.Read],
|
||||
["r", write, RelayMode.Write],
|
||||
["alt", "x"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(reader.urls().sort()).toEqual(
|
||||
[both, read, write].map(normalizeRelayUrl).sort(),
|
||||
)
|
||||
expect(reader.readUrls().sort()).toEqual(
|
||||
[both, read].map(normalizeRelayUrl).sort(),
|
||||
)
|
||||
expect(reader.writeUrls().sort()).toEqual(
|
||||
[both, write].map(normalizeRelayUrl).sort(),
|
||||
)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await RelayList.fromEvent(
|
||||
makeEvent({
|
||||
tags: [["r", both], ["r", read, RelayMode.Read], ["alt", "x"]],
|
||||
}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "r").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("adds modeless and single-mode relays via a fresh builder", async () => {
|
||||
const tmpl = await new RelayListBuilder()
|
||||
.addRelay(read, RelayMode.Read)
|
||||
.addRelay(write, RelayMode.Write)
|
||||
.addRelay(both, RelayMode.Read)
|
||||
.addRelay(both, RelayMode.Write)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAYS)
|
||||
// both was added for read then write, so it should collapse to modeless.
|
||||
expect(tmpl.tags).toContainEqual(["r", both])
|
||||
expect(tmpl.tags).toContainEqual(["r", read, RelayMode.Read])
|
||||
expect(tmpl.tags).toContainEqual(["r", write, RelayMode.Write])
|
||||
})
|
||||
|
||||
it("downgrades a modeless relay when one mode is removed", async () => {
|
||||
const tmpl = await new RelayListBuilder()
|
||||
.addRelay(both, RelayMode.Read)
|
||||
.addRelay(both, RelayMode.Write)
|
||||
.removeRelay(both, RelayMode.Read)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["r", both, RelayMode.Write])
|
||||
expect(tmpl.tags).not.toContainEqual(["r", both])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAY_MEMBERS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelayMembers, RelayMembersBuilder} from "../src/kinds/RelayMembers"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
const c = "cc".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAY_MEMBERS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayMembers", () => {
|
||||
it("reads members from p tags", async () => {
|
||||
const members = await RelayMembers.fromEvent(makeEvent({tags: [["p", a], ["p", b], ["p", a]]}))
|
||||
|
||||
expect(members.pubkeys().sort()).toEqual([a, b].sort())
|
||||
expect(members.isMember(a)).toBe(true)
|
||||
expect(members.isMember(c)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips with deduped p tags and passthrough", async () => {
|
||||
const members = await RelayMembers.fromEvent(
|
||||
makeEvent({tags: [["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await members.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_MEMBERS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("adds and removes members via the builder", async () => {
|
||||
const tmpl = await new RelayMembersBuilder()
|
||||
.addPubkey(a)
|
||||
.addPubkey(b)
|
||||
.addPubkey(a)
|
||||
.removePubkey(b)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayMembers.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {
|
||||
RelayAddMember,
|
||||
RelayAddMemberBuilder,
|
||||
RelayRemoveMember,
|
||||
RelayRemoveMemberBuilder,
|
||||
} from "../src/kinds/RelayMembershipOp"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: RELAY_ADD_MEMBER,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelayAddMember", () => {
|
||||
it("reads affected pubkeys, deduped", async () => {
|
||||
const op = await RelayAddMember.fromEvent(makeEvent({tags: [["p", a], ["p", b], ["p", a]]}))
|
||||
|
||||
expect(op.pubkeys().sort()).toEqual([a, b].sort())
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate p tags and passthrough", async () => {
|
||||
const op = await RelayAddMember.fromEvent(
|
||||
makeEvent({tags: [["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await op.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_ADD_MEMBER)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds fresh with the right kind", async () => {
|
||||
const tmpl = await new RelayAddMemberBuilder().addPubkey(a).addPubkey(a).toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_ADD_MEMBER)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelayAddMember.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("RelayRemoveMember", () => {
|
||||
it("reads affected pubkeys with the remove kind", async () => {
|
||||
const op = await RelayRemoveMember.fromEvent(
|
||||
makeEvent({kind: RELAY_REMOVE_MEMBER, tags: [["p", a]]}),
|
||||
)
|
||||
|
||||
expect(op.kind).toBe(RELAY_REMOVE_MEMBER)
|
||||
expect(op.pubkeys()).toEqual([a])
|
||||
})
|
||||
|
||||
it("builds fresh with the remove kind", async () => {
|
||||
const tmpl = await new RelayRemoveMemberBuilder().addPubkey(a).toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(RELAY_REMOVE_MEMBER)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
})
|
||||
|
||||
it("throws when the add kind is read as a remove", async () => {
|
||||
await expect(
|
||||
RelayRemoveMember.fromEvent(makeEvent({kind: RELAY_ADD_MEMBER})),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, NAMED_RELAYS, NOTE, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RelaySet, RelaySetBuilder} from "../src/kinds/RelaySet"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const relayA = "wss://set-a.example.com/"
|
||||
const relayB = "wss://set-b.example.com/"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: NAMED_RELAYS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RelaySet", () => {
|
||||
it("reads metadata and relay urls", async () => {
|
||||
const reader = await RelaySet.fromEvent(
|
||||
makeEvent({
|
||||
tags: [
|
||||
["d", "my-set"],
|
||||
["title", "My Set"],
|
||||
["description", "a set of relays"],
|
||||
["image", "https://example.com/img.png"],
|
||||
["relay", relayA],
|
||||
["alt", "x"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(reader.identifier()).toBe("my-set")
|
||||
expect(reader.title()).toBe("My Set")
|
||||
expect(reader.description()).toBe("a set of relays")
|
||||
expect(reader.image()).toBe("https://example.com/img.png")
|
||||
expect(reader.urls()).toEqual([normalizeRelayUrl(relayA)])
|
||||
})
|
||||
|
||||
it("round-trips metadata, d and relay tags exactly once each", async () => {
|
||||
const reader = await RelaySet.fromEvent(
|
||||
makeEvent({
|
||||
tags: [
|
||||
["d", "my-set"],
|
||||
["title", "My Set"],
|
||||
["description", "a set of relays"],
|
||||
["image", "https://example.com/img.png"],
|
||||
["relay", relayA],
|
||||
["alt", "x"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "title").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "description").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "image").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relay").length).toBe(1)
|
||||
// Metadata round-trips with the exact same values.
|
||||
expect(tmpl.tags).toContainEqual(["d", "my-set"])
|
||||
expect(tmpl.tags).toContainEqual(["title", "My Set"])
|
||||
expect(tmpl.tags).toContainEqual(["description", "a set of relays"])
|
||||
expect(tmpl.tags).toContainEqual(["image", "https://example.com/img.png"])
|
||||
expect(tmpl.tags).toContainEqual(["relay", normalizeRelayUrl(relayA)])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("auto-generates a d identifier on a fresh builder", async () => {
|
||||
const builder = new RelaySetBuilder()
|
||||
|
||||
expect(builder.identifier).toBeTruthy()
|
||||
|
||||
const tmpl = await builder.setTitle("Fresh").addRelay(relayA).toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(NAMED_RELAYS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["title", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["relay", normalizeRelayUrl(relayA)])
|
||||
})
|
||||
|
||||
it("setRelays replaces relays but preserves metadata", async () => {
|
||||
const reader = await RelaySet.fromEvent(
|
||||
makeEvent({tags: [["d", "my-set"], ["title", "My Set"], ["relay", relayA]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().setRelays([relayB]).toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["relay", normalizeRelayUrl(relayB)])
|
||||
expect(tmpl.tags.some(t => t[0] === "relay" && t[1] === normalizeRelayUrl(relayA))).toBe(false)
|
||||
expect(tmpl.tags).toContainEqual(["title", "My Set"])
|
||||
expect(tmpl.tags).toContainEqual(["d", "my-set"])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RelaySet.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, REPORT, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Report, ReportBuilder} from "../src/kinds/Report"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const reported = "aa".repeat(32)
|
||||
const eventId = "bb".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: REPORT,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Report", () => {
|
||||
it("reads represented tags and content", async () => {
|
||||
const event = makeEvent({
|
||||
content: "this is spam",
|
||||
tags: [
|
||||
["p", reported],
|
||||
["e", eventId, "spam"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const report = await Report.fromEvent(event)
|
||||
|
||||
expect(report.reportedPubkey()).toBe(reported)
|
||||
expect(report.eventId()).toBe(eventId)
|
||||
expect(report.reason()).toBe("spam")
|
||||
expect(report.content()).toBe("this is spam")
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate represented tags", async () => {
|
||||
const event = makeEvent({
|
||||
content: "this is spam",
|
||||
tags: [
|
||||
["p", reported],
|
||||
["e", eventId, "spam"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Report.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["p", reported])
|
||||
expect(tmpl.tags).toContainEqual(["e", eventId, "spam"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("this is spam")
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new ReportBuilder()
|
||||
.setReportedPubkey(reported)
|
||||
.setEventId(eventId)
|
||||
.setReason("impersonation")
|
||||
.setContent("bad actor")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(REPORT)
|
||||
expect(tmpl.tags).toContainEqual(["p", reported])
|
||||
expect(tmpl.tags).toContainEqual(["e", eventId, "impersonation"])
|
||||
expect(tmpl.content).toBe("bad actor")
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Report.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_ADMINS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomAdmins, RoomAdminsBuilder} from "../src/kinds/RoomAdmins"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_ADMINS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomAdmins", () => {
|
||||
it("reads represented tags", async () => {
|
||||
const room = await RoomAdmins.fromEvent(
|
||||
makeEvent({tags: [["d", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(room.h()).toBe("room1")
|
||||
expect(room.identifier()).toBe("room1")
|
||||
expect(room.pubkeys()).toEqual([a, b])
|
||||
})
|
||||
|
||||
it("round-trips with no duplicated tags", async () => {
|
||||
const room = await RoomAdmins.fromEvent(
|
||||
makeEvent({tags: [["d", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await room.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_ADMINS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["d", "room1"])
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RoomAdminsBuilder()
|
||||
.setH("room2")
|
||||
.addPubkey(a)
|
||||
.addPubkey(a) // dedup
|
||||
.addPubkey(b)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["d", "room2"])
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomAdmins.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_CREATE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomCreate, RoomCreateBuilder} from "../src/kinds/RoomCreate"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const group = "abcd1234"
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_CREATE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomCreate", () => {
|
||||
it("round-trips the group behavior tag without duplication", async () => {
|
||||
const create = await RoomCreate.fromEvent(makeEvent({tags: [["h", group], ["alt", "x"]]}))
|
||||
|
||||
expect(create.group()).toBe(group)
|
||||
|
||||
const tmpl = await create.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_CREATE)
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("sets the group via a fresh builder", async () => {
|
||||
const tmpl = await new RoomCreateBuilder().group(group).toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomCreate.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_CREATE_PERMISSION, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {
|
||||
RoomCreatePermission,
|
||||
RoomCreatePermissionBuilder,
|
||||
} from "../src/kinds/RoomCreatePermission"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
const c = "cc".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_CREATE_PERMISSION,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomCreatePermission", () => {
|
||||
it("reads permitted pubkeys from p tags", async () => {
|
||||
const perm = await RoomCreatePermission.fromEvent(
|
||||
makeEvent({tags: [["p", a], ["p", b], ["p", a]]}),
|
||||
)
|
||||
|
||||
expect(perm.pubkeys().sort()).toEqual([a, b].sort())
|
||||
expect(perm.canCreate(a)).toBe(true)
|
||||
expect(perm.canCreate(c)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate p tags and passthrough", async () => {
|
||||
const perm = await RoomCreatePermission.fromEvent(
|
||||
makeEvent({tags: [["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await perm.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_CREATE_PERMISSION)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("sets pubkeys via a fresh builder, deduped", async () => {
|
||||
const tmpl = await new RoomCreatePermissionBuilder().setPubkeys([a, b, a]).toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomCreatePermission.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_DELETE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomDelete, RoomDeleteBuilder} from "../src/kinds/RoomDelete"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_DELETE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomDelete", () => {
|
||||
it("reads multiple repeatable h tags", async () => {
|
||||
const del = await RoomDelete.fromEvent(
|
||||
makeEvent({tags: [["h", "room1"], ["h", "room2"], ["h", "room3"], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(del.hs()).toEqual(["room1", "room2", "room3"])
|
||||
expect(del.h()).toBe("room1")
|
||||
})
|
||||
|
||||
it("round-trips multiple rooms, emitting one h each with no duplication", async () => {
|
||||
const del = await RoomDelete.fromEvent(
|
||||
makeEvent({tags: [["h", "room1"], ["h", "room2"], ["h", "room3"], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await del.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_DELETE)
|
||||
// Three rooms in, three h tags out — exactly one per room, no duplicates.
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(3)
|
||||
expect(tmpl.tags).toContainEqual(["h", "room1"])
|
||||
expect(tmpl.tags).toContainEqual(["h", "room2"])
|
||||
expect(tmpl.tags).toContainEqual(["h", "room3"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder and edits the room set", async () => {
|
||||
const tmpl = await new RoomDeleteBuilder()
|
||||
.addRoom("room1")
|
||||
.addRoom("room1") // dedup
|
||||
.addRoom("room2")
|
||||
.removeRoom("room1")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["h", "room2"])
|
||||
expect(tmpl.tags).not.toContainEqual(["h", "room1"])
|
||||
})
|
||||
|
||||
it("requires at least one h tag", async () => {
|
||||
await expect(new RoomDeleteBuilder().toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomDelete.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_JOIN, NOTE, getTagValue} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomJoin, RoomJoinBuilder} from "../src/kinds/RoomJoin"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_JOIN,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomJoin", () => {
|
||||
it("reads represented fields", async () => {
|
||||
const join = await RoomJoin.fromEvent(
|
||||
makeEvent({
|
||||
tags: [["h", "room1"], ["claim", "invite-code"], ["alt", "x"]],
|
||||
content: "please let me in",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(join.group()).toBe("room1")
|
||||
expect(join.code()).toBe("invite-code")
|
||||
expect(join.reason()).toBe("please let me in")
|
||||
})
|
||||
|
||||
it("round-trips with no duplicated tags", async () => {
|
||||
const join = await RoomJoin.fromEvent(
|
||||
makeEvent({
|
||||
tags: [["h", "room1"], ["claim", "invite-code"], ["alt", "x"]],
|
||||
content: "please let me in",
|
||||
}),
|
||||
)
|
||||
|
||||
const tmpl = await join.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_JOIN)
|
||||
// h round-trips via the base behavior tag.
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "claim").length).toBe(1)
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room1")
|
||||
expect(tmpl.tags).toContainEqual(["claim", "invite-code"])
|
||||
// Content (free-text reason) is preserved.
|
||||
expect(tmpl.content).toBe("please let me in")
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RoomJoinBuilder()
|
||||
.group("room2")
|
||||
.setCode("xyz")
|
||||
.setReason("hi there")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room2")
|
||||
expect(tmpl.tags).toContainEqual(["claim", "xyz"])
|
||||
expect(tmpl.content).toBe("hi there")
|
||||
})
|
||||
|
||||
it("requires an h/group", async () => {
|
||||
await expect(new RoomJoinBuilder().setCode("xyz").toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomJoin.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_LEAVE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomLeave, RoomLeaveBuilder} from "../src/kinds/RoomLeave"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const group = "abcd1234"
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_LEAVE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomLeave", () => {
|
||||
it("reads the group via group() and h()", async () => {
|
||||
const leave = await RoomLeave.fromEvent(makeEvent({tags: [["h", group]]}))
|
||||
|
||||
expect(leave.group()).toBe(group)
|
||||
expect(leave.h()).toBe(group)
|
||||
})
|
||||
|
||||
it("round-trips the group behavior tag without duplication", async () => {
|
||||
const leave = await RoomLeave.fromEvent(makeEvent({tags: [["h", group], ["alt", "x"]]}))
|
||||
|
||||
const tmpl = await leave.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_LEAVE)
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("sets the group via a fresh builder", async () => {
|
||||
const tmpl = await new RoomLeaveBuilder().group(group).toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["h", group])
|
||||
})
|
||||
|
||||
it("requires an h identifier", async () => {
|
||||
await expect(new RoomLeaveBuilder().toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomLeave.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOMS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomList, RoomListBuilder} from "../src/kinds/RoomList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const relay = "wss://groups.example.com/"
|
||||
const groupA = "groupa"
|
||||
const groupB = "groupb"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOMS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomList", () => {
|
||||
it("reads joined groups", async () => {
|
||||
const reader = await RoomList.fromEvent(
|
||||
makeEvent({tags: [["group", groupA, relay], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(reader.groups()).toEqual([groupA])
|
||||
expect(reader.groupTags()).toEqual([["group", groupA, relay]])
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await RoomList.fromEvent(
|
||||
makeEvent({tags: [["group", groupA, relay], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "group").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("joins and leaves groups via a fresh builder", async () => {
|
||||
const tmpl = await new RoomListBuilder()
|
||||
.join(groupA, relay)
|
||||
.join(groupB, relay)
|
||||
.leave(groupA)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOMS)
|
||||
expect(tmpl.tags).toContainEqual(["group", groupB, relay])
|
||||
expect(tmpl.tags.some(t => t[1] === groupA)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips public and private groups through encryption", async () => {
|
||||
const event = await new RoomListBuilder()
|
||||
.join(groupA, relay)
|
||||
.addPrivate(["group", groupB, relay])
|
||||
.toEvent(signer)
|
||||
|
||||
const decrypted = await RoomList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.groups().sort()).toEqual([groupA, groupB].sort())
|
||||
|
||||
const publicOnly = await RoomList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.groups()).toEqual([groupA])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_MEMBERS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomMembers, RoomMembersBuilder} from "../src/kinds/RoomMembers"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
const c = "cc".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_MEMBERS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomMembers", () => {
|
||||
it("reads represented tags", async () => {
|
||||
const room = await RoomMembers.fromEvent(
|
||||
makeEvent({tags: [["d", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(room.h()).toBe("room1")
|
||||
expect(room.members()).toEqual([a, b])
|
||||
expect(room.isMember(a)).toBe(true)
|
||||
expect(room.isMember(c)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips with no duplicated tags", async () => {
|
||||
const room = await RoomMembers.fromEvent(
|
||||
makeEvent({tags: [["d", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await room.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_MEMBERS)
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["d", "room1"])
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("requires an h identifier on a fresh builder", async () => {
|
||||
await expect(new RoomMembersBuilder().addMember(a).toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("builds from a fresh builder and edits membership", async () => {
|
||||
const builder = new RoomMembersBuilder()
|
||||
builder.h = "room2"
|
||||
|
||||
const tmpl = await builder
|
||||
.addMember(a)
|
||||
.addMember(a) // dedup
|
||||
.addMember(b)
|
||||
.removeMember(b)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["d", "room2"])
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).not.toContainEqual(["p", b])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomMembers.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, NOTE, getTagValue} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {
|
||||
RoomAddMember,
|
||||
RoomAddMemberBuilder,
|
||||
RoomRemoveMember,
|
||||
RoomRemoveMemberBuilder,
|
||||
} from "../src/kinds/RoomMembershipOp"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_ADD_MEMBER,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomAddMember", () => {
|
||||
it("reads pubkeys and group", async () => {
|
||||
const op = await RoomAddMember.fromEvent(
|
||||
makeEvent({tags: [["h", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(op.kind).toBe(ROOM_ADD_MEMBER)
|
||||
expect(op.group()).toBe("room1")
|
||||
expect(op.pubkeys()).toEqual([a, b])
|
||||
})
|
||||
|
||||
it("round-trips with no duplicated tags", async () => {
|
||||
const op = await RoomAddMember.fromEvent(
|
||||
makeEvent({tags: [["h", "room1"], ["p", a], ["p", b], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await op.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_ADD_MEMBER)
|
||||
// h round-trips via the base behavior tag.
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room1")
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RoomAddMemberBuilder()
|
||||
.group("room2")
|
||||
.addPubkey(a)
|
||||
.addPubkey(a) // dedup
|
||||
.addPubkey(b)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_ADD_MEMBER)
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room2")
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
expect(tmpl.tags).toContainEqual(["p", b])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomAddMember.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("RoomRemoveMember", () => {
|
||||
it("uses the remove kind and reads pubkeys", async () => {
|
||||
const op = await RoomRemoveMember.fromEvent(
|
||||
makeEvent({kind: ROOM_REMOVE_MEMBER, tags: [["h", "room1"], ["p", a]]}),
|
||||
)
|
||||
|
||||
expect(op.kind).toBe(ROOM_REMOVE_MEMBER)
|
||||
expect(op.pubkeys()).toEqual([a])
|
||||
})
|
||||
|
||||
it("round-trips through the remove builder", async () => {
|
||||
const op = await RoomRemoveMember.fromEvent(
|
||||
makeEvent({kind: ROOM_REMOVE_MEMBER, tags: [["h", "room1"], ["p", a], ["p", b]]}),
|
||||
)
|
||||
|
||||
const tmpl = await op.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_REMOVE_MEMBER)
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room1")
|
||||
})
|
||||
|
||||
it("builds from a fresh remove builder", async () => {
|
||||
const tmpl = await new RoomRemoveMemberBuilder().group("room2").addPubkey(a).toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_REMOVE_MEMBER)
|
||||
expect(getTagValue("h", tmpl.tags)).toBe("room2")
|
||||
expect(tmpl.tags).toContainEqual(["p", a])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(
|
||||
RoomRemoveMember.fromEvent(makeEvent({kind: ROOM_ADD_MEMBER, tags: [["p", a]]})),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ROOM_META, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {RoomMeta, RoomMetaBuilder} from "../src/kinds/RoomMeta"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ROOM_META,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("RoomMeta", () => {
|
||||
it("reads represented tags", async () => {
|
||||
const room = await RoomMeta.fromEvent(
|
||||
makeEvent({
|
||||
tags: [
|
||||
["d", "room1"],
|
||||
["name", "My Room"],
|
||||
["about", "a place"],
|
||||
["picture", "https://img", "256x256"],
|
||||
["closed"],
|
||||
["private"],
|
||||
["livekit"],
|
||||
["alt", "x"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(room.h()).toBe("room1")
|
||||
expect(room.identifier()).toBe("room1")
|
||||
expect(room.name()).toBe("My Room")
|
||||
expect(room.about()).toBe("a place")
|
||||
expect(room.picture()).toBe("https://img")
|
||||
expect(room.pictureMeta()).toEqual(["256x256"])
|
||||
expect(room.isClosed()).toBe(true)
|
||||
expect(room.isHidden()).toBe(false)
|
||||
expect(room.isPrivate()).toBe(true)
|
||||
expect(room.isRestricted()).toBe(false)
|
||||
expect(room.livekit()).toBe(true)
|
||||
})
|
||||
|
||||
it("round-trips with no duplicated tags", async () => {
|
||||
const room = await RoomMeta.fromEvent(
|
||||
makeEvent({
|
||||
tags: [
|
||||
["d", "room1"],
|
||||
["name", "My Room"],
|
||||
["about", "a place"],
|
||||
["picture", "https://img", "256x256"],
|
||||
["closed"],
|
||||
["private"],
|
||||
["alt", "x"],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const tmpl = await room.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ROOM_META)
|
||||
expect(tmpl.tags.filter(t => t[0] === "d").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "name").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "about").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "picture").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "closed").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "private").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["d", "room1"])
|
||||
expect(tmpl.tags).toContainEqual(["name", "My Room"])
|
||||
expect(tmpl.tags).toContainEqual(["picture", "https://img", "256x256"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new RoomMetaBuilder()
|
||||
.setName("Fresh")
|
||||
.setAbout("desc")
|
||||
.setPicture("https://pic", ["100x100"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags).toContainEqual(["name", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["about", "desc"])
|
||||
expect(tmpl.tags).toContainEqual(["picture", "https://pic", "100x100"])
|
||||
// A d-identifier is auto-generated.
|
||||
expect(tmpl.tags.find(t => t[0] === "d")?.[1]).toBeTruthy()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(RoomMeta.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, SEARCH_RELAYS, NOTE, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {SearchRelayList, SearchRelayListBuilder} from "../src/kinds/SearchRelayList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const relayA = "wss://search-a.example.com/"
|
||||
const relayB = "wss://search-b.example.com/"
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: SEARCH_RELAYS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("SearchRelayList", () => {
|
||||
it("reads search relay urls", async () => {
|
||||
const reader = await SearchRelayList.fromEvent(
|
||||
makeEvent({tags: [["relay", relayA], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(reader.urls()).toEqual([normalizeRelayUrl(relayA)])
|
||||
expect(reader.includes(relayA)).toBe(true)
|
||||
expect(reader.includes(relayB)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await SearchRelayList.fromEvent(
|
||||
makeEvent({tags: [["relay", relayA], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "relay").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("adds and removes relays via a fresh builder", async () => {
|
||||
const tmpl = await new SearchRelayListBuilder()
|
||||
.addRelay(relayA)
|
||||
.addRelay(relayB)
|
||||
.removeRelay(relayA)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(SEARCH_RELAYS)
|
||||
expect(tmpl.tags).toContainEqual(["relay", normalizeRelayUrl(relayB)])
|
||||
expect(tmpl.tags.some(t => t[1] === normalizeRelayUrl(relayA))).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips public and private relays through encryption", async () => {
|
||||
const event = await new SearchRelayListBuilder()
|
||||
.addRelay(relayA)
|
||||
.addPrivate(["relay", relayB])
|
||||
.toEvent(signer)
|
||||
|
||||
const decrypted = await SearchRelayList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.urls().sort()).toEqual(
|
||||
[normalizeRelayUrl(relayA), normalizeRelayUrl(relayB)].sort(),
|
||||
)
|
||||
|
||||
const publicOnly = await SearchRelayList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.urls()).toEqual([normalizeRelayUrl(relayA)])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(SearchRelayList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, THREAD, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Thread, ThreadBuilder} from "../src/kinds/Thread"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: THREAD,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Thread", () => {
|
||||
it("reads title and content", async () => {
|
||||
const event = makeEvent({
|
||||
content: "thread body",
|
||||
tags: [
|
||||
["title", "Hello"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const thread = await Thread.fromEvent(event)
|
||||
|
||||
expect(thread.title()).toBe("Hello")
|
||||
expect(thread.content()).toBe("thread body")
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate title tag", async () => {
|
||||
const event = makeEvent({
|
||||
content: "thread body",
|
||||
tags: [
|
||||
["title", "Hello"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Thread.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "title").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["title", "Hello"])
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("thread body")
|
||||
})
|
||||
|
||||
it("round-trips a group (h) behavior tag without rebuilding", async () => {
|
||||
const event = makeEvent({
|
||||
content: "thread body",
|
||||
tags: [
|
||||
["title", "Hello"],
|
||||
["h", "room"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await Thread.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "h").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["h", "room"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new ThreadBuilder()
|
||||
.setTitle("New thread")
|
||||
.setContent("body")
|
||||
.group("room")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(THREAD)
|
||||
expect(tmpl.tags).toContainEqual(["title", "New thread"])
|
||||
expect(tmpl.tags).toContainEqual(["h", "room"])
|
||||
expect(tmpl.content).toBe("body")
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(Thread.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, EVENT_TIME, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {TimeEvent, TimeEventBuilder} from "../src/kinds/TimeEvent"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
// Both timestamps fall within the same epoch-day so exactly one derived "D" tag
|
||||
// is produced (range yields only `start` when end < start + DAY).
|
||||
const start = 1700000000
|
||||
const end = 1700003600
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: EVENT_TIME,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("TimeEvent", () => {
|
||||
it("reads represented tags and content", async () => {
|
||||
const event = makeEvent({
|
||||
content: "meetup",
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "Party"],
|
||||
["location", "Town Hall"],
|
||||
["start", String(start)],
|
||||
["end", String(end)],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const time = await TimeEvent.fromEvent(event)
|
||||
|
||||
expect(time.identifier()).toBe("abc")
|
||||
expect(time.title()).toBe("Party")
|
||||
expect(time.location()).toBe("Town Hall")
|
||||
expect(time.start()).toBe(start)
|
||||
expect(time.end()).toBe(end)
|
||||
expect(time.content()).toBe("meetup")
|
||||
})
|
||||
|
||||
it("falls back to the legacy name tag for title", async () => {
|
||||
const event = makeEvent({tags: [["name", "Legacy"]]})
|
||||
const time = await TimeEvent.fromEvent(event)
|
||||
|
||||
expect(time.title()).toBe("Legacy")
|
||||
})
|
||||
|
||||
it("round-trips with no duplicate represented tags", async () => {
|
||||
const event = makeEvent({
|
||||
content: "meetup",
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["title", "Party"],
|
||||
["location", "Town Hall"],
|
||||
["start", String(start)],
|
||||
["end", String(end)],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await TimeEvent.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
for (const key of ["d", "title", "location", "start", "end"]) {
|
||||
expect(tmpl.tags.filter(t => t[0] === key).length).toBe(1)
|
||||
}
|
||||
expect(tmpl.tags).toContainEqual(["d", "abc"])
|
||||
expect(tmpl.tags).toContainEqual(["title", "Party"])
|
||||
expect(tmpl.tags).toContainEqual(["start", String(start)])
|
||||
// Derived day index recomputed exactly once for this single-day span.
|
||||
expect(tmpl.tags.filter(t => t[0] === "D").length).toBe(1)
|
||||
// Unknown passthrough tag survives.
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
expect(tmpl.content).toBe("meetup")
|
||||
})
|
||||
|
||||
it("does not duplicate the D index on round-trip", async () => {
|
||||
// Even though the source already carries a "D" tag, it is consumed on read
|
||||
// and recomputed, so it must appear exactly once.
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["d", "abc"],
|
||||
["start", String(start)],
|
||||
["end", String(end)],
|
||||
["D", "999999"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await TimeEvent.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "D").length).toBe(1)
|
||||
expect(tmpl.tags).not.toContainEqual(["D", "999999"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder with an auto-generated d", async () => {
|
||||
const tmpl = await new TimeEventBuilder()
|
||||
.setTitle("Fresh")
|
||||
.setStart(start)
|
||||
.setEnd(end)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(EVENT_TIME)
|
||||
expect(tmpl.tags.find(t => t[0] === "d")?.[1]).toBeTruthy()
|
||||
expect(tmpl.tags).toContainEqual(["title", "Fresh"])
|
||||
expect(tmpl.tags).toContainEqual(["start", String(start)])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(TimeEvent.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, TOPICS, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {TopicList, TopicListBuilder} from "../src/kinds/TopicList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const topicA = "nostr"
|
||||
const topicB = "bitcoin"
|
||||
const address = `30015:${"22".repeat(32)}:interests`
|
||||
|
||||
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: TOPICS,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...o,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("TopicList", () => {
|
||||
it("reads followed topics and interest-set addresses", async () => {
|
||||
const reader = await TopicList.fromEvent(
|
||||
makeEvent({tags: [["t", topicA], ["a", address], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
expect(reader.topics()).toEqual([topicA])
|
||||
expect(reader.addresses()).toEqual([address])
|
||||
expect(reader.includes(topicA)).toBe(true)
|
||||
expect(reader.includes(topicB)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips without duplicating represented tags", async () => {
|
||||
const reader = await TopicList.fromEvent(
|
||||
makeEvent({tags: [["t", topicA], ["a", address], ["alt", "x"]]}),
|
||||
)
|
||||
|
||||
const tmpl = await reader.builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "t").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "a").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("follows and unfollows via a fresh builder", async () => {
|
||||
const tmpl = await new TopicListBuilder()
|
||||
.follow(topicA)
|
||||
.follow(topicB)
|
||||
.unfollow(topicA)
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(TOPICS)
|
||||
expect(tmpl.tags).toContainEqual(["t", topicB])
|
||||
expect(tmpl.tags.some(t => t[1] === topicA)).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips public and private topics through encryption", async () => {
|
||||
const event = await new TopicListBuilder()
|
||||
.followPublicly(topicA)
|
||||
.followPrivately(topicB)
|
||||
.toEvent(signer)
|
||||
|
||||
const decrypted = await TopicList.fromEvent(event, signer)
|
||||
|
||||
expect(decrypted.decrypted).toBe(true)
|
||||
expect(decrypted.topics().sort()).toEqual([topicA, topicB].sort())
|
||||
|
||||
const publicOnly = await TopicList.fromEvent(event)
|
||||
|
||||
expect(publicOnly.decrypted).toBe(false)
|
||||
expect(publicOnly.topics()).toEqual([topicA])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(TopicList.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ZAP_GOAL, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {ZapGoal, ZapGoalBuilder} from "../src/kinds/ZapGoal"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ZAP_GOAL,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("ZapGoal", () => {
|
||||
it("parses the represented tags and plain-text title", async () => {
|
||||
const event = makeEvent({
|
||||
content: "New server fund",
|
||||
tags: [
|
||||
["summary", "help us buy a server"],
|
||||
["amount", "500000"],
|
||||
["relays", "wss://relay.one"],
|
||||
["relays", "wss://relay.two"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const goal = await ZapGoal.fromEvent(event)
|
||||
|
||||
expect(goal.title()).toBe("New server fund")
|
||||
expect(goal.summary()).toBe("help us buy a server")
|
||||
expect(goal.amount()).toBe(500000)
|
||||
expect(goal.relays()).toEqual(["wss://relay.one", "wss://relay.two"])
|
||||
})
|
||||
|
||||
it("defaults amount to 0 when missing or unparseable", async () => {
|
||||
const goal = await ZapGoal.fromEvent(makeEvent({content: "Goal"}))
|
||||
|
||||
expect(goal.amount()).toBe(0)
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
content: "New server fund",
|
||||
tags: [
|
||||
["summary", "help us buy a server"],
|
||||
["amount", "500000"],
|
||||
["relays", "wss://relay.one"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await ZapGoal.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe("New server fund")
|
||||
expect(tmpl.tags.filter(t => t[0] === "summary").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "amount").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relays").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new ZapGoalBuilder()
|
||||
.setTitle("Goal")
|
||||
.setSummary("a summary")
|
||||
.setAmount(1000)
|
||||
.setRelays(["wss://relay.one"])
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ZAP_GOAL)
|
||||
expect(tmpl.content).toBe("Goal")
|
||||
expect(tmpl.tags).toContainEqual(["summary", "a summary"])
|
||||
expect(tmpl.tags).toContainEqual(["amount", "1000"])
|
||||
expect(tmpl.tags).toContainEqual(["relays", "wss://relay.one"])
|
||||
})
|
||||
|
||||
it("requires a title", async () => {
|
||||
await expect(new ZapGoalBuilder().setAmount(1000).toTemplate(signer)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(ZapGoal.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ZAP_RECEIPT, ZAP_REQUEST, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent, Zapper} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {ZapReceipt, ZapReceiptBuilder} from "../src/kinds/ZapReceipt"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const sender = "aa".repeat(32)
|
||||
const recipient = "bb".repeat(32)
|
||||
const eventId = "cc".repeat(32)
|
||||
|
||||
// hrp "100n" => 100 * 1e11 / 1e9 = 10000 millisats.
|
||||
const bolt11 = "lnbc100n1pjexample"
|
||||
|
||||
// The embedded kind-9734 zap request carried inside the JSON "description" tag.
|
||||
const request = {
|
||||
id: "11".repeat(32),
|
||||
pubkey: sender,
|
||||
created_at: 0,
|
||||
kind: ZAP_REQUEST,
|
||||
tags: [
|
||||
["amount", "10000"],
|
||||
["lnurl", "lnurl1xyz"],
|
||||
],
|
||||
content: "thanks!",
|
||||
sig: "00".repeat(64),
|
||||
}
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ZAP_RECEIPT,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("ZapReceipt", () => {
|
||||
it("parses the represented tags and the embedded zap request", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["bolt11", bolt11],
|
||||
["description", JSON.stringify(request)],
|
||||
["p", recipient],
|
||||
["e", eventId],
|
||||
["preimage", "abcd"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const receipt = await ZapReceipt.fromEvent(event)
|
||||
|
||||
expect(receipt.bolt11()).toBe(bolt11)
|
||||
expect(receipt.invoiceAmount()).toBe(10000)
|
||||
expect(receipt.recipient()).toBe(recipient)
|
||||
expect(receipt.eventId()).toBe(eventId)
|
||||
expect(receipt.preimage()).toBe("abcd")
|
||||
|
||||
// Parsed-values accessors derived from the JSON "description" tag.
|
||||
expect(receipt.request()).toEqual(request)
|
||||
expect(receipt.sender()).toBe(sender)
|
||||
expect(receipt.comment()).toBe("thanks!")
|
||||
})
|
||||
|
||||
it("returns undefined invoice amount for a malformed bolt11", async () => {
|
||||
const receipt = await ZapReceipt.fromEvent(makeEvent({tags: [["bolt11", "not-an-invoice"]]}))
|
||||
|
||||
expect(receipt.invoiceAmount()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("verifies a legitimate zap from a zapper", async () => {
|
||||
const event = makeEvent({
|
||||
pubkey: "dd".repeat(32),
|
||||
tags: [
|
||||
["bolt11", bolt11],
|
||||
["description", JSON.stringify(request)],
|
||||
["p", recipient],
|
||||
],
|
||||
})
|
||||
|
||||
const receipt = await ZapReceipt.fromEvent(event)
|
||||
|
||||
const zapper = {
|
||||
pubkey: recipient,
|
||||
lnurl: "lnurl1xyz",
|
||||
nostrPubkey: "dd".repeat(32),
|
||||
} as Zapper
|
||||
|
||||
expect(receipt.verify(zapper)).toBe(true)
|
||||
// A forged receipt (wrong nostrPubkey) is rejected.
|
||||
expect(receipt.verify({...zapper, nostrPubkey: "ab".repeat(32)})).toBe(false)
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
tags: [
|
||||
["bolt11", bolt11],
|
||||
["description", JSON.stringify(request)],
|
||||
["p", recipient],
|
||||
["e", eventId],
|
||||
["preimage", "abcd"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await ZapReceipt.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.tags.filter(t => t[0] === "bolt11").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "description").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "preimage").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new ZapReceiptBuilder()
|
||||
.setBolt11(bolt11)
|
||||
.setDescription(JSON.stringify(request))
|
||||
.setRecipient(recipient)
|
||||
.setEventId(eventId)
|
||||
.setPreimage("abcd")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ZAP_RECEIPT)
|
||||
expect(tmpl.tags).toContainEqual(["bolt11", bolt11])
|
||||
expect(tmpl.tags).toContainEqual(["p", recipient])
|
||||
expect(tmpl.tags).toContainEqual(["e", eventId])
|
||||
expect(tmpl.tags).toContainEqual(["preimage", "abcd"])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(ZapReceipt.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, ZAP_REQUEST, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {ZapRequest, ZapRequestBuilder} from "../src/kinds/ZapRequest"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
const recipient = "aa".repeat(32)
|
||||
const eventId = "bb".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: ZAP_REQUEST,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("ZapRequest", () => {
|
||||
it("parses the represented tags and comment content", async () => {
|
||||
const event = makeEvent({
|
||||
content: "thanks!",
|
||||
tags: [
|
||||
["amount", "21000"],
|
||||
["lnurl", "lnurl1xyz"],
|
||||
["p", recipient],
|
||||
["e", eventId],
|
||||
["relays", "wss://relay.one", "wss://relay.two"],
|
||||
["anon"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const req = await ZapRequest.fromEvent(event)
|
||||
|
||||
expect(req.amount()).toBe(21000)
|
||||
expect(req.lnurl()).toBe("lnurl1xyz")
|
||||
expect(req.recipient()).toBe(recipient)
|
||||
expect(req.eventId()).toBe(eventId)
|
||||
expect(req.relays()).toEqual(["wss://relay.one", "wss://relay.two"])
|
||||
expect(req.isAnonymous()).toBe(true)
|
||||
expect(req.comment()).toBe("thanks!")
|
||||
})
|
||||
|
||||
it("round-trips with no duplication", async () => {
|
||||
const event = makeEvent({
|
||||
content: "thanks!",
|
||||
tags: [
|
||||
["amount", "21000"],
|
||||
["lnurl", "lnurl1xyz"],
|
||||
["p", recipient],
|
||||
["e", eventId],
|
||||
["relays", "wss://relay.one"],
|
||||
["anon"],
|
||||
["alt", "x"],
|
||||
],
|
||||
})
|
||||
|
||||
const tmpl = await (await ZapRequest.fromEvent(event)).builder().toTemplate(signer)
|
||||
|
||||
expect(tmpl.content).toBe("thanks!")
|
||||
expect(tmpl.tags.filter(t => t[0] === "amount").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "lnurl").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "e").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "relays").length).toBe(1)
|
||||
expect(tmpl.tags.filter(t => t[0] === "anon").length).toBe(1)
|
||||
expect(tmpl.tags).toContainEqual(["alt", "x"])
|
||||
})
|
||||
|
||||
it("builds from a fresh builder", async () => {
|
||||
const tmpl = await new ZapRequestBuilder()
|
||||
.setAmount(1000)
|
||||
.setLnurl("lnurl1abc")
|
||||
.setRecipient(recipient)
|
||||
.setEventId(eventId)
|
||||
.setRelays(["wss://relay.one"])
|
||||
.setAnonymous()
|
||||
.setComment("hi")
|
||||
.toTemplate(signer)
|
||||
|
||||
expect(tmpl.kind).toBe(ZAP_REQUEST)
|
||||
expect(tmpl.content).toBe("hi")
|
||||
expect(tmpl.tags).toContainEqual(["amount", "1000"])
|
||||
expect(tmpl.tags).toContainEqual(["lnurl", "lnurl1abc"])
|
||||
expect(tmpl.tags).toContainEqual(["p", recipient])
|
||||
expect(tmpl.tags).toContainEqual(["e", eventId])
|
||||
expect(tmpl.tags).toContainEqual(["relays", "wss://relay.one"])
|
||||
expect(tmpl.tags).toContainEqual(["anon"])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
await expect(ZapRequest.fromEvent(makeEvent({kind: NOTE}))).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user