Remove some poorly written tests

This commit is contained in:
Jon Staab
2025-03-31 14:32:46 -07:00
parent 8bc336ae5d
commit d0eca0d1b8
24 changed files with 59 additions and 2454 deletions
-1
View File
@@ -1,6 +1,5 @@
--ignore-dir=docs --ignore-dir=docs
--ignore-dir=dist --ignore-dir=dist
--ignore-dir=build --ignore-dir=build
# --ignore-dir=__tests__
--ignore-dir=.svelte-kit --ignore-dir=.svelte-kit
--ignore-file=match:yarn.lock --ignore-file=match:yarn.lock
-272
View File
@@ -1,272 +0,0 @@
import {ctx} from "@welshman/lib"
import {FOLLOWS, MUTES, PINS} from "@welshman/util"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {follow, mute, pin, unfollow, unmute, unpin} from "../src/commands"
import * as thunkModule from "../src/thunk"
import {thunkWorker} from "../src/thunk"
import {repository} from "../src/core"
vi.mock(import("@welshman/lib"), async importOriginal => ({
...(await importOriginal()),
ctx: {
app: {
router: {
FromUser: vi.fn().mockReturnValue({
getUrls: vi.fn().mockReturnValue(["relay1", "relay2"]),
}),
},
},
net: {
getExecutor: vi.fn(),
optimizeSubscriptions: vi.fn().mockReturnValue([]),
},
},
}))
vi.mock(import("../src/session"), async importOriginal => ({
...(await importOriginal()),
nip44EncryptToSelf: vi.fn().mockImplementation(text => `encrypted:${text}`),
pubkey: {
get: () => "ee".repeat(32),
subscribe: run => {
run("ee".repeat(32))
return () => null
},
},
}))
describe("commands", () => {
const pubkey1 = "aa".repeat(32)
const pubkey2 = "bb".repeat(32)
const event1 = "ee".repeat(32)
const event2 = "ff".repeat(32)
beforeEach(async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date())
vi.resetModules()
vi.clearAllMocks()
repository.load([])
})
afterEach(() => {
vi.clearAllTimers()
vi.useRealTimers()
thunkWorker.clear()
thunkWorker.pause()
thunkWorker.resume()
})
describe("follow commands", () => {
it("should create new follows list if none exists", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await follow(["p", pubkey1])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: FOLLOWS,
tags: expect.arrayContaining([["p", pubkey1]]),
}),
relays: ["relay1", "relay2"],
}),
)
})
it("should use existing follows list if available", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await follow(["p", pubkey1])
await vi.runAllTimersAsync()
await follow(["p", pubkey2])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: FOLLOWS,
tags: expect.arrayContaining([
["p", pubkey1],
["p", pubkey2],
]),
}),
relays: ["relay1", "relay2"],
}),
)
})
it("should handle unfollow command", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await follow(["p", pubkey1])
await vi.runAllTimersAsync()
await unfollow(pubkey1)
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: FOLLOWS,
tags: expect.arrayContaining([]),
}),
relays: ["relay1", "relay2"],
}),
)
})
})
describe("mute commands", () => {
it("should create new mutes list if none exists", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await mute(["p", pubkey1])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith({
event: expect.objectContaining({
kind: MUTES,
tags: expect.arrayContaining([["p", pubkey1]]),
}),
relays: ["relay1", "relay2"],
})
})
it.skip("should use existing mutes list if available", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await mute(["p", pubkey1])
await vi.runAllTimersAsync()
await mute(["p", pubkey2])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: MUTES,
tags: expect.arrayContaining([
["p", pubkey1],
["p", pubkey2],
]),
}),
relays: ["relay1", "relay2"],
}),
)
})
it("should handle unmute command", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await mute(["p", pubkey1])
await vi.runAllTimersAsync()
await unmute("pubkey1")
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: MUTES,
tags: expect.arrayContaining([]),
}),
relays: ["relay1", "relay2"],
}),
)
})
})
describe("pin commands", () => {
it("should create new pins list if none exists", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await pin(["e", event1])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: PINS,
tags: expect.arrayContaining([["e", event1]]),
}),
relays: ["relay1", "relay2"],
}),
)
})
it.skip("should use existing pins list if available", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await pin(["e", event1])
await vi.runAllTimersAsync()
await pin(["e", event2])
await vi.runAllTimersAsync()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: PINS,
tags: expect.arrayContaining([
["e", event1],
["e", event2],
]),
}),
relays: ["relay1", "relay2"],
}),
)
})
it("should handle unpin command", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
await pin(["e", event1])
await vi.runAllTimersAsync()
await unpin("event1")
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
kind: PINS,
tags: expect.arrayContaining([]),
}),
relays: ["relay1", "relay2"],
}),
)
})
})
describe("relay selection", () => {
it("should use correct relays from router", async () => {
const publishThunkSpy = vi.spyOn(thunkModule, "publishThunk")
const mockGetUrls = vi.fn().mockReturnValue(["relay3", "relay4"])
vi.mocked(ctx.app.router.FromUser).mockReturnValue({
getUrls: mockGetUrls,
})
await follow(["p", pubkey1])
expect(ctx.app.router.FromUser).toHaveBeenCalled()
expect(mockGetUrls).toHaveBeenCalled()
expect(publishThunkSpy).toHaveBeenCalledWith(
expect.objectContaining({
relays: ["relay3", "relay4"],
}),
)
})
})
})
-167
View File
@@ -1,167 +0,0 @@
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import {ConnectionEvent} from "@welshman/net"
import type {Connection} from "@welshman/net"
import {now} from "@welshman/lib"
import {Relay, relays} from "../src/relays"
import {trackRelayStats} from "../src/relays"
import {get} from "svelte/store"
describe("Relay Stats", () => {
const mockUrl = "wss://test.relay"
let mockConnection: Connection
beforeEach(() => {
vi.useFakeTimers()
// Reset relays store
relays.set([])
// Create mock connection
mockConnection = {
url: mockUrl,
state: {
pendingPublishes: new Map(),
pendingRequests: new Map(),
},
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
} as any
})
afterEach(() => {
vi.useRealTimers()
vi.clearAllMocks()
})
describe("trackRelayStats", () => {
it("should subscribe to all connection events", () => {
trackRelayStats(mockConnection)
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Open, expect.any(Function))
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Close, expect.any(Function))
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Send, expect.any(Function))
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Receive, expect.any(Function))
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Error, expect.any(Function))
})
it("should unsubscribe from all events when cleanup is called", () => {
const cleanup = trackRelayStats(mockConnection)
cleanup()
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Open, expect.any(Function))
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Close, expect.any(Function))
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Send, expect.any(Function))
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Receive, expect.any(Function))
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Error, expect.any(Function))
})
})
describe("Connection Event Handlers", () => {
let eventHandlers: Record<string, Function> = {}
beforeEach(() => {
eventHandlers = {}
mockConnection.on.mockImplementation((event, handler) => {
eventHandlers[event] = handler
})
trackRelayStats(mockConnection)
// Add initial relay to the store
relays.set([{url: mockUrl}])
// Allow batched updates to process
vi.runAllTimers()
})
it("should update stats on connection open", () => {
eventHandlers[ConnectionEvent.Open](mockConnection)
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.open_count).toBe(1)
expect(updatedRelays[0].stats?.last_open).toBeGreaterThan(0)
})
it("should update stats on connection close", () => {
eventHandlers[ConnectionEvent.Close](mockConnection)
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.close_count).toBe(1)
expect(updatedRelays[0].stats?.last_close).toBeGreaterThan(0)
})
it("should update stats on REQ send", () => {
eventHandlers[ConnectionEvent.Send](mockConnection, ["REQ", "test"])
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.request_count).toBe(1)
expect(updatedRelays[0].stats?.last_request).toBeGreaterThanOrEqual(now() - 1)
})
it("should update stats on EVENT send", () => {
eventHandlers[ConnectionEvent.Send](mockConnection, ["EVENT", {}])
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.publish_count).toBe(1)
expect(updatedRelays[0].stats?.last_publish).toBeGreaterThanOrEqual(now() - 1)
})
it("should update stats on OK receive with success", () => {
const eventId = "test-event"
mockConnection.state.pendingPublishes.set(eventId, {sent: now() - 1000})
eventHandlers[ConnectionEvent.Receive](mockConnection, ["OK", eventId, true])
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.publish_success_count).toBe(1)
expect(updatedRelays[0].stats?.publish_timer).toBe(1000)
})
it("should update stats on OK receive with failure", () => {
const eventId = "test-event"
mockConnection.state.pendingPublishes.set(eventId, {sent: Date.now() - 1000})
eventHandlers[ConnectionEvent.Receive](mockConnection, ["OK", eventId, false])
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.publish_failure_count).toBe(1)
})
it("should update stats on EOSE receive", () => {
const subId = "test-sub"
mockConnection.state.pendingRequests.set(subId, {sent: now() - 1000})
eventHandlers[ConnectionEvent.Receive](mockConnection, ["EOSE", subId])
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.eose_count).toBe(1)
expect(updatedRelays[0].stats?.eose_timer).toBe(1000)
})
it("should update stats on error", () => {
eventHandlers[ConnectionEvent.Error](mockConnection)
vi.runAllTimers()
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.last_error).toBeGreaterThan(0)
expect(updatedRelays[0].stats?.recent_errors).toHaveLength(1)
})
it("should limit recent errors to 10", () => {
// Trigger 12 errors
for (let i = 0; i < 12; i++) {
eventHandlers[ConnectionEvent.Error](mockConnection)
vi.advanceTimersByTime(1000)
}
const updatedRelays = get(relays) as Relay[]
expect(updatedRelays[0].stats?.recent_errors).toHaveLength(10)
})
})
})
@@ -1,121 +0,0 @@
import * as util from "@welshman/util"
import {afterEach, beforeEach, describe, expect, test, vi} from "vitest"
import * as relaySelectionModule from "../src/relaySelections"
// Mock dependencies
vi.mock("@welshman/util", async imports => {
return {
...(await imports()),
normalizeRelayUrl: vi.fn(url => url),
asDecryptedEvent: vi.fn(event => event),
readList: vi.fn(),
getListTags: vi.fn(() => []),
getRelayTags: vi.fn(() => []),
getRelayTagValues: vi.fn(() => []),
}
})
vi.mock("@welshman/store", async imports => {
return {
...(await imports()),
deriveEventsMapped: vi.fn(() => ({subscribe: () => ({unsubscribe: () => {}})})),
}
})
vi.mock("../src/subscribe.js", () => ({
load: vi.fn().mockResolvedValue([]),
}))
vi.mock("../src/collection.js", () => ({
collection: vi.fn(() => ({
indexStore: {},
deriveItem: vi.fn(),
loadItem: vi.fn(),
})),
}))
describe("relaySelections", () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("getRelayUrls", () => {
test("returns unique normalized relay URLs", () => {
// Setup
const mockList = {
tags: [
["r", "wss://relay1.com"],
["r", "wss://relay2.com"],
],
}
vi.mocked(util.getListTags).mockReturnValue(mockList.tags)
vi.mocked(util.getRelayTagValues).mockReturnValue([
"wss://relay1.com",
"wss://relay2.com",
"wss://relay1.com",
])
// Execute
const result = relaySelectionModule.getRelayUrls(mockList)
// Verify
expect(util.getListTags).toHaveBeenCalledWith(mockList)
expect(util.getRelayTagValues).toHaveBeenCalledWith(mockList.tags)
expect(util.normalizeRelayUrl).toHaveBeenCalledTimes(3)
expect(result).toEqual(["wss://relay1.com", "wss://relay2.com"])
})
test("returns empty array when list is undefined", () => {
vi.mocked(util.getListTags).mockReturnValue([])
vi.mocked(util.getRelayTagValues).mockReturnValue([])
const result = relaySelectionModule.getRelayUrls(undefined)
expect(result).toEqual([])
})
})
describe("getReadRelayUrls", () => {
test("returns read relay URLs", () => {
// Setup
const mockTags = [
["r", "wss://relay1.com", "read"],
["r", "wss://relay2.com", "write"],
["r", "wss://relay3.com"], // no marker is also read
["r", "wss://relay4.com", "read"],
]
vi.mocked(util.getListTags).mockReturnValue(mockTags)
vi.mocked(util.getRelayTags).mockReturnValue(mockTags)
// Execute
const result = relaySelectionModule.getReadRelayUrls({tags: mockTags})
// Verify
expect(result).toEqual(["wss://relay1.com", "wss://relay3.com", "wss://relay4.com"])
})
})
describe("getWriteRelayUrls", () => {
test("returns write relay URLs", () => {
// Setup
const mockTags = [
["r", "wss://relay1.com", "read"],
["r", "wss://relay2.com", "write"],
["r", "wss://relay3.com"], // no marker is also write
["r", "wss://relay4.com", "write"],
]
vi.mocked(util.getListTags).mockReturnValue(mockTags)
vi.mocked(util.getRelayTags).mockReturnValue(mockTags)
// Execute
const result = relaySelectionModule.getWriteRelayUrls({tags: mockTags})
// Verify
expect(result).toEqual(["wss://relay2.com", "wss://relay3.com", "wss://relay4.com"])
})
})
})
-296
View File
@@ -1,296 +0,0 @@
import {ctx, now} from "@welshman/lib"
import {COMMENT, PROFILE, RELAYS, TrustedEvent} from "@welshman/util"
import {beforeEach, describe, expect, it, vi} from "vitest"
import {relaysByUrl} from "../src/relays"
import {relaySelectionsByPubkey} from "../src/relaySelections"
import {
RelayMode,
Router,
addMaximalFallbacks,
addMinimalFallbacks,
addNoFallbacks,
getFilterSelections,
getPubkeyRelays,
getRelayQuality,
makeRouter,
} from "../src/router"
// Mock dependencies
vi.mock(import("@welshman/lib"), async imports => ({
...(await imports()),
ctx: {
net: {
pool: {
has: vi.fn(),
},
},
app: {
indexerRelays: ["wss://indexer1.com", "wss://indexer2.com"],
},
},
}))
vi.mock(import("../src/relays"), async imports => ({
...(await imports()),
relaysByUrl: {
get: vi.fn(),
},
}))
vi.mock(import("../src/relaySelections"), async imports => ({
...(await imports()),
relaySelectionsByPubkey: {
get: vi.fn().mockReturnValue(new Map()),
},
inboxRelaySelectionsByPubkey: {
get: vi.fn().mockReturnValue(new Map()),
},
}))
describe("Router", () => {
const id = "00".repeat(32)
const pubkey = "aa".repeat(32)
const pubkey1 = "bb".repeat(32)
const pubkey2 = "cc".repeat(32)
let router: Router
const mockEvent: TrustedEvent = {
id,
pubkey,
created_at: now(),
kind: COMMENT,
tags: [
["E", "11".repeat(32), "wss://relay.com", pubkey1],
["P", pubkey2, "wss://relay2.com"],
],
content: "test content",
sig: "test-sig",
}
beforeEach(() => {
vi.clearAllMocks()
router = makeRouter({
getUserPubkey: () => pubkey,
getPubkeyRelays: (user: string, mode?: RelayMode) => [`wss://${mode}.${user.slice(-4)}.com`],
getFallbackRelays: () => ["wss://fallback1.com", "wss://fallback2.com"],
getRelayQuality: () => 1,
getLimit: () => 2,
})
ctx.app.router = router
})
describe("Basic Router Functions", () => {
it("should create router with default options", () => {
const router = makeRouter()
expect(router).toBeInstanceOf(Router)
})
it("should respect limit option", () => {
const urls = router.FromRelays(["wss://1.com", "wss://2.com", "wss://3.com"]).getUrls()
expect(urls).toHaveLength(2)
})
it("should filter invalid relay URLs", () => {
const urls = router.FromRelays(["invalid", "wss://valid.com"]).getUrls()
expect(urls).toHaveLength(2)
// invalid should be filtered out
expect(urls.includes("invalid")).toBe(false)
// one of the relay should be a fallback
expect(urls.some(url => url.startsWith("wss://fallback"))).toBe(true)
expect(urls[0]).toBe("wss://valid.com/")
})
})
describe("Fallback Policies", () => {
it("should implement no fallbacks policy", () => {
expect(addNoFallbacks(1, 3)).toBe(0)
expect(addNoFallbacks(0, 3)).toBe(0)
})
it("should implement minimal fallbacks policy", () => {
expect(addMinimalFallbacks(1, 3)).toBe(0)
expect(addMinimalFallbacks(0, 3)).toBe(1)
})
it("should implement maximal fallbacks policy", () => {
expect(addMaximalFallbacks(1, 3)).toBe(2)
expect(addMaximalFallbacks(0, 3)).toBe(3)
})
})
describe("RouterScenario", () => {
it("should apply weight to selections", () => {
const scenario = router.FromRelays(["wss://1.com", "wss://2.com"]).weight(0.5)
expect(scenario.selections[0].weight).toBe(0.5)
})
it("should merge scenarios", () => {
const scenario1 = router.FromRelays(["wss://1.com"])
const scenario2 = router.FromRelays(["wss://2.com"])
const merged = router.merge([scenario1, scenario2])
expect(merged.selections).toHaveLength(2)
})
it("should respect security options", () => {
const urls = router
.FromRelays(["ws://insecure.com", "wss://secure.com"])
.allowInsecure(false)
.getUrls()
expect(urls).toContain("wss://secure.com/")
expect(urls).not.toContain("ws://insecure.com/")
})
})
describe("Routing Scenarios", () => {
describe("ForUser/FromUser", () => {
it("should handle user routing", () => {
const readUrls = router.ForUser().getUrls()
const writeUrls = router.FromUser().getUrls()
expect(readUrls).toContain(`wss://read.${pubkey.slice(-4)}.com/`)
expect(writeUrls).toContain(`wss://write.${pubkey.slice(-4)}.com/`)
})
})
describe("Event Routing", () => {
it("should route for event author", () => {
const urls = router.Event(mockEvent).getUrls()
expect(urls[0]).toBe(`wss://write.${mockEvent.pubkey.slice(-4)}.com/`)
expect(urls.length).toBeGreaterThan(0)
})
it("should handle event replies", () => {
const urls = router.Replies(mockEvent).getUrls()
expect(urls[0]).toBe(`wss://read.${mockEvent.pubkey.slice(-4)}.com/`)
expect(urls.length).toBeGreaterThan(0)
})
it("should handle event ancestors", () => {
const urls = router.EventRoots(mockEvent).getUrls()
// should have the relay of the mention and the relay of the parent
expect(urls.length).toBe(2)
// @check, super random results
// expect(urls).contains("wss://relay.com/")
// expect(urls).contains("wss://relay2.com/")
})
})
describe("Pubkey Routing", () => {
it("should route for single pubkey", () => {
const urls = router.ForPubkey("test-pubkey").getUrls()
expect(urls.length).toBeGreaterThan(0)
})
it("should route for multiple pubkeys", () => {
const urls = router.ForPubkeys(["pubkey1", "pubkey2"]).getUrls()
expect(urls.length).toBeGreaterThan(0)
})
})
})
describe("Relay Quality", () => {
beforeEach(() => {
vi.mocked(relaysByUrl.get).mockReturnValue(
new Map([
[
"wss://relay.com",
{
url: "wss://relay.com",
stats: {
recent_errors: [],
},
},
],
[
"wss://error.com",
{
url: "wss://error.com",
stats: {
recent_errors: [Date.now()],
},
},
],
]),
)
})
it("should score connected relays highly", () => {
vi.mocked(ctx.net.pool.has).mockReturnValue(true)
expect(getRelayQuality("wss://relay.com")).toBe(1)
})
it("should penalize relays with recent errors", () => {
expect(getRelayQuality("wss://error.com")).toBe(0)
})
it("should handle relays without stats", () => {
vi.mocked(ctx.net.pool.has).mockReturnValue(false)
expect(getRelayQuality("wss://new.com")).toBe(0.8)
})
})
describe("Relay Selection", () => {
beforeEach(() => {
vi.mocked(relaySelectionsByPubkey.get).mockReturnValue(
new Map([
[
"pubkey1",
{
event: {pubkey: "pubkey1"},
publicTags: [
["r", "wss://read.com", "read"],
["r", "wss://write.com", "write"],
],
},
],
]),
)
})
it("should get read relays for pubkey", () => {
const relays = getPubkeyRelays("pubkey1", RelayMode.Read)
expect(relays).toContain("wss://read.com/")
})
it("should get write relays for pubkey", () => {
const relays = getPubkeyRelays("pubkey1", RelayMode.Write)
expect(relays).toContain("wss://write.com/")
})
it("should handle missing relay selections", () => {
const relays = getPubkeyRelays("unknown-pubkey")
expect(relays).toEqual([])
})
})
describe("Filter Selections", () => {
it("should handle search filters", () => {
const selections = getFilterSelections([
{
search: "test",
},
])
expect(selections.length).toBeGreaterThan(0)
})
it("should handle author filters", () => {
const selections = getFilterSelections([
{
authors: ["pubkey1", "pubkey2"],
},
])
expect(selections.length).toBeGreaterThan(0)
})
it("should handle indexed kinds", () => {
const selections = getFilterSelections([
{
kinds: [PROFILE, RELAYS],
},
])
expect(selections.length).toBeGreaterThan(0)
})
})
})
-118
View File
@@ -1,118 +0,0 @@
import {describe, it, expect, vi} from "vitest"
import {createSearch} from "../src/search"
import type {SearchOptions} from "../src/search"
describe("createSearch", () => {
// Test data
type TestItem = {
id: string
name: string
description: string
}
const testItems: TestItem[] = [
{id: "1", name: "Apple", description: "A fruit"},
{id: "2", name: "Banana", description: "Yellow fruit"},
{id: "3", name: "Orange", description: "Citrus fruit"},
]
const baseOptions: SearchOptions<string, TestItem> = {
getValue: item => item.id,
fuseOptions: {
keys: ["name", "description"],
},
}
it("should create a search object with required methods", () => {
const search = createSearch(testItems, baseOptions)
expect(search).toHaveProperty("options")
expect(search).toHaveProperty("getValue")
expect(search).toHaveProperty("getOption")
expect(search).toHaveProperty("searchOptions")
expect(search).toHaveProperty("searchValues")
})
it("should return all items when search term is empty", () => {
const search = createSearch(testItems, baseOptions)
const results = search.searchOptions("")
expect(results).toHaveLength(testItems.length)
expect(results).toEqual(testItems)
})
it("should find items by name", () => {
const search = createSearch(testItems, baseOptions)
const results = search.searchOptions("Apple")
expect(results).toHaveLength(1)
expect(results[0].name).toBe("Apple")
})
it("should find items by description", () => {
const search = createSearch(testItems, baseOptions)
const results = search.searchOptions("Citrus")
expect(results).toHaveLength(1)
expect(results[0].name).toBe("Orange")
})
it("should return values using getValue function", () => {
const search = createSearch(testItems, baseOptions)
const results = search.searchValues("Apple")
expect(results).toHaveLength(1)
expect(results[0]).toBe("1") // The id of Apple
})
it("should get option by value", () => {
const search = createSearch(testItems, baseOptions)
const item = search.getOption("1")
expect(item).toBeDefined()
expect(item?.name).toBe("Apple")
})
it("should call onSearch callback when provided", () => {
const onSearch = vi.fn()
const search = createSearch(testItems, {...baseOptions, onSearch})
search.searchOptions("test")
expect(onSearch).toHaveBeenCalledWith("test")
})
it("should apply custom sort function when provided", () => {
const sortFn = vi.fn()
const items = [
{id: "1", name: "test item", description: "exact match"},
{id: "2", name: "testing", description: "partial match"},
{id: "3", name: "other", description: "test somewhere"},
]
const search = createSearch(items, {...baseOptions, sortFn})
const results = search.searchOptions("test")
// Results should be sorted by score
expect(sortFn).toHaveBeenCalled()
})
it("should handle fuzzy matching", () => {
const search = createSearch(testItems, baseOptions)
const results = search.searchOptions("Aple") // Misspelled "Apple"
expect(results.length).toBe(2)
expect(results[0].name).toBe("Apple")
})
it("should respect fuseOptions threshold", () => {
const search = createSearch(testItems, {
...baseOptions,
fuseOptions: {
...baseOptions.fuseOptions,
threshold: 0.1, // Very strict matching
},
})
const results = search.searchOptions("Aple")
expect(results).toHaveLength(0) // Should not match with strict threshold
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest" import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import {writable, get} from "svelte/store" import {writable, get} from "svelte/store"
import {Repository} from "@welshman/util" import {Repository} from "@welshman/relay"
import {Tracker} from "@welshman/net" import {Tracker} from "@welshman/net"
import { import {
initStorage, initStorage,
-384
View File
@@ -1,384 +0,0 @@
import {ctx} from "@welshman/lib"
import {subscribe as baseSubscribe, SubscriptionEvent} from "@welshman/net"
import {getFilterResultCardinality, LOCAL_RELAY_URL} from "@welshman/util"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {repository} from "../src/core.js"
import {load, subscribe} from "../src/subscribe.ts"
// Mock dependencies
vi.mock("@welshman/lib", async () => ({
ctx: {
app: {
requestDelay: 50,
authTimeout: 500,
requestTimeout: 3000,
},
},
isNil: vi.fn(x => x === null || x === undefined),
}))
vi.mock("@welshman/util", async () => ({
LOCAL_RELAY_URL: "ws://localhost:3000",
getFilterResultCardinality: vi.fn(),
}))
vi.mock("@welshman/net", async () => {
const mockEmitter = {
emit: vi.fn(),
on: vi.fn(),
close: vi.fn(),
}
return {
subscribe: vi.fn(() => mockEmitter),
SubscriptionEvent: {
Event: "event",
Complete: "complete",
Eose: "eose",
},
}
})
vi.mock("../src/core.js", async () => ({
repository: {
query: vi.fn(() => []),
},
}))
describe("subscribe.ts", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
describe("subscribe", () => {
it("should pass request to baseSubscribe with default options", () => {
const request = {
filters: [{kinds: [1], limit: 10}],
}
const result = subscribe(request)
// Assert
expect(baseSubscribe).toHaveBeenCalledWith({
filters: [{kinds: [1], limit: 10}],
relays: [],
delay: ctx.app.requestDelay,
authTimeout: ctx.app.authTimeout,
timeout: 0, // timeout should be 0 when closeOnEose is not set
})
expect(result).toBeDefined()
})
it("should check filter cardinality when closeOnEose is true", () => {
const request = {
filters: [{kinds: [1], limit: 10}],
closeOnEose: true,
}
vi.mocked(getFilterResultCardinality).mockReturnValue(10)
subscribe(request)
// Assert
expect(getFilterResultCardinality).toHaveBeenCalledWith({kinds: [1], limit: 10})
})
it("should use cached results when filter cardinality matches repository results", () => {
// Arrange
const filter = {kinds: [1], limit: 2}
const cachedEvents = [
{id: "1", kind: 1, content: "test1", tags: [], pubkey: "pk1", created_at: 123, sig: "sig1"},
{id: "2", kind: 1, content: "test2", tags: [], pubkey: "pk2", created_at: 456, sig: "sig2"},
]
const request = {
filters: [filter],
closeOnEose: true,
}
vi.mocked(getFilterResultCardinality).mockReturnValue(2)
vi.mocked(repository.query).mockReturnValue(cachedEvents)
// Act
const sub = subscribe(request)
vi.runAllTimers() // Run setTimeout
// Assert
expect(repository.query).toHaveBeenCalledWith([filter])
expect(sub.emit).toHaveBeenCalledWith(
SubscriptionEvent.Event,
LOCAL_RELAY_URL,
cachedEvents[0],
)
expect(sub.emit).toHaveBeenCalledWith(
SubscriptionEvent.Event,
LOCAL_RELAY_URL,
cachedEvents[1],
)
expect(request.filters).toEqual([]) // All filters should be removed
})
it("should keep filter when repository has fewer results than cardinality", () => {
// Arrange
const filter = {kinds: [1], limit: 10}
const cachedEvents = [
{id: "1", kind: 1, content: "test1", tags: [], pubkey: "pk1", created_at: 123, sig: "sig1"},
]
const request = {
filters: [filter],
closeOnEose: true,
}
vi.mocked(getFilterResultCardinality).mockReturnValue(10)
vi.mocked(repository.query).mockReturnValue(cachedEvents)
// Act
subscribe(request)
// Assert
expect(repository.query).toHaveBeenCalledWith([filter])
expect(request.filters).toEqual([filter]) // Filter should be kept
})
it("should set timeout when closeOnEose is true", () => {
// Arrange
const request = {
filters: [{kinds: [1], limit: 10}],
closeOnEose: true,
}
// Act
subscribe(request)
// Assert
expect(baseSubscribe).toHaveBeenCalledWith(
expect.objectContaining({
timeout: ctx.app.requestTimeout,
}),
)
})
it("should respect custom options", () => {
// Arrange
const request = {
filters: [{kinds: [1], limit: 10}],
relays: ["wss://relay.example.com"],
delay: 100,
timeout: 5000,
authTimeout: 1000,
}
// Act
subscribe(request)
// Assert
expect(baseSubscribe).toHaveBeenCalledWith(
expect.objectContaining({
relays: ["wss://relay.example.com"],
delay: 100,
timeout: 5000,
authTimeout: 1000,
}),
)
})
it("should emit cached events asynchronously", () => {
// Arrange
const filter = {kinds: [1], limit: 2}
const cachedEvents = [
{id: "1", kind: 1, content: "test1", tags: [], pubkey: "pk1", created_at: 123, sig: "sig1"},
{id: "2", kind: 1, content: "test2", tags: [], pubkey: "pk2", created_at: 456, sig: "sig2"},
]
const request = {
filters: [filter],
closeOnEose: true,
}
vi.mocked(getFilterResultCardinality).mockReturnValue(2)
vi.mocked(repository.query).mockReturnValue(cachedEvents)
// Act
const sub = subscribe(request)
// Assert - should not have emitted events synchronously
expect(sub.emit).not.toHaveBeenCalled()
// Fast-forward timers
vi.runAllTimers()
// Now events should be emitted
expect(sub.emit).toHaveBeenCalledTimes(2)
expect(sub.emit).toHaveBeenCalledWith(
SubscriptionEvent.Event,
LOCAL_RELAY_URL,
cachedEvents[0],
)
expect(sub.emit).toHaveBeenCalledWith(
SubscriptionEvent.Event,
LOCAL_RELAY_URL,
cachedEvents[1],
)
})
})
describe("load", () => {
it("should return a promise that resolves with events", async () => {
// Arrange
const request = {
filters: [{kinds: [1], limit: 10}],
}
const mockEvents = [
{id: "1", kind: 1, content: "test1", tags: [], pubkey: "pk1", created_at: 123, sig: "sig1"},
{id: "2", kind: 1, content: "test2", tags: [], pubkey: "pk2", created_at: 456, sig: "sig2"},
]
// Mock the subscribe function and event handling
vi.mocked(baseSubscribe).mockReturnValue({
on: (event, handler) => {
// Simulate events
if (event === SubscriptionEvent.Event) {
mockEvents.forEach(evt => handler(LOCAL_RELAY_URL, evt))
}
// Simulate completion
if (event === SubscriptionEvent.Complete) {
setTimeout(() => handler(), 0)
}
return {unsubscribe: vi.fn()}
},
emit: vi.fn(),
close: vi.fn(),
})
// Act
const promise = load(request)
vi.runAllTimers() // Run setTimeout for completion
// Assert
const events = await promise
expect(events).toEqual(mockEvents)
expect(baseSubscribe).toHaveBeenCalledWith(
expect.objectContaining({
closeOnEose: true,
timeout: ctx.app.requestTimeout,
}),
)
})
it("should override load options with request options", async () => {
// Arrange
const request = {
filters: [{kinds: [1], limit: 10}],
closeOnEose: false, // This should be overridden
timeout: 1000, // This should be overridden
}
// Mock minimal subscription to resolve immediately
vi.mocked(baseSubscribe).mockReturnValue({
on: (event, handler) => {
if (event === SubscriptionEvent.Complete) {
setTimeout(() => handler(), 0)
}
return {unsubscribe: vi.fn()}
},
emit: vi.fn(),
close: vi.fn(),
})
const promise = load(request)
vi.runAllTimers()
await promise
expect(baseSubscribe).toHaveBeenCalledWith(
expect.objectContaining({
closeOnEose: false,
timeout: 1000,
}),
)
})
it("should collect events from multiple sources", async () => {
// Arrange
const request = {
filters: [{kinds: [1], limit: 10}],
}
const localEvents = [
{
id: "1",
kind: 1,
content: "local1",
tags: [],
pubkey: "pk1",
created_at: 123,
sig: "sig1",
},
]
const remoteEvents = [
{
id: "2",
kind: 1,
content: "remote1",
tags: [],
pubkey: "pk2",
created_at: 456,
sig: "sig2",
},
]
// Simulate cached events triggering local events first
vi.mocked(getFilterResultCardinality).mockReturnValue(1)
vi.mocked(repository.query).mockReturnValue(localEvents)
// Mock subscription that also receives remote events
let eventHandler: any
let completeHandler: any
vi.mocked(baseSubscribe).mockReturnValue({
on: (event: string, handler: any) => {
if (event === SubscriptionEvent.Event) {
eventHandler = handler
// Simulate cached events
localEvents.forEach(evt => handler(LOCAL_RELAY_URL, evt))
}
if (event === SubscriptionEvent.Complete) {
completeHandler = handler
}
return {unsubscribe: vi.fn()}
},
emit: vi.fn(),
close: vi.fn(),
})
const promise = load(request)
// Simulate receiving remote events
if (eventHandler) {
remoteEvents.forEach(evt => eventHandler("wss://remote.com", evt))
}
// Simulate completion
if (completeHandler) {
setTimeout(() => completeHandler(), 0)
}
vi.runAllTimers()
// Assert
const events = await promise
expect(events.length).toBe(2)
expect(events).toContainEqual(localEvents[0])
expect(events).toContainEqual(remoteEvents[0])
})
})
})
+15 -51
View File
@@ -11,42 +11,6 @@ import {
tagZapSplit, tagZapSplit,
} from "../src/tags" } from "../src/tags"
// Mock dependencies
vi.mock(import("@welshman/lib"), async imports => ({
...(await imports()),
ctx: {
app: {
router: {
FromPubkey: vi.fn().mockReturnValue({
getUrl: () => "pubkey-relay-url",
}),
Event: vi.fn().mockReturnValue({
getUrl: () => "event-relay-url",
}),
EventRoots: vi.fn().mockReturnValue({
getUrl: () => "roots-relay-url",
}),
EventParents: vi.fn().mockReturnValue({
getUrl: () => "parents-relay-url",
}),
},
},
},
uniq: vi.fn(arr => Array.from(new Set(arr))),
remove: vi.fn((item, arr) => arr.filter(x => x !== item)),
nthEq: vi.fn((n, val) => (arr: any[]) => arr[n] === val),
}))
vi.mock("../src/session", () => ({
pubkey: {
get: vi.fn().mockReturnValue("current-user-pubkey"),
},
}))
vi.mock("../src/profiles", () => ({
displayProfileByPubkey: vi.fn().mockReturnValue("display-name"),
}))
describe("tags", () => { describe("tags", () => {
const id = "00".repeat(32) const id = "00".repeat(32)
const id1 = "11".repeat(32) const id1 = "11".repeat(32)
@@ -70,19 +34,19 @@ describe("tags", () => {
describe("tagZapSplit", () => { describe("tagZapSplit", () => {
it("should create zap split tag with default split", () => { it("should create zap split tag with default split", () => {
const result = tagZapSplit(pubkey1) const result = tagZapSplit(pubkey1)
expect(result).toEqual(["zap", pubkey1, "pubkey-relay-url", "1"]) expect(result).toEqual(["zap", pubkey1, expect.any(String), "1"])
}) })
it("should create zap split tag with custom split", () => { it("should create zap split tag with custom split", () => {
const result = tagZapSplit(pubkey1, 0.5) const result = tagZapSplit(pubkey1, 0.5)
expect(result).toEqual(["zap", pubkey1, "pubkey-relay-url", "0.5"]) expect(result).toEqual(["zap", pubkey1, expect.any(String), "0.5"])
}) })
}) })
describe("tagPubkey", () => { describe("tagPubkey", () => {
it("should create pubkey tag with relay hint and display name", () => { it("should create pubkey tag with relay hint and display name", () => {
const result = tagPubkey(pubkey1) const result = tagPubkey(pubkey1)
expect(result).toEqual(["p", pubkey1, "pubkey-relay-url", "display-name"]) expect(result).toEqual(["p", pubkey1, expect.any(String), expect.any(String)])
}) })
}) })
@@ -90,7 +54,7 @@ describe("tags", () => {
it("should create basic event tag", () => { it("should create basic event tag", () => {
const result = tagEvent(mockEvent) const result = tagEvent(mockEvent)
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
expect(result[0]).toEqual(["e", mockEvent.id, "event-relay-url", "", mockEvent.pubkey]) expect(result[0]).toEqual(["e", mockEvent.id, expect.any(String), "", mockEvent.pubkey])
}) })
it("should include address tag for replaceable events", () => { it("should include address tag for replaceable events", () => {
@@ -119,7 +83,7 @@ describe("tags", () => {
describe("tagEventForQuote", () => { describe("tagEventForQuote", () => {
it("should create quote tag", () => { it("should create quote tag", () => {
const result = tagEventForQuote(mockEvent) const result = tagEventForQuote(mockEvent)
expect(result).toEqual(["q", mockEvent.id, "event-relay-url", mockEvent.pubkey]) expect(result).toEqual(["q", mockEvent.id, expect.any(String), mockEvent.pubkey])
}) })
}) })
@@ -262,11 +226,11 @@ describe("tags", () => {
expect(result).toEqual([ expect(result).toEqual([
["K", String(NOTE)], ["K", String(NOTE)],
["P", pubkey, "pubkey-relay-url"], ["P", pubkey, expect.any(String)],
["E", id, "event-relay-url", pubkey], ["E", id, expect.any(String), pubkey],
["k", String(NOTE)], ["k", String(NOTE)],
["p", pubkey, "pubkey-relay-url"], ["p", pubkey, expect.any(String)],
["e", id, "event-relay-url", pubkey], ["e", id, expect.any(String), pubkey],
]) ])
}) })
@@ -286,13 +250,13 @@ describe("tags", () => {
expect(result).toEqual([ expect(result).toEqual([
["K", String(MUTES)], ["K", String(MUTES)],
["P", pubkey, "pubkey-relay-url"], ["P", pubkey, expect.any(String)],
["E", id, "event-relay-url", pubkey], ["E", id, expect.any(String), pubkey],
["A", getAddress(eventWithMixedTags), "event-relay-url", pubkey], ["A", getAddress(eventWithMixedTags), expect.any(String), pubkey],
["k", String(MUTES)], ["k", String(MUTES)],
["p", pubkey, "pubkey-relay-url"], ["p", pubkey, expect.any(String)],
["e", id, "event-relay-url", pubkey], ["e", id, expect.any(String), pubkey],
["a", getAddress(eventWithMixedTags), "event-relay-url", pubkey], ["a", getAddress(eventWithMixedTags), expect.any(String), pubkey],
]) ])
}) })
-229
View File
@@ -1,229 +0,0 @@
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import {ctx, fetchJson, bech32ToHex, hexToBech32, tryCatch, postJson} from "@welshman/lib"
import {fetchZappers} from "../src/zappers.ts"
// Mock dependencies
vi.mock("@welshman/lib", async imports => {
return {
...(await imports()),
ctx: {
app: {
dufflepudUrl: undefined, // Will be modified in tests
},
},
identity: x => x,
fetchJson: vi.fn(),
bech32ToHex: vi.fn(),
hexToBech32: vi.fn(),
tryCatch: vi.fn(fn => {
try {
return fn()
} catch (e) {
return undefined
}
}),
postJson: vi.fn(),
}
})
describe("fetchZappers", () => {
const mockLnurls = [
"lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0d3h82unvwqhkxctvd46820w0fjx",
"lnurl2wd68gurn8ghj7ur5v9kxjerrv9kzum5v93kket39ehx7mmwp5hxsmwda8hx",
]
const mockHexLnurls = ["41414141414141", "42424242424242"]
const mockZapperInfo1 = {
callback: "https://zapper1.com/callback",
minSendable: 1000,
maxSendable: 100000000,
metadata: JSON.stringify([["text/plain", "Zapper One"]]),
}
const mockZapperInfo2 = {
callback: "https://zapper2.com/callback",
minSendable: 2000,
maxSendable: 200000000,
metadata: JSON.stringify([["text/plain", "Zapper Two"]]),
}
beforeEach(() => {
vi.clearAllMocks()
// Default bech32ToHex mockup with 1:1 mapping to hexes
vi.mocked(bech32ToHex).mockImplementation(lnurl => {
if (lnurl === mockLnurls[0]) return mockHexLnurls[0]
if (lnurl === mockLnurls[1]) return mockHexLnurls[1]
throw new Error("Invalid lnurl")
})
// Default hexToBech32 mockup with inverse mapping
vi.mocked(hexToBech32).mockImplementation((prefix, hex) => {
if (hex === mockHexLnurls[0]) return mockLnurls[0]
if (hex === mockHexLnurls[1]) return mockLnurls[1]
throw new Error("Invalid hex")
})
})
afterEach(() => {
vi.resetAllMocks()
})
it("should fetch zappers using dufflepud when URL is provided", async () => {
// Arrange
const dufflepudUrl = "https://dufflepud.com"
ctx.app.dufflepudUrl = dufflepudUrl
vi.mocked(postJson).mockResolvedValue({
data: [
{lnurl: mockHexLnurls[0], info: mockZapperInfo1},
{lnurl: mockHexLnurls[1], info: mockZapperInfo2},
],
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(postJson).toHaveBeenCalledWith(`${dufflepudUrl}/zapper/info`, {lnurls: mockHexLnurls})
expect(bech32ToHex).toHaveBeenCalledTimes(2)
expect(hexToBech32).toHaveBeenCalledTimes(2)
expect(result.size).toBe(2)
expect(result.get(mockLnurls[0])).toEqual(mockZapperInfo1)
expect(result.get(mockLnurls[1])).toEqual(mockZapperInfo2)
})
it("should fetch zappers directly when dufflepud URL is not provided", async () => {
// Arrange
ctx.app.dufflepudUrl = undefined
vi.mocked(fetchJson).mockImplementation(async url => {
if (url === mockHexLnurls[0]) return mockZapperInfo1
if (url === mockHexLnurls[1]) return mockZapperInfo2
throw new Error("Invalid URL")
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(fetchJson).toHaveBeenCalledWith(mockHexLnurls[0])
expect(fetchJson).toHaveBeenCalledWith(mockHexLnurls[1])
expect(postJson).not.toHaveBeenCalled()
expect(result.size).toBe(2)
expect(result.get(mockLnurls[0])).toEqual(mockZapperInfo1)
expect(result.get(mockLnurls[1])).toEqual(mockZapperInfo2)
})
it("should handle invalid lnurls when using dufflepud", async () => {
// Arrange
ctx.app.dufflepudUrl = "https://dufflepud.com"
// Make only the first lnurl valid
vi.mocked(bech32ToHex).mockImplementation(lnurl => {
if (lnurl === mockLnurls[0]) return mockHexLnurls[0]
throw new Error("Invalid lnurl")
})
vi.mocked(postJson).mockResolvedValue({
data: [{lnurl: mockHexLnurls[0], info: mockZapperInfo1}],
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(postJson).toHaveBeenCalledWith(`${ctx.app.dufflepudUrl}/zapper/info`, {
lnurls: [mockHexLnurls[0]],
})
expect(result.size).toBe(1)
expect(result.get(mockLnurls[0])).toEqual(mockZapperInfo1)
expect(result.get(mockLnurls[1])).toBeUndefined()
})
it("should handle invalid lnurls when fetching directly", async () => {
// Arrange
ctx.app.dufflepudUrl = undefined
// Make only the first lnurl valid
vi.mocked(bech32ToHex).mockImplementation(lnurl => {
if (lnurl === mockLnurls[0]) return mockHexLnurls[0]
throw new Error("Invalid lnurl")
})
vi.mocked(fetchJson).mockImplementation(async url => {
if (url === mockHexLnurls[0]) return mockZapperInfo1
throw new Error("Invalid URL")
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(fetchJson).toHaveBeenCalledWith(mockHexLnurls[0])
expect(fetchJson).toHaveBeenCalledTimes(1)
expect(result.size).toBe(1)
expect(result.get(mockLnurls[0])).toEqual(mockZapperInfo1)
expect(result.get(mockLnurls[1])).toBeUndefined()
})
it("should handle empty lnurl list", async () => {
// Arrange
ctx.app.dufflepudUrl = "https://dufflepud.com"
// Act
const result = await fetchZappers([])
// Assert
expect(postJson).not.toHaveBeenCalled()
expect(fetchJson).not.toHaveBeenCalled()
expect(result.size).toBe(0)
})
it("should handle malformed zapper responses", async () => {
// Arrange
ctx.app.dufflepudUrl = "https://dufflepud.com"
vi.mocked(postJson).mockResolvedValue({
// Missing data field
wrong_field: [],
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(postJson).toHaveBeenCalled()
expect(result.size).toBe(0)
})
it("should handle hexToBech32 errors when processing dufflepud response", async () => {
// Arrange
ctx.app.dufflepudUrl = "https://dufflepud.com"
vi.mocked(hexToBech32).mockImplementation(() => {
throw new Error("Invalid hex")
})
vi.mocked(postJson).mockResolvedValue({
data: [
{lnurl: mockHexLnurls[0], info: mockZapperInfo1},
{lnurl: mockHexLnurls[1], info: mockZapperInfo2},
],
})
// Act
const result = await fetchZappers(mockLnurls)
// Assert
expect(postJson).toHaveBeenCalled()
expect(hexToBech32).toHaveBeenCalled()
expect(result.size).toBe(0)
})
})
+6 -6
View File
@@ -9,40 +9,40 @@ export const unfollow = async (value: string) => {
const list = get(userFollows) || makeList({kind: FOLLOWS}) const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
export const follow = async (tag: string[]) => { export const follow = async (tag: string[]) => {
const list = get(userFollows) || makeList({kind: FOLLOWS}) const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
export const unmute = async (value: string) => { export const unmute = async (value: string) => {
const list = get(userMutes) || makeList({kind: MUTES}) const list = get(userMutes) || makeList({kind: MUTES})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
export const mute = async (tag: string[]) => { export const mute = async (tag: string[]) => {
const list = get(userMutes) || makeList({kind: MUTES}) const list = get(userMutes) || makeList({kind: MUTES})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
export const unpin = async (value: string) => { export const unpin = async (value: string) => {
const list = get(userPins) || makeList({kind: PINS}) const list = get(userPins) || makeList({kind: PINS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
export const pin = async (tag: string[]) => { export const pin = async (tag: string[]) => {
const list = get(userPins) || makeList({kind: PINS}) const list = get(userPins) || makeList({kind: PINS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()}) return publishThunk({event, relays: Router.get().FromUser().getUrls()})
} }
+2 -2
View File
@@ -39,8 +39,8 @@ export const requestDVM = async ({kind, onEvent, ...request}: DVMOpts) => {
const $signer = signer.get() || new Nip01Signer(makeSecret()) const $signer = signer.get() || new Nip01Signer(makeSecret())
const pubkey = await $signer.getPubkey() const pubkey = await $signer.getPubkey()
const relays = request.relays const relays = request.relays
? Router.getInstance().FromRelays(request.relays).getUrls() ? Router.get().FromRelays(request.relays).getUrls()
: Router.getInstance().FromPubkeys(getPubkeyTagValues(tags)).getUrls() : Router.get().FromPubkeys(getPubkeyTagValues(tags)).getUrls()
if (!tags.some(nthEq(0, "expiration"))) { if (!tags.some(nthEq(0, "expiration"))) {
tags.push(["expiration", String(now() + 60)]) tags.push(["expiration", String(now() + 60)])
+1 -1
View File
@@ -25,7 +25,7 @@ export const {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [FOLLOWS], authors: [pubkey]} const filter = {kinds: [FOLLOWS], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filter})
}, },
+1 -1
View File
@@ -31,7 +31,7 @@ export const {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [MUTES], authors: [pubkey]} const filter = {kinds: [MUTES], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filter})
}, },
+1 -1
View File
@@ -25,7 +25,7 @@ export const {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [PINS], authors: [pubkey]} const filter = {kinds: [PINS], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filter})
}, },
+1 -1
View File
@@ -27,7 +27,7 @@ export const {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const router = Router.getInstance() const router = Router.get()
const filter = {kinds: [PROFILE], authors: [pubkey]} const filter = {kinds: [PROFILE], authors: [pubkey]}
const relays = router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls() const relays = router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls()
+2 -2
View File
@@ -48,7 +48,7 @@ export const {
store: relaySelections, store: relaySelections,
getKey: relaySelections => relaySelections.event.pubkey, getKey: relaySelections => relaySelections.event.pubkey,
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
const router = Router.getInstance() const router = Router.get()
await load({ await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(), relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
@@ -73,7 +73,7 @@ export const {
store: inboxRelaySelections, store: inboxRelaySelections,
getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey, getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey,
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
const router = Router.getInstance() const router = Router.get()
await load({ await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(), relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
+7 -7
View File
@@ -194,7 +194,7 @@ export class Router {
Object.assign(globalRouterOptions, options) Object.assign(globalRouterOptions, options)
} }
static getInstance() { static get() {
return new Router(globalRouterOptions) return new Router(globalRouterOptions)
} }
@@ -422,7 +422,7 @@ export const getFilterSelectionsForSearch = (filter: Filter) => {
const relays = globalRouterOptions.getSearchRelays?.() || [] const relays = globalRouterOptions.getSearchRelays?.() || []
return [{filter, scenario: Router.getInstance().FromRelays(relays).weight(10)}] return [{filter, scenario: Router.get().FromRelays(relays).weight(10)}]
} }
export const getFilterSelectionsForWraps = (filter: Filter) => { export const getFilterSelectionsForWraps = (filter: Filter) => {
@@ -431,7 +431,7 @@ export const getFilterSelectionsForWraps = (filter: Filter) => {
return [ return [
{ {
filter: {...filter, kinds: [WRAP]}, filter: {...filter, kinds: [WRAP]},
scenario: Router.getInstance().UserInbox(), scenario: Router.get().UserInbox(),
}, },
] ]
} }
@@ -446,7 +446,7 @@ export const getFilterSelectionsForIndexedKinds = (filter: Filter) => {
return [ return [
{ {
filter: {...filter, kinds}, filter: {...filter, kinds},
scenario: Router.getInstance().FromRelays(relays), scenario: Router.get().FromRelays(relays),
}, },
] ]
} }
@@ -458,12 +458,12 @@ export const getFilterSelectionsForAuthors = (filter: Filter) => {
return chunks(chunkCount, filter.authors).map(authors => ({ return chunks(chunkCount, filter.authors).map(authors => ({
filter: {...filter, authors}, filter: {...filter, authors},
scenario: Router.getInstance().FromPubkeys(authors), scenario: Router.get().FromPubkeys(authors),
})) }))
} }
export const getFilterSelectionsForUser = (filter: Filter) => [ export const getFilterSelectionsForUser = (filter: Filter) => [
{filter, scenario: Router.getInstance().ForUser().weight(0.2)}, {filter, scenario: Router.get().ForUser().weight(0.2)},
] ]
export const defaultFilterSelectionRules = [ export const defaultFilterSelectionRules = [
@@ -493,7 +493,7 @@ export const getFilterSelections = (
const result = [] const result = []
for (const [id, filter] of filtersById.entries()) { for (const [id, filter] of filtersById.entries()) {
const scenario = Router.getInstance().merge(scenariosById.get(id) || []) const scenario = Router.get().merge(scenariosById.get(id) || [])
result.push({filters: [filter], relays: scenario.getUrls()}) result.push({filters: [filter], relays: scenario.getUrls()})
} }
+1 -1
View File
@@ -56,7 +56,7 @@ export const searchProfiles = debounce(500, (search: string) => {
if (search.length > 2) { if (search.length > 2) {
load({ load({
filter: {kinds: [PROFILE], search}, filter: {kinds: [PROFILE], search},
relays: Router.getInstance().Search().getUrls(), relays: Router.get().Search().getUrls(),
}) })
} }
}) })
+10 -10
View File
@@ -14,19 +14,19 @@ import {Router} from "./router.js"
export const tagZapSplit = (pubkey: string, split = 1) => [ export const tagZapSplit = (pubkey: string, split = 1) => [
"zap", "zap",
pubkey, pubkey,
Router.getInstance().FromPubkey(pubkey).getUrl(), Router.get().FromPubkey(pubkey).getUrl(),
String(split), String(split),
] ]
export const tagPubkey = (pubkey: string, ...args: unknown[]) => [ export const tagPubkey = (pubkey: string, ...args: unknown[]) => [
"p", "p",
pubkey, pubkey,
Router.getInstance().FromPubkey(pubkey).getUrl(), Router.get().FromPubkey(pubkey).getUrl(),
displayProfileByPubkey(pubkey), displayProfileByPubkey(pubkey),
] ]
export const tagEvent = (event: TrustedEvent, mark = "") => { export const tagEvent = (event: TrustedEvent, mark = "") => {
const url = Router.getInstance().Event(event).getUrl() const url = Router.get().Event(event).getUrl()
const tags = [["e", event.id, url, mark, event.pubkey]] const tags = [["e", event.id, url, mark, event.pubkey]]
if (isReplaceable(event)) { if (isReplaceable(event)) {
@@ -42,7 +42,7 @@ export const tagEventPubkeys = (event: TrustedEvent) =>
export const tagEventForQuote = (event: TrustedEvent) => [ export const tagEventForQuote = (event: TrustedEvent) => [
"q", "q",
event.id, event.id,
Router.getInstance().Event(event).getUrl(), Router.get().Event(event).getUrl(),
event.pubkey, event.pubkey,
] ]
@@ -55,11 +55,11 @@ export const tagEventForReply = (event: TrustedEvent) => {
// Root comes first // Root comes first
if (roots.length > 0) { if (roots.length > 0) {
for (const t of roots) { for (const t of roots) {
tags.push([...t.slice(0, 2), Router.getInstance().EventRoots(event).getUrl(), "root"]) tags.push([...t.slice(0, 2), Router.get().EventRoots(event).getUrl(), "root"])
} }
} else { } else {
for (const t of replies) { for (const t of replies) {
tags.push([...t.slice(0, 2), Router.getInstance().EventParents(event).getUrl(), "root"]) tags.push([...t.slice(0, 2), Router.get().EventParents(event).getUrl(), "root"])
} }
} }
@@ -81,7 +81,7 @@ export const tagEventForReply = (event: TrustedEvent) => {
// Finally, tag the event itself // Finally, tag the event itself
const mark = replies.length > 0 ? "reply" : "root" const mark = replies.length > 0 ? "reply" : "root"
const hint = Router.getInstance().Event(event).getUrl() const hint = Router.get().Event(event).getUrl()
// e-tag the event // e-tag the event
tags.push(["e", event.id, hint, mark, event.pubkey]) tags.push(["e", event.id, hint, mark, event.pubkey])
@@ -95,8 +95,8 @@ export const tagEventForReply = (event: TrustedEvent) => {
} }
export const tagEventForComment = (event: TrustedEvent) => { export const tagEventForComment = (event: TrustedEvent) => {
const pubkeyHint = Router.getInstance().FromPubkey(event.pubkey).getUrl() const pubkeyHint = Router.get().FromPubkey(event.pubkey).getUrl()
const eventHint = Router.getInstance().Event(event).getUrl() const eventHint = Router.get().Event(event).getUrl()
const address = getAddress(event) const address = getAddress(event)
const seenRoots = new Set<string>() const seenRoots = new Set<string>()
const tags: string[][] = [] const tags: string[][] = []
@@ -130,7 +130,7 @@ export const tagEventForComment = (event: TrustedEvent) => {
} }
export const tagEventForReaction = (event: TrustedEvent) => { export const tagEventForReaction = (event: TrustedEvent) => {
const hint = Router.getInstance().Event(event).getUrl() const hint = Router.get().Event(event).getUrl()
const tags: string[][] = [] const tags: string[][] = []
// Mention the event's author // Mention the event's author
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@welshman/net", "name": "@welshman/net",
"version": "0.0.48", "version": "0.0.49",
"author": "hodlbod", "author": "hodlbod",
"license": "MIT", "license": "MIT",
"description": "Utilities for connecting with nostr relays.", "description": "Utilities for connecting with nostr relays.",
+8 -104
View File
@@ -1,20 +1,10 @@
import {now} from "@welshman/lib"
import {describe, it, expect, beforeEach, vi, afterEach} from "vitest" import {describe, it, expect, beforeEach, vi, afterEach} from "vitest"
import { import {now} from "@welshman/lib"
Relay, import type {TrustedEvent} from "@welshman/util"
normalizeRelayUrl, import {LocalRelay} from "../src/relay"
isRelayUrl, import {Repository} from "../src/repository"
isOnionUrl,
isLocalUrl,
isIPAddress,
isShareableRelayUrl,
displayRelayUrl,
displayRelayProfile,
} from "../src/Relay"
import {Repository} from "../src/Repository"
import type {TrustedEvent} from "../src/Events"
describe("Relay", () => { describe("LocalRelay", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
vi.useFakeTimers() vi.useFakeTimers()
@@ -42,99 +32,13 @@ describe("Relay", () => {
...overrides, ...overrides,
}) })
describe("URL utilities", () => { describe("LocalRelay class", () => {
describe("isRelayUrl", () => { let relay: LocalRelay
it("should validate proper relay URLs", () => {
expect(isRelayUrl("wss://relay.example.com")).toBe(true)
expect(isRelayUrl("ws://relay.example.com")).toBe(true)
expect(isRelayUrl("relay.example.com")).toBe(true)
})
it("should reject invalid URLs", () => {
expect(isRelayUrl("http://relay.example.com")).toBe(false)
expect(isRelayUrl("not-a-url")).toBe(false)
expect(isRelayUrl("ws:\\example.com\\path\\to\\file.ext")).toBe(false)
})
})
describe("isOnionUrl", () => {
it("should validate onion URLs", () => {
expect(isOnionUrl(onionUrl)).toBe(true)
})
it("should reject non-onion URLs", () => {
expect(isOnionUrl("wss://relay.example.com")).toBe(false)
})
})
describe("isLocalUrl", () => {
it("should validate local URLs", () => {
expect(isLocalUrl("wss://relay.local")).toBe(true)
expect(isLocalUrl("ws://localhost:8080")).toBe(true)
})
it("should reject non-local URLs", () => {
expect(isLocalUrl("wss://relay.example.com")).toBe(false)
})
})
describe("isIPAddress", () => {
it("should validate IP addresses", () => {
expect(isIPAddress("wss://192.168.1.1")).toBe(true)
})
it("should reject domains", () => {
expect(isIPAddress("wss://relay.example.com")).toBe(false)
})
})
describe("isShareableRelayUrl", () => {
it("should validate shareable URLs", () => {
expect(isShareableRelayUrl("wss://relay.example.com")).toBe(true)
})
it("should reject local URLs", () => {
expect(isShareableRelayUrl("wss://relay.local")).toBe(false)
})
})
describe("normalizeRelayUrl", () => {
it("should normalize URLs consistently", () => {
expect(normalizeRelayUrl("relay.example.com")).toBe("wss://relay.example.com/")
expect(normalizeRelayUrl("wss://RELAY.EXAMPLE.COM")).toBe("wss://relay.example.com/")
})
it("should handle onion URLs", () => {
expect(normalizeRelayUrl(onionUrl)).toBe(`ws://${onionUrl}/`)
})
})
describe("displayRelayUrl", () => {
it("should format URLs for display", () => {
expect(displayRelayUrl("wss://relay.example.com/")).toBe("relay.example.com")
})
})
describe("displayRelayProfile", () => {
it("should display profile name when available", () => {
const profile = {url: "wss://relay.example.com", name: "Test Relay"}
expect(displayRelayProfile(profile)).toBe("Test Relay")
})
it("should use fallback when no name", () => {
const profile = {url: "wss://relay.example.com"}
expect(displayRelayProfile(profile, "Fallback")).toBe("Fallback")
})
})
})
describe("Relay class", () => {
let relay: Relay
let repository: Repository<TrustedEvent> let repository: Repository<TrustedEvent>
beforeEach(() => { beforeEach(() => {
repository = new Repository<TrustedEvent>() repository = new Repository<TrustedEvent>()
relay = new Relay(repository) relay = new LocalRelay(repository)
}) })
describe("EVENT handling", () => { describe("EVENT handling", () => {
+2 -4
View File
@@ -1,9 +1,7 @@
import {now} from "@welshman/lib"
import {getAddress} from "@welshman/util"
import {describe, it, vi, expect, beforeEach} from "vitest" import {describe, it, vi, expect, beforeEach} from "vitest"
import {now} from "@welshman/lib"
import {getAddress, TrustedEvent, DELETE, MUTES} from "@welshman/util"
import {Repository} from "../src/Repository" import {Repository} from "../src/Repository"
import type {TrustedEvent} from "../src/Events"
import {DELETE, MUTES} from "../src/Kinds"
describe("Repository", () => { describe("Repository", () => {
beforeEach(() => { beforeEach(() => {
-673
View File
@@ -1,673 +0,0 @@
import {afterAll, beforeEach, describe, expect, it, vi} from "vitest"
import {
Nip46Signer,
Nip46Broker,
Nip46Event,
Nip46Receiver,
Nip46Sender,
Nip46Request,
Nip46Response,
Nip46BrokerParams,
} from "../src/signers/nip46"
import {testSigner} from "./common"
import {NOSTR_CONNECT, SignedEvent, TrustedEvent} from "@welshman/util"
import {publish, subscribe, SubscriptionEvent} from "@welshman/net"
import {now} from "@welshman/lib"
const mockSubscription = {
on: vi.fn(),
close: vi.fn(),
}
vi.mock(import("@welshman/net"), async importOriginal => ({
...(await importOriginal()),
subscribe: vi.fn().mockImplementation(() => mockSubscription),
publish: vi.fn().mockImplementation(() => ({
emitter: {
on: vi.fn(),
},
})),
}))
describe("Nip46Signer", () => {
let mockBroker: any
const signerPubkey = "ee".repeat(32)
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
mockBroker = {
getPublicKey: vi.fn().mockResolvedValue("ee".repeat(32)),
signEvent: vi.fn().mockResolvedValue({sig: "ff".repeat(64)} as SignedEvent),
nip04Encrypt: vi.fn((pubkey, message) => "encrypted:" + message),
nip04Decrypt: vi.fn((pubkey, encryptedMessage) => encryptedMessage.split("encrypted:")[1]),
nip44Encrypt: vi.fn((pubkey, message) => "encrypted:" + message),
nip44Decrypt: vi.fn((pubkey, encryptedMessage) => encryptedMessage.split("encrypted:")[1]),
}
})
afterAll(() => {
vi.useRealTimers()
})
testSigner("Nip46Signer", () => new Nip46Signer(mockBroker))
describe("Nip46Receiver", () => {
let mockSigner: any
let receiver: Nip46Receiver
beforeEach(() => {
mockSigner = {
getPubkey: vi.fn().mockResolvedValue("test-pubkey"),
nip04: {
decrypt: vi.fn().mockResolvedValue('{"method":"test","params":[]}'),
},
nip44: {
decrypt: vi.fn().mockResolvedValue('{"method":"test","params":[]}'),
},
}
receiver = new Nip46Receiver(mockSigner, {
relays: ["wss://relay.test"],
clientSecret: "test-secret",
})
})
it("should setup subscription with correct filters", async () => {
receiver.start()
await vi.advanceTimersToNextTimerAsync()
expect(subscribe).toHaveBeenCalledWith({
relays: ["wss://relay.test"],
filters: [
{
kinds: [NOSTR_CONNECT],
"#p": ["test-pubkey"],
},
],
})
})
it("should handle incoming events", async () => {
const receiveSpy = vi.fn()
receiver.on(Nip46Event.Receive, receiveSpy)
receiver.start()
await vi.advanceTimersToNextTimerAsync()
// Get the event handler
const eventHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Event,
)[1]
// Simulate incoming event
await eventHandler("wss://relay.test", {
pubkey: "sender-pubkey",
content: "encrypted-content",
} as TrustedEvent)
expect(receiveSpy).toHaveBeenCalledWith(
expect.objectContaining({
url: "wss://relay.test",
event: expect.any(Object),
}),
)
})
it("should cleanup on stop", async () => {
receiver.start()
await vi.advanceTimersToNextTimerAsync()
receiver.stop()
expect(mockSubscription.close).toHaveBeenCalled()
})
})
describe("Nip46Sender", () => {
let mockSigner: any
let sender: Nip46Sender
let mockPublish: any
beforeEach(() => {
vi.clearAllMocks()
mockSigner = {
getPubkey: vi.fn().mockResolvedValue("test-pubkey"),
sign: vi.fn(template => ({...template, sig: "ee".repeat(64)})),
nip44: {
encrypt: vi.fn((pub, message) => "encrypted:" + message),
},
}
mockPublish = {
emitter: {on: vi.fn()},
}
vi.mocked(publish).mockReturnValue(mockPublish)
sender = new Nip46Sender(mockSigner, {
relays: ["wss://relay.test"],
clientSecret: "test-secret",
signerPubkey,
})
})
it("should encrypt and send request", async () => {
const request = new Nip46Request("test-method", ["param1"])
await sender.send(request)
expect(mockSigner.nip44.encrypt).toHaveBeenCalledWith(signerPubkey, expect.any(String))
expect(publish).toHaveBeenCalledWith(
expect.objectContaining({
relays: ["wss://relay.test"],
event: expect.any(Object),
}),
)
})
it("should throw error if no signer pubkey", async () => {
sender = new Nip46Sender(mockSigner, {
relays: ["wss://relay.test"],
clientSecret: "test-secret",
})
const request = new Nip46Request("test-method", ["param1"])
await expect(sender.send(request)).rejects.toThrow("signer pubkey")
})
it("should process queue sequentially", async () => {
const request1 = new Nip46Request("method1", ["param1"])
const request2 = new Nip46Request("method2", ["param2"])
sender.enqueue(request1)
sender.enqueue(request2)
await vi.runAllTimersAsync()
// Check that requests were processed in order
const calls = vi.mocked(publish).mock.calls
expect(calls[0][0].event.content).toContain("method1")
expect(calls[1][0].event.content).toContain("method2")
})
})
describe("Nip46Request", () => {
let mockReceiver: any
let mockSender: any
let request: Nip46Request
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
mockReceiver = {
start: vi.fn().mockResolvedValue(undefined),
on: vi.fn(),
off: vi.fn(),
}
mockSender = {
enqueue: vi.fn(),
}
request = new Nip46Request("test-method", ["param1"])
})
afterAll(() => {
vi.useRealTimers()
})
it("should handle successful response", async () => {
// Setup response handler
let responseHandler: (response: Nip46Response) => void
mockReceiver.on.mockImplementation((event, handler) => {
responseHandler = handler
})
// Start listening
const listenPromise = request.listen(mockReceiver)
await vi.advanceTimersToNextTimerAsync()
// Simulate successful response
responseHandler!({
id: request.id,
url: "wss://relay.test",
event: {} as TrustedEvent,
result: "success",
})
await listenPromise
const result = await request.promise
expect(result.result).toBe("success")
})
it("should handle error response", async () => {
let responseHandler: (response: Nip46Response) => void
mockReceiver.on.mockImplementation((event, handler) => {
responseHandler = handler
})
const listenPromise = request.listen(mockReceiver)
await vi.advanceTimersToNextTimerAsync()
responseHandler!({
id: request.id,
url: "wss://relay.test",
event: {} as TrustedEvent,
error: "test error",
})
await listenPromise
await expect(request.promise).rejects.toMatchObject({
error: "test error",
})
})
it("should handle auth_url result", async () => {
const popupSpy = vi.spyOn(window, "open")
let responseHandler: (response: Nip46Response) => void
mockReceiver.on.mockImplementation((event, handler) => {
responseHandler = handler
})
const listenPromise = request.listen(mockReceiver)
await vi.advanceTimersToNextTimerAsync()
responseHandler!({
id: request.id,
url: "wss://relay.test",
event: {} as TrustedEvent,
result: "auth_url",
error: "https://auth.test",
})
await listenPromise
expect(popupSpy).toHaveBeenCalledWith(
"https://auth.test",
expect.any(String),
expect.any(String),
)
})
it("should ignore responses with different ids", async () => {
let responseHandler: (response: Nip46Response) => void
mockReceiver.on.mockImplementation((event, handler) => {
responseHandler = handler
})
const listenPromise = request.listen(mockReceiver)
await vi.advanceTimersToNextTimerAsync()
responseHandler!({
id: "different-id",
url: "wss://relay.test",
event: {} as TrustedEvent,
result: "success",
})
await listenPromise
// Promise should not be resolved
const promiseStatus = await Promise.race([
request.promise,
vi.advanceTimersByTimeAsync(100).then(() => "timeout"),
])
expect(promiseStatus).toBe("timeout")
})
it("should enqueue request on send", async () => {
await request.send(mockSender)
expect(mockSender.enqueue).toHaveBeenCalledWith(request)
})
})
describe("Nip46Broker", () => {
let defaultParams: Nip46BrokerParams
const pubkey = "cc".repeat(32)
beforeEach(() => {
vi.clearAllMocks()
defaultParams = {
relays: ["wss://relay.test"],
clientSecret: "dd".repeat(32),
signerPubkey: "ee".repeat(32),
}
})
describe("singleton management", () => {
it("should maintain single instance with same params", () => {
const broker1 = Nip46Broker.get(defaultParams)
const broker2 = Nip46Broker.get(defaultParams)
expect(broker1).toBe(broker2)
})
it("should create new instance with different params", () => {
const broker1 = Nip46Broker.get(defaultParams)
const broker2 = Nip46Broker.get({
...defaultParams,
relays: ["wss://other.relay"],
})
expect(broker1).not.toBe(broker2)
})
it("should teardown old instance when creating new one", () => {
const broker1 = Nip46Broker.get(defaultParams)
const teardownSpy = vi.spyOn(broker1, "teardown")
Nip46Broker.get({
...defaultParams,
relays: ["wss://other.relay"],
})
expect(teardownSpy).toHaveBeenCalled()
})
})
describe("URL handling", () => {
it("should parse bunker URL correctly", () => {
const url = `bunker://${pubkey}?relay=wss://relay1.test&relay=wss://relay2.test&secret=testsecret`
const result = Nip46Broker.parseBunkerUrl(url)
expect(result).toEqual({
signerPubkey: pubkey,
relays: ["wss://relay1.test/", "wss://relay2.test/"],
connectSecret: "testsecret",
})
})
it("should handle invalid bunker URL", () => {
const result = Nip46Broker.parseBunkerUrl("invalid-url")
expect(result).toEqual({
signerPubkey: "",
connectSecret: "",
relays: [],
})
})
it("should generate bunker URL", () => {
const broker = new Nip46Broker(defaultParams)
const url = broker.getBunkerUrl()
expect(url).toContain("bunker://")
expect(url).toContain(defaultParams.signerPubkey)
expect(url).toContain(encodeURIComponent(defaultParams.relays[0]))
})
it("should throw when generating bunker URL without signerPubkey", () => {
const broker = new Nip46Broker({
...defaultParams,
signerPubkey: undefined,
})
expect(() => broker.getBunkerUrl()).toThrow("no signerPubkey")
})
it("should generate nostrconnect URL", async () => {
const broker = new Nip46Broker(defaultParams)
const url = await broker.makeNostrconnectUrl({app: "test"})
expect(url).toContain("nostrconnect://")
expect(url).toContain("app=test")
expect(url).toContain("secret=")
expect(url).toContain(encodeURIComponent(defaultParams.relays[0]))
})
})
describe("connection handling", () => {
it("should handle nostrconnect response", async () => {
const broker = new Nip46Broker(defaultParams)
const url = await broker.makeNostrconnectUrl()
// Start waiting for response
const connectPromise = broker.waitForNostrconnect(url)
// Get the secret from the URL we're connecting to
const secret = new URL(url).searchParams.get("secret")
// Simulate a response through the broker's receiver
broker.receiver.emit(Nip46Event.Receive, {
result: secret,
event: {pubkey: "responder-pubkey"},
})
const response = await connectPromise
expect(broker.params.signerPubkey).toBe("responder-pubkey")
})
it("should handle connection abort", async () => {
const broker = new Nip46Broker(defaultParams)
const url = await broker.makeNostrconnectUrl()
const controller = new AbortController()
const connectPromise = broker.waitForNostrconnect(url, controller)
controller.abort()
await expect(connectPromise).rejects.toBeUndefined()
})
})
describe("NIP-46 methods", () => {
let broker: Nip46Broker
beforeEach(() => {
broker = new Nip46Broker(defaultParams)
})
it("should send ping request", async () => {
const pingPromise = broker.ping()
// We need to wait a tick for the request to be created and registered
await vi.runAllTimersAsync()
// Make sure we started the handshake with the remote signer
const sentHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Send,
)[1]
// the sub was sent
sentHandler()
let req = {} as Nip46Request
// catch up the send event to get the request id
broker.sender.on(Nip46Event.Send, (res: Nip46Request) => (req = res))
await vi.runAllTimersAsync()
// The receiver should emit a response with the same ID as the request
broker.receiver.emit(Nip46Event.Receive, {
id: req?.id,
result: "pong",
error: undefined,
event: {} as TrustedEvent,
url: "wss://test.relay",
})
const result = await pingPromise
expect(result).toBe("pong")
})
it("should get public key", async () => {
const pubkeyPromise = broker.getPublicKey()
await vi.runAllTimersAsync()
// Make sure we started the handshake with the remote signer
const sentHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Send,
)[1]
// the sub handshake was sent
sentHandler()
let req = {} as Nip46Request
// catch up the send event to get the request id
broker.sender.on(Nip46Event.Send, (res: Nip46Request) => (req = res))
await vi.runAllTimersAsync()
expect(req.method).toBe("get_public_key")
// Simulate response
broker.receiver.emit(Nip46Event.Receive, {
id: req.id,
result: "test-pubkey",
error: undefined,
event: {} as TrustedEvent,
url: "wss://test.relay",
})
const result = await pubkeyPromise
expect(result).toBe("test-pubkey")
})
it("should sign event", async () => {
const event = {kind: 1, pubkey, created_at: now(), content: "test", tags: []}
const signedEvent = {...event, sig: "signature"}
const signPromise = broker.signEvent(event)
await vi.runAllTimersAsync()
// Make sure we started the handshake with the remote signer
const sentHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Send,
)[1]
// the sub handshake was sent
sentHandler()
let req = {} as Nip46Request
// catch up the request send event to get the request id
broker.sender.on(Nip46Event.Send, (res: Nip46Request) => (req = res))
await vi.runAllTimersAsync()
// Simulate response
broker.receiver.emit(Nip46Event.Receive, {id: req.id, result: JSON.stringify(signedEvent)})
const result = await signPromise
expect(result).toEqual(signedEvent)
})
it("should handle encryption methods", async () => {
const encryptPromise = broker.nip04Encrypt("bb".repeat(32), "message")
await vi.runAllTimersAsync()
// Make sure we started the handshake with the remote signer
const sentHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Send,
)[1]
// the sub handshake was sent
sentHandler()
let req = {} as Nip46Request
// catch up the request send event to get the request id
broker.sender.on(Nip46Event.Send, (res: Nip46Request) => (req = res))
await vi.runAllTimersAsync()
// Simulate response
broker.receiver.emit(Nip46Event.Receive, {id: req.id, result: "encrypted"})
const result = await encryptPromise
expect(result).toBe("encrypted")
})
})
describe("error handling", () => {
it("should handle request timeout", async () => {
// const broker = new Nip46Broker({
// ...defaultParams,
// timeout: 100,
// })
// const pingPromise = broker.ping()
// await expect(pingPromise).rejects.toThrow()
})
it("should handle request errors", async () => {
const broker = new Nip46Broker(defaultParams)
const pingPromise = broker.ping()
// We need to wait a tick for the request to be created and registered
await vi.runAllTimersAsync()
// Make sure we started the handshake with the remote signer
const sentHandler = (mockSubscription as any).on.mock.calls.find(
call => call[0] === SubscriptionEvent.Send,
)[1]
// the sub was sent
sentHandler()
let req = {} as Nip46Request
// catch up the send event to get the request id
broker.sender.on(Nip46Event.Send, (res: Nip46Request) => (req = res))
await vi.runAllTimersAsync()
// The receiver should emit a response with the same ID as the request
broker.receiver.emit(Nip46Event.Receive, {
id: req?.id,
result: "",
error: "test error",
event: {} as TrustedEvent,
url: "wss://test.relay",
})
await expect(pingPromise).rejects.toMatchObject({error: "test error"})
})
})
describe("state management", () => {
it("should update params correctly", () => {
const broker = new Nip46Broker(defaultParams)
const newParams = {
signerPubkey: "new-pubkey",
algorithm: "nip04" as const,
}
broker.setParams(newParams)
expect(broker.params).toEqual({
...defaultParams,
...newParams,
})
})
it("should cleanup on teardown", () => {
const broker = new Nip46Broker(defaultParams)
const senderStopSpy = vi.spyOn(broker.sender, "stop")
const receiverStopSpy = vi.spyOn(broker.receiver, "stop")
broker.teardown()
expect(senderStopSpy).toHaveBeenCalled()
expect(receiverStopSpy).toHaveBeenCalled()
})
})
})
describe("Nip46Broker", () => {
// Test broker-specific functionality
it("should parse bunker URL correctly", () => {
const url = `bunker://${signerPubkey}?relay=wss://relay1&relay=wss://relay2&secret=123`
const result = Nip46Broker.parseBunkerUrl(url)
expect(result).toEqual({
signerPubkey,
relays: ["wss://relay1/", "wss://relay2/"],
connectSecret: "123",
})
})
})
})