import { afterEach, describe, expect, test, vi } from "vitest"; import type { HandleResolver } from "@atcute/identity-resolver"; import { FallbackHandleResolver, handleResolverFor, handleResolverUrls } from "#lib/auth/handleResolver.ts"; const answering = (did: string): HandleResolver => ({ resolve: vi.fn(async () => did as `did:plc:${string}`) }); const refusing = (message: string): HandleResolver => ({ resolve: vi.fn(async () => { throw new Error(message); }) }); describe("handleResolverUrls", () => { test.each([ [undefined, []], ["", []], ["https://pds.test/", ["https://pds.test"]], ["https://tranquil.test, https://pds.test/", ["https://tranquil.test", "https://pds.test"]] ])("reads %j", (raw, expected) => { expect(handleResolverUrls(raw)).toEqual(expected); }); }); describe("handleResolverFor without urls", () => { const bobbinUrl = () => "https://bobbin.test"; afterEach(() => vi.unstubAllGlobals()); const network = (routes: Record Response>) => vi.stubGlobal( "fetch", vi.fn(async (input: URL) => { const answer = routes[input.hostname]; if (!answer) throw new TypeError(`unexpected fetch of ${input}`); return answer(); }) ); const notFound = () => new Response("", { status: 404 }); const dns = (Status: number, answers: string[] = []) => () => Response.json({ Status, TC: false, RD: true, RA: true, AD: false, CD: false, Question: [{ name: "_atproto.x", type: 16 }], Answer: answers.map((data) => ({ name: "_atproto.x", type: 16, TTL: 60, data })) }); test("asks the handle's own host for a handle with no dns record", async () => { network({ "cloudflare-dns.com": dns(3), "new.tngl.sh": () => new Response("did:plc:abcdefghijklmnopqrstuvwx\n") }); expect(await handleResolverFor([], bobbinUrl).resolve("new.tngl.sh")).toBe( "did:plc:abcdefghijklmnopqrstuvwx" ); }); test("reads the dns record when the handle's host answers nothing", async () => { network({ "cloudflare-dns.com": dns(0, ["did=did:plc:abcdefghijklmnopqrstuvwx"]), "example.com": notFound }); expect(await handleResolverFor([], bobbinUrl).resolve("example.com")).toBe( "did:plc:abcdefghijklmnopqrstuvwx" ); }); test("asks bobbin when the handle's host hides its answer behind cors", async () => { network({ "cloudflare-dns.com": dns(3), "crashkeys.dev": () => { throw new TypeError("Failed to fetch"); }, "bobbin.test": () => Response.json({ did: "did:plc:abcdefghijklmnopqrstuvwx", handle: "crashkeys.dev", pds: "https://pds.crashkeys.dev" }) }); expect(await handleResolverFor([], bobbinUrl).resolve("crashkeys.dev")).toBe( "did:plc:abcdefghijklmnopqrstuvwx" ); }); test("fails when nobody answers", async () => { network({ "cloudflare-dns.com": dns(3), "nobody.test": notFound, "bobbin.test": () => Response.json({ error: "UpstreamFailed" }, { status: 502 }) }); await expect(handleResolverFor([], bobbinUrl).resolve("nobody.test")).rejects.toThrow(); }); }); describe("FallbackHandleResolver", () => { test("takes the first answer without asking the rest", async () => { const later = answering("did:plc:later"); const resolver = new FallbackHandleResolver([answering("did:plc:first"), later]); expect(await resolver.resolve("org.tranquil.test")).toBe("did:plc:first"); expect(later.resolve).not.toHaveBeenCalled(); }); test("falls back when an earlier resolver can't resolve the handle", async () => { const resolver = new FallbackHandleResolver([ refusing("HandleNotFound"), answering("did:plc:alice") ]); expect(await resolver.resolve("alice.pds.test")).toBe("did:plc:alice"); }); test("throws the last failure when nobody can resolve it", async () => { const resolver = new FallbackHandleResolver([refusing("first"), refusing("last")]); await expect(resolver.resolve("nobody.test")).rejects.toThrow("last"); }); test("stops on an aborted signal", async () => { const controller = new AbortController(); controller.abort(); const later = answering("did:plc:later"); const resolver = new FallbackHandleResolver([refusing("aborted"), later]); await expect( resolver.resolve("org.tranquil.test", { signal: controller.signal }) ).rejects.toThrow("aborted"); expect(later.resolve).not.toHaveBeenCalled(); }); });