diff --git a/src/app/__tests__/not-found.test.tsx b/src/app/__tests__/not-found.test.tsx index e7ad2793..eb32f9c3 100644 --- a/src/app/__tests__/not-found.test.tsx +++ b/src/app/__tests__/not-found.test.tsx @@ -7,15 +7,17 @@ import { render, screen } from "@testing-library/react" * not "Profile not found" (Greptile on #267). */ -const { pathname, lookupActor, lookupRecord } = vi.hoisted(() => ({ +const { pathname, lookupActor, lookupRecord, resolveHandle } = vi.hoisted(() => ({ pathname: { value: "" }, lookupActor: vi.fn(), lookupRecord: vi.fn(), + resolveHandle: vi.fn(), })) vi.mock("next/headers", () => ({ headers: async () => new Headers({ "x-pathname": pathname.value }), })) vi.mock("@/lib/og-metadata", () => ({ lookupActor, lookupRecord })) +vi.mock("@/lib/atproto/did", () => ({ resolveHandle })) import NotFoundPage from "../not-found" @@ -28,6 +30,7 @@ async function renderAt(path: string) { beforeEach(() => { lookupActor.mockReset().mockResolvedValue({ status: "found", did: "did:plc:alice", handle: "alice.example.com" }) lookupRecord.mockReset().mockResolvedValue({ uri: "", value: {} }) + resolveHandle.mockReset().mockResolvedValue(null) }) describe("not-found page", () => { @@ -49,6 +52,24 @@ describe("not-found page", () => { ) }) + it("a DID-addressed missing record links the profile by its handle", async () => { + lookupActor.mockResolvedValue({ status: "found", did: "did:plc:alice", handle: null }) + lookupRecord.mockResolvedValue("missing") + resolveHandle.mockResolvedValue("alice.example.com") + await renderAt("/did:plc:alice/activity/3abc") + expect(screen.getByRole("link", { name: "View @alice.example.com" }).getAttribute("href")).toBe( + "/alice.example.com", + ) + }) + + it("falls back to the DID when the handle can't be resolved", async () => { + lookupActor.mockResolvedValue({ status: "found", did: "did:plc:alice", handle: null }) + lookupRecord.mockResolvedValue("missing") + resolveHandle.mockRejectedValue(new Error("down")) + await renderAt("/did:plc:alice/activity/3abc") + expect(screen.getByRole("link", { name: "View @did:plc:alice" })).toBeTruthy() + }) + it("a missing record's edit page is a missing record too", async () => { lookupRecord.mockResolvedValue("missing") expect(await renderAt("/alice.example.com/activity/3abc/edit")).toBe("Activity not found") diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index a84c05c2..0b735c3f 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -3,6 +3,7 @@ import { FileQuestion, UserX } from "lucide-react"; import Button from "@/components/ui/button"; import EmptyState from "@/components/ui/empty-state"; import NotFoundMarker from "@/components/layout/not-found-marker"; +import { resolveHandle } from "@/lib/atproto/did"; import { lookupActor, lookupRecord } from "@/lib/og-metadata"; import { collectionForType, @@ -45,7 +46,12 @@ async function whatIsMissing(pathname: string | null): Promise { const [, type, rkey] = segments; if (segments.length >= 3 && isRecordType(type) && rkey) { const record = await lookupRecord(resolved.did, collectionForType(type), rkey); - if (record === "missing") return { kind: "record", actor: actor.value, type }; + if (record === "missing") { + // A DID-addressed link still names the profile by its handle. + const handle = + resolved.handle ?? (await resolveHandle(resolved.did).catch(() => null)); + return { kind: "record", actor: handle ?? actor.value, type }; + } } return null; }