@fedify/fedify
Version:
An ActivityPub server framework
420 lines (419 loc) • 13.9 kB
JavaScript
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 {};