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<string, () => 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();
});
});