@fedify/fedify
Version:
An ActivityPub server framework
537 lines (536 loc) • 16.4 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { r as createRequestContext } from "../context-Dk_tacqz.mjs";
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
import "../std__assert-CRDpx_HF.mjs";
import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
import { o as createFederation, s as handleWebFinger } from "../middleware-D9k0Knum.mjs";
import { test } from "@fedify/fixture";
import { Image, Link, Person, Tombstone } from "@fedify/vocab";
//#region src/federation/webfinger.test.ts
test("handleWebFinger()", async (t) => {
const url = new URL("https://example.com/.well-known/webfinger");
function createContext(url) {
const context = createRequestContext({
federation: createFederation({ kv: new MemoryKvStore() }),
url,
data: void 0,
getActorUri(identifier) {
return new URL(`${url.origin}/users/${identifier}`);
},
async getActor(handle) {
const actor = await actorDispatcher(context, handle);
return actor instanceof Tombstone ? null : actor;
},
parseUri(uri) {
if (uri == null) return null;
if (uri.protocol === "acct:") return null;
if (!uri.pathname.startsWith("/users/")) return null;
const paths = uri.pathname.split("/");
return {
type: "actor",
identifier: paths[paths.length - 1]
};
}
});
return context;
}
const actorDispatcher = (ctx, identifier) => {
if (identifier === "gone") return new Tombstone({
id: ctx.getActorUri(identifier),
deleted: Temporal.Instant.from("2024-01-15T00:00:00Z")
});
if (identifier !== "someone" && identifier !== "someone2") return null;
const actorUri = ctx.getActorUri(identifier);
return new Person({
id: actorUri,
name: identifier === "someone" ? "Someone" : "Someone 2",
preferredUsername: identifier === "someone" ? null : identifier === "someone2" ? "bar" : null,
icon: new Image({
url: new URL(`${actorUri.origin}/icon.jpg`),
mediaType: "image/jpeg"
}),
urls: [new URL(`${actorUri.origin}/{identifier}`), new Link({
href: new URL(`${actorUri.origin}/{identifier}`),
rel: "alternate",
mediaType: "text/html"
})]
});
};
let onNotFoundCalled = null;
const onNotFound = (request) => {
onNotFoundCalled = request;
return new Response("Not found", { status: 404 });
};
await t.step("no actor dispatcher", async () => {
const context = createContext(url);
const request = context.request;
assertEquals((await handleWebFinger(request, {
context,
onNotFound
})).status, 404);
assertEquals(onNotFoundCalled, request);
});
onNotFoundCalled = null;
await t.step("no resource", async () => {
const context = createContext(url);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 400);
assertEquals(await response.text(), "Missing resource parameter.");
assertEquals(onNotFoundCalled, null);
});
await t.step("invalid resource", async () => {
const u = new URL(url);
u.searchParams.set("resource", " invalid ");
const context = createContext(u);
const response = await handleWebFinger(new Request(u), {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 400);
assertEquals(await response.text(), "Invalid resource URL.");
assertEquals(onNotFoundCalled, null);
});
const expected = {
subject: "acct:someone@example.com",
aliases: ["https://example.com/users/someone"],
links: [
{
href: "https://example.com/users/someone",
rel: "self",
type: "application/activity+json"
},
{
href: "https://example.com/@someone",
rel: "http://webfinger.net/rel/profile-page"
},
{
href: "https://example.com/@someone",
rel: "alternate",
type: "text/html"
},
{
href: "https://example.com/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg"
}
]
};
await t.step("ok: resource=acct:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:someone@example.com");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(response.headers.get("Content-Type"), "application/jrd+json");
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
assertEquals(await response.json(), expected);
});
const expected2 = {
subject: "https://example.com/users/someone2",
aliases: ["acct:bar@example.com"],
links: [
{
href: "https://example.com/users/someone2",
rel: "self",
type: "application/activity+json"
},
{
href: "https://example.com/@someone2",
rel: "http://webfinger.net/rel/profile-page"
},
{
href: "https://example.com/@someone2",
rel: "alternate",
type: "text/html"
},
{
href: "https://example.com/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg"
}
]
};
await t.step("ok: resource=https:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "https://example.com/users/someone");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: [],
subject: "https://example.com/users/someone"
});
u.searchParams.set("resource", "https://example.com/users/someone2");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), expected2);
});
await t.step("gone: resource=acct:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:gone@example.com");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 410);
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
assertEquals(onNotFoundCalled, null);
});
await t.step("gone: resource=https:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "https://example.com/users/gone");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 410);
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
assertEquals(onNotFoundCalled, null);
});
await t.step("not found: resource=acct:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:no-one@example.com");
const context = createContext(u);
const request = context.request;
assertEquals((await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
})).status, 404);
assertEquals(onNotFoundCalled, request);
});
onNotFoundCalled = null;
await t.step("not found: resource=http:...", async () => {
const u = new URL(url);
u.searchParams.set("resource", "https://example.com/users/no-one");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 404);
assertEquals(onNotFoundCalled, request);
onNotFoundCalled = null;
u.searchParams.set("resource", "https://google.com/");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 404);
assertEquals(onNotFoundCalled, request);
});
onNotFoundCalled = null;
const actorHandleMapper = (_ctx, username) => {
return username === "foo" ? "someone" : username === "bar" ? "someone2" : null;
};
await t.step("handle mapper", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:foo@example.com");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: ["https://example.com/users/someone"],
subject: "acct:foo@example.com"
});
u.searchParams.set("resource", "acct:bar@example.com");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected2,
aliases: ["https://example.com/users/someone2"],
subject: "acct:bar@example.com"
});
u.searchParams.set("resource", "https://example.com/users/someone");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: [],
subject: "https://example.com/users/someone"
});
u.searchParams.set("resource", "acct:baz@example.com");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
onNotFound
});
assertEquals(response.status, 404);
});
const actorAliasMapper = (_ctx, resource) => {
if (resource.protocol !== "https:") return null;
if (resource.host !== "example.com") return null;
const m = /^\/@(\w+)$/.exec(resource.pathname);
if (m == null) return null;
return { username: m[1] };
};
await t.step("alias mapper", async () => {
const u = new URL(url);
u.searchParams.set("resource", "https://example.com/@someone");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
actorDispatcher,
actorAliasMapper,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: ["https://example.com/users/someone"],
subject: "https://example.com/@someone"
});
u.searchParams.set("resource", "https://example.com/@bar");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
actorAliasMapper,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected2,
aliases: ["acct:bar@example.com", "https://example.com/users/someone2"],
subject: "https://example.com/@bar"
});
u.searchParams.set("resource", "https://example.com/@no-one");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorAliasMapper,
onNotFound
});
assertEquals(response.status, 404);
u.searchParams.set("resource", "https://example.com/@no-one");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
actorDispatcher,
actorHandleMapper,
actorAliasMapper,
onNotFound
});
assertEquals(response.status, 404);
});
await t.step("handleHost", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:someone@example.com");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
host: "handle.example.com",
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: [...expected.aliases, "acct:someone@handle.example.com"]
});
u.searchParams.set("resource", "acct:someone@handle.example.com");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
host: "handle.example.com",
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
subject: "acct:someone@handle.example.com"
});
u.searchParams.set("resource", "https://example.com/users/someone2");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
host: "handle.example.com",
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected2,
aliases: ["acct:bar@handle.example.com", "acct:bar@example.com"],
subject: "https://example.com/users/someone2"
});
});
const expectedForLocalhostWithPort = {
subject: "acct:someone@localhost:8000",
aliases: ["https://localhost:8000/users/someone"],
links: [
{
href: "https://localhost:8000/users/someone",
rel: "self",
type: "application/activity+json"
},
{
href: "https://localhost:8000/@someone",
rel: "http://webfinger.net/rel/profile-page"
},
{
href: "https://localhost:8000/@someone",
rel: "alternate",
type: "text/html"
},
{
href: "https://localhost:8000/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg"
}
]
};
await t.step("on localhost with port, ok: resource=acct:...", async () => {
const u = new URL("https://localhost:8000/.well-known/webfinger");
u.searchParams.set("resource", "acct:someone@localhost:8000");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(response.headers.get("Content-Type"), "application/jrd+json");
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
assertEquals(await response.json(), expectedForLocalhostWithPort);
});
const expectedForHostnameWithPort = {
subject: "acct:someone@example.com:8000",
aliases: ["http://example.com:8000/users/someone"],
links: [
{
href: "http://example.com:8000/users/someone",
rel: "self",
type: "application/activity+json"
},
{
href: "http://example.com:8000/@someone",
rel: "http://webfinger.net/rel/profile-page"
},
{
href: "http://example.com:8000/@someone",
rel: "alternate",
type: "text/html"
},
{
href: "http://example.com:8000/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg"
}
]
};
await t.step("on hostname with port, ok: resource=acct:...", async () => {
const u = new URL("http://example.com:8000/.well-known/webfinger");
u.searchParams.set("resource", "acct:someone@example.com:8000");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(response.headers.get("Content-Type"), "application/jrd+json");
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
assertEquals(await response.json(), expectedForHostnameWithPort);
});
await t.step("webFingerLinksDispatcher", async () => {
const webFingerLinksDispatcher = (_ctx) => {
return [{
rel: "http://ostatus.org/schema/1.0/subscribe",
template: "https://example.com/follow?acct={uri}"
}];
};
const u = new URL(url);
u.searchParams.set("resource", "acct:someone@example.com");
const context = createContext(u);
const request = context.request;
const response = await handleWebFinger(request, {
context,
actorDispatcher,
webFingerLinksDispatcher,
onNotFound
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
links: [...expected.links, {
rel: "http://ostatus.org/schema/1.0/subscribe",
template: "https://example.com/follow?acct={uri}"
}]
});
});
});
//#endregion
export {};