UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

420 lines (419 loc) 13.9 kB
import "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as esm_default } from "../esm-DVILvP5e.mjs"; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import "../std__assert-CRDpx_HF.mjs"; import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs"; import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs"; import { t as assertNotEquals } from "../assert_not_equals--wG9hV7u.mjs"; import { t as assert } from "../assert-DikXweDx.mjs"; import { l as verifyRequest } from "../http-C_edJspG.mjs"; import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs"; import { t as doesActorOwnKey } from "../owner-DRHNR5YO.mjs"; import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-C7tim5U9.mjs"; import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab"; //#region src/federation/send.test.ts test("extractInboxes()", () => { const recipients = [ new Person({ id: new URL("https://example.com/alice"), inbox: new URL("https://example.com/alice/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox") }) }), new Application({ id: new URL("https://example.com/app"), inbox: new URL("https://example.com/app/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox") }) }), new Group({ id: new URL("https://example.org/group"), inbox: new URL("https://example.org/group/inbox") }), new Service({ id: new URL("https://example.net/service"), inbox: new URL("https://example.net/service/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.net/inbox") }) }) ]; let inboxes = extractInboxes({ recipients }); assertEquals(inboxes, { "https://example.com/alice/inbox": { actorIds: new Set(["https://example.com/alice"]), sharedInbox: false }, "https://example.com/app/inbox": { actorIds: new Set(["https://example.com/app"]), sharedInbox: false }, "https://example.org/group/inbox": { actorIds: new Set(["https://example.org/group"]), sharedInbox: false }, "https://example.net/service/inbox": { actorIds: new Set(["https://example.net/service"]), sharedInbox: false } }); inboxes = extractInboxes({ recipients, preferSharedInbox: true }); assertEquals(inboxes, { "https://example.com/inbox": { actorIds: new Set(["https://example.com/alice", "https://example.com/app"]), sharedInbox: true }, "https://example.org/group/inbox": { actorIds: new Set(["https://example.org/group"]), sharedInbox: false }, "https://example.net/inbox": { actorIds: new Set(["https://example.net/service"]), sharedInbox: true } }); inboxes = extractInboxes({ recipients, excludeBaseUris: [new URL("https://foo.bar/")] }); assertEquals(inboxes, { "https://example.com/alice/inbox": { actorIds: new Set(["https://example.com/alice"]), sharedInbox: false }, "https://example.com/app/inbox": { actorIds: new Set(["https://example.com/app"]), sharedInbox: false }, "https://example.org/group/inbox": { actorIds: new Set(["https://example.org/group"]), sharedInbox: false }, "https://example.net/service/inbox": { actorIds: new Set(["https://example.net/service"]), sharedInbox: false } }); inboxes = extractInboxes({ recipients, excludeBaseUris: [new URL("https://example.com/")] }); assertEquals(inboxes, { "https://example.org/group/inbox": { actorIds: new Set(["https://example.org/group"]), sharedInbox: false }, "https://example.net/service/inbox": { actorIds: new Set(["https://example.net/service"]), sharedInbox: false } }); inboxes = extractInboxes({ recipients, preferSharedInbox: true, excludeBaseUris: [new URL("https://example.com/")] }); assertEquals(inboxes, { "https://example.org/group/inbox": { actorIds: new Set(["https://example.org/group"]), sharedInbox: false }, "https://example.net/inbox": { actorIds: new Set(["https://example.net/service"]), sharedInbox: true } }); }); test("sendActivity()", async (t) => { esm_default.spyGlobal(); let httpSigVerified = null; let request = null; esm_default.post("https://example.com/inbox", async (cl) => { httpSigVerified = false; request = cl.request.clone(); const options = { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }; const key = await verifyRequest(request, options); const activity = await Activity.fromJsonLd(await request.json(), options); if (key != null && await doesActorOwnKey(activity, key, options)) httpSigVerified = true; if (httpSigVerified) return new Response("", { status: 202 }); return new Response("", { status: 401 }); }); await t.step("success", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; await sendActivity({ activity, keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox"), headers: new Headers({ "X-Test": "test" }) }); assert(httpSigVerified); assertNotEquals(request, null); assertEquals(request?.method, "POST"); assertEquals(request?.url, "https://example.com/inbox"); assertEquals(request?.headers.get("Content-Type"), "application/activity+json"); assertEquals(request?.headers.get("X-Test"), "test"); httpSigVerified = null; await assertRejects(() => sendActivity({ activity: { ...activity, actor: "https://example.com/person2" }, keys: [{ privateKey: ed25519PrivateKey, keyId: ed25519Multikey.id }], inbox: new URL("https://example.com/inbox") })); assertFalse(httpSigVerified); assertNotEquals(request, null); assertEquals(request?.method, "POST"); assertEquals(request?.url, "https://example.com/inbox"); assertEquals(request?.headers.get("Content-Type"), "application/activity+json"); }); esm_default.post("https://example.com/inbox2", { status: 500, body: "something went wrong" }); await t.step("failure", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; await assertRejects(() => sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox2") }), Error, "Failed to send activity https://example.com/activity to https://example.com/inbox2 (500 Internal Server Error):\nsomething went wrong"); }); await t.step("failure throws SendActivityError", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox2") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.statusCode, 500); assertEquals(e.inbox, new URL("https://example.com/inbox2")); assertEquals(e.responseBody, "something went wrong"); } }); esm_default.post("https://example.com/inbox-gone", { status: 410, body: "Gone" }); await t.step("410 Gone throws SendActivityError", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox-gone") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.statusCode, 410); assertEquals(e.inbox, new URL("https://example.com/inbox-gone")); assertEquals(e.responseBody, "Gone"); } }); esm_default.post("https://example.com/inbox-notfound", { status: 404, body: "Not Found" }); await t.step("404 Not Found throws SendActivityError", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox-notfound") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.statusCode, 404); assertEquals(e.inbox, new URL("https://example.com/inbox-notfound")); assertEquals(e.responseBody, "Not Found"); } }); const longErrorBody = "x".repeat(1500); esm_default.post("https://example.com/inbox-long-error", { status: 500, body: longErrorBody }); await t.step("long error response body is truncated", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox-long-error") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.statusCode, 500); assertEquals(e.inbox, new URL("https://example.com/inbox-long-error")); const expectedTruncated = "x".repeat(1024) + "… (truncated)"; assertEquals(e.responseBody, expectedTruncated); assertEquals(e.message.includes("… (truncated)"), true); } }); await t.step("short error response body is not truncated", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox2") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.responseBody, "something went wrong"); assertEquals(e.message.includes("… (truncated)"), false); } }); const exactLimitBody = "y".repeat(1024); esm_default.post("https://example.com/inbox-exact-limit", { status: 500, body: exactLimitBody }); await t.step("error response body exactly at limit is not truncated", async () => { const activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://example.com/activity", "actor": "https://example.com/person" }; try { await sendActivity({ activity, activityId: "https://example.com/activity", keys: [{ privateKey: rsaPrivateKey2, keyId: rsaPublicKey2.id }], inbox: new URL("https://example.com/inbox-exact-limit") }); assert(false, "Should have thrown"); } catch (e) { assertInstanceOf(e, SendActivityError); assertEquals(e.responseBody, exactLimitBody); assertEquals(e.message.includes("… (truncated)"), false); } }); esm_default.hardReset(); }); test("sendActivity() records OpenTelemetry span events", async (t) => { const [tracerProvider, exporter] = createTestTracerProvider(); esm_default.spyGlobal(); await t.step("successful send", async () => { esm_default.get("https://example.com/", { status: 404 }); esm_default.post("https://example.com/inbox", { status: 202 }); await sendActivity({ activity: { "@context": "https://www.w3.org/ns/activitystreams", type: "Create", id: "https://example.com/activity", actor: "https://example.com/person" }, activityId: "https://example.com/activity", activityType: "https://www.w3.org/ns/activitystreams#Create", keys: [{ keyId: new URL("https://example.com/person#key"), privateKey: rsaPrivateKey2 }], inbox: new URL("https://example.com/inbox"), tracerProvider }); const spans = exporter.getSpans("activitypub.send_activity"); assertEquals(spans.length, 1); const span = spans[0]; assertEquals(span.attributes["activitypub.activity.id"], "https://example.com/activity"); assertEquals(span.attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create"); const events = exporter.getEvents("activitypub.send_activity", "activitypub.activity.sent"); assertEquals(events.length, 1); const event = events[0]; assert(event.attributes != null); assertEquals(event.attributes["activitypub.inbox.url"], "https://example.com/inbox"); assertEquals(event.attributes["activitypub.activity.id"], "https://example.com/activity"); assert(typeof event.attributes["activitypub.activity.json"] === "string"); const recordedActivity = JSON.parse(event.attributes["activitypub.activity.json"]); assertEquals(recordedActivity.id, "https://example.com/activity"); assertEquals(recordedActivity.type, "Create"); exporter.clear(); esm_default.hardReset(); }); }); //#endregion export {};