Files
welshman/packages/app/__tests__/router.test.ts
T
2025-02-28 14:18:02 -08:00

297 lines
8.3 KiB
TypeScript

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)
})
})
})