@fedify/fedify
Version:
An ActivityPub server framework
499 lines (495 loc) • 17.6 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { assertEquals } from "../assert_equals-CTYbeopb.js";
import "../assert-DmFG7ppO.js";
import "../assert_instance_of-CF09JHYM.js";
import { MemoryKvStore } from "../kv-BMY6Qf_A.js";
import { FetchError, contexts_default, deno_default, getDocumentLoader, getUserAgent, kvCache } from "../docloader-C8mmLN-Y.js";
import { UrlError } from "../url-kTAI6_KP.js";
import { test } from "../testing-BZ0dJ4qn.js";
import "../std__assert-vp0TKMS1.js";
import { assertRejects } from "../assert_rejects-C-sxEMM5.js";
import "../assert_is_error-nrwA1GeT.js";
import "../assert_not_equals-Dc7y-V5Q.js";
import { assertThrows } from "../assert_throws-Cn9C6Jur.js";
import { mockDocumentLoader } from "../docloader-09nVWLAZ.js";
import { esm_default } from "../esm-Db4De7AS.js";
import process from "node:process";
//#region runtime/docloader.test.ts
test("new FetchError()", () => {
const e = new FetchError("https://example.com/", "An error message.");
assertEquals(e.name, "FetchError");
assertEquals(e.url, new URL("https://example.com/"));
assertEquals(e.message, "https://example.com/: An error message.");
const e2 = new FetchError(new URL("https://example.org/"));
assertEquals(e2.url, new URL("https://example.org/"));
assertEquals(e2.message, "https://example.org/");
});
test("getDocumentLoader()", async (t) => {
const fetchDocumentLoader = getDocumentLoader();
esm_default.spyGlobal();
esm_default.get("https://example.com/object", { body: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
} });
await t.step("ok", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/object"), {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
esm_default.get("https://example.com/link-ctx", {
body: {
id: "https://example.com/link-ctx",
name: "Fetched object",
type: "Object"
},
headers: {
"Content-Type": "application/activity+json",
Link: "<https://www.w3.org/ns/activitystreams>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\""
}
});
esm_default.get("https://example.com/link-obj", { headers: {
"Content-Type": "text/html; charset=utf-8",
Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/activity+json\""
} });
esm_default.get("https://example.com/link-obj-relative", { headers: {
"Content-Type": "text/html; charset=utf-8",
Link: "</object>; rel=\"alternate\"; type=\"application/activity+json\""
} });
esm_default.get("https://example.com/obj-w-wrong-link", {
body: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/obj-w-wrong-link",
name: "Fetched object",
type: "Object"
},
headers: {
"Content-Type": "text/html; charset=utf-8",
Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
}
});
await t.step("Link header", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/link-ctx"), {
contextUrl: "https://www.w3.org/ns/activitystreams",
documentUrl: "https://example.com/link-ctx",
document: {
id: "https://example.com/link-ctx",
name: "Fetched object",
type: "Object"
}
});
assertEquals(await fetchDocumentLoader("https://example.com/link-obj"), {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
await t.step("Link header relative url", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/link-ctx"), {
contextUrl: "https://www.w3.org/ns/activitystreams",
documentUrl: "https://example.com/link-ctx",
document: {
id: "https://example.com/link-ctx",
name: "Fetched object",
type: "Object"
}
});
assertEquals(await fetchDocumentLoader("https://example.com/link-obj-relative"), {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
await t.step("wrong Link header syntax", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
contextUrl: null,
documentUrl: "https://example.com/obj-w-wrong-link",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/obj-w-wrong-link",
name: "Fetched object",
type: "Object"
}
});
});
esm_default.get("https://example.com/html-link", {
body: `<html>
<head>
<meta charset=utf-8>
<link
rel=alternate
type='application/activity+json'
href="https://example.com/object">
</head>
</html>`,
headers: { "Content-Type": "text/html; charset=utf-8" }
});
await t.step("HTML <link>", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/html-link"), {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
esm_default.get("https://example.com/html-a", {
body: `<html>
<head>
<meta charset=utf-8>
</head>
<body>
<a
rel=alternate
type=application/activity+json
href=https://example.com/object>test</a>
</body>
</html>`,
headers: { "Content-Type": "text/html; charset=utf-8" }
});
await t.step("HTML <a>", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/html-a"), {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
esm_default.get("https://example.com/wrong-content-type", {
body: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/wrong-content-type",
name: "Fetched object",
type: "Object"
},
headers: { "Content-Type": "text/html; charset=utf-8" }
});
await t.step("Wrong Content-Type", async () => {
assertEquals(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
contextUrl: null,
documentUrl: "https://example.com/wrong-content-type",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/wrong-content-type",
name: "Fetched object",
type: "Object"
}
});
});
esm_default.get("https://example.com/404", { status: 404 });
await t.step("not ok", async () => {
await assertRejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
});
await t.step("preloaded contexts", async () => {
for (const [url, document] of Object.entries(contexts_default)) assertEquals(await fetchDocumentLoader(url), {
contextUrl: null,
documentUrl: url,
document
});
});
await t.step("deny non-HTTP/HTTPS", async () => {
await assertRejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
});
esm_default.get("https://example.com/localhost-redirect", {
status: 302,
headers: { Location: "https://localhost/object" }
});
esm_default.get("https://example.com/localhost-link", {
body: `<html>
<head>
<meta charset=utf-8>
<link
rel=alternate
type='application/activity+json'
href="https://localhost/object">
</head>
</html>`,
headers: { "Content-Type": "text/html; charset=utf-8" }
});
esm_default.get("https://localhost/object", { body: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://localhost/object",
name: "Fetched object",
type: "Object"
} });
await t.step("allowPrivateAddress: false", async () => {
await assertRejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
await assertRejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
await assertRejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
});
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
await t.step("allowPrivateAddress: true", async () => {
const expected = {
contextUrl: null,
documentUrl: "https://localhost/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://localhost/object",
name: "Fetched object",
type: "Object"
}
};
assertEquals(await fetchDocumentLoader2("https://localhost/object"), expected);
assertEquals(await fetchDocumentLoader2("https://example.com/localhost-redirect"), expected);
assertEquals(await fetchDocumentLoader2("https://example.com/localhost-link"), expected);
});
esm_default.hardReset();
});
test("kvCache()", async (t) => {
const kv = new MemoryKvStore();
await t.step("cached", async () => {
const loader = kvCache({
kv,
loader: mockDocumentLoader,
rules: [
["https://example.org/", Temporal.Duration.from({ days: 1 })],
[new URL("https://example.net/"), Temporal.Duration.from({ days: 1 })],
[new URLPattern("https://example.com/*"), Temporal.Duration.from({ days: 30 })]
],
prefix: ["_test", "cached"]
});
const result = await loader("https://example.com/object");
assertEquals(result, {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
const cache = await kv.get([
"_test",
"cached",
"https://example.com/object"
]);
assertEquals(cache, result);
await kv.set([
"_test",
"cached",
"https://example.org/"
], {
contextUrl: null,
documentUrl: "https://example.org/",
document: { "id": "https://example.org/" }
});
const result2 = await loader("https://example.org/");
assertEquals(result2, {
contextUrl: null,
documentUrl: "https://example.org/",
document: { "id": "https://example.org/" }
});
await kv.set([
"_test",
"cached",
"https://example.net/"
], {
contextUrl: null,
documentUrl: "https://example.net/",
document: { "id": "https://example.net/" }
});
const result3 = await loader("https://example.net/");
assertEquals(result3, {
contextUrl: null,
documentUrl: "https://example.net/",
document: { "id": "https://example.net/" }
});
});
await t.step("not cached", async () => {
const loader = kvCache({
kv,
loader: mockDocumentLoader,
rules: [],
prefix: ["_test", "not cached"]
});
const result = await loader("https://example.com/object");
assertEquals(result, {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
const cache = await kv.get([
"test2",
"not cached",
"https://example.com/object"
]);
assertEquals(cache, void 0);
});
await t.step("maximum cache duration", () => {
assertThrows(() => kvCache({
kv,
loader: mockDocumentLoader,
rules: [["https://example.com/", Temporal.Duration.from({
days: 30,
seconds: 1
})]]
}), TypeError, "The maximum cache duration is 30 days");
assertThrows(() => kvCache({
kv,
loader: mockDocumentLoader,
rules: [[new URLPattern("https://example.com/*"), Temporal.Duration.from({
days: 30,
seconds: 1
})]]
}), TypeError, "The maximum cache duration is 30 days");
});
await t.step("on kv store exception", async () => {
class KvStoreThrowsException {
get(_key) {
throw new Error("Failed to get key");
}
set(_key, _value, _options) {
throw new Error("Failed to set key");
}
delete(_key) {
throw new Error("Failed to delete key");
}
}
const loader = kvCache({
kv: new KvStoreThrowsException(),
loader: mockDocumentLoader,
rules: [
["https://example.org/", Temporal.Duration.from({ days: 1 })],
[new URL("https://example.net/"), Temporal.Duration.from({ days: 1 })],
[new URLPattern("https://example.com/*"), Temporal.Duration.from({ days: 30 })]
],
prefix: ["_test", "not cached"]
});
const result = await loader("https://example.com/object");
assertEquals(result, {
contextUrl: null,
documentUrl: "https://example.com/object",
document: {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/object",
name: "Fetched object",
type: "Object"
}
});
});
await t.step("preloaded contexts bypass cache", async () => {
const kv$1 = new MemoryKvStore();
let loaderCalled = false;
const mockLoader = (url) => {
loaderCalled = true;
return Promise.resolve({
contextUrl: null,
documentUrl: url,
document: { "mock": "document" }
});
};
const loader = kvCache({
kv: kv$1,
loader: mockLoader,
prefix: ["_test", "preloaded"]
});
const activityStreamsUrl = "https://www.w3.org/ns/activitystreams";
loaderCalled = false;
const result = await loader(activityStreamsUrl);
assertEquals(result, {
contextUrl: null,
documentUrl: activityStreamsUrl,
document: contexts_default[activityStreamsUrl]
});
assertEquals(loaderCalled, false, "Loader should not be called for preloaded contexts");
const cachedValue = await kv$1.get([
"_test",
"preloaded",
activityStreamsUrl
]);
assertEquals(cachedValue, void 0, "Preloaded contexts should not be cached in KV store");
const securityUrl = "https://w3id.org/security/v1";
loaderCalled = false;
const result2 = await loader(securityUrl);
assertEquals(result2, {
contextUrl: null,
documentUrl: securityUrl,
document: contexts_default[securityUrl]
});
assertEquals(loaderCalled, false, "Loader should not be called for preloaded contexts");
const nonPreloadedUrl = "https://example.com/not-preloaded";
loaderCalled = false;
const result3 = await loader(nonPreloadedUrl);
assertEquals(result3, {
contextUrl: null,
documentUrl: nonPreloadedUrl,
document: { "mock": "document" }
});
assertEquals(loaderCalled, true, "Loader should be called for non-preloaded URLs");
const cachedValue2 = await kv$1.get([
"_test",
"preloaded",
nonPreloadedUrl
]);
assertEquals(cachedValue2, result3, "Non-preloaded URLs should be cached");
});
});
test("getUserAgent()", () => {
if ("Deno" in globalThis) {
assertEquals(getUserAgent(), `Fedify/${deno_default.version} (Deno/${Deno.version.deno})`);
assertEquals(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Deno/${Deno.version.deno})`);
assertEquals(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Deno/${Deno.version.deno}; +https://example.com/)`);
assertEquals(getUserAgent({
software: "MyApp/1.0.0",
url: new URL("https://example.com/")
}), `MyApp/1.0.0 (Fedify/${deno_default.version}; Deno/${Deno.version.deno}; +https://example.com/)`);
} else if ("Bun" in globalThis) {
assertEquals(getUserAgent(), `Fedify/${deno_default.version} (Bun/${Bun.version})`);
assertEquals(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Bun/${Bun.version})`);
assertEquals(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Bun/${Bun.version}; +https://example.com/)`);
assertEquals(getUserAgent({
software: "MyApp/1.0.0",
url: new URL("https://example.com/")
}), `MyApp/1.0.0 (Fedify/${deno_default.version}; Bun/${Bun.version}; +https://example.com/)`);
} else if (navigator.userAgent === "Cloudflare-Workers") {
assertEquals(getUserAgent(), `Fedify/${deno_default.version} (Cloudflare-Workers)`);
assertEquals(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Cloudflare-Workers)`);
assertEquals(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Cloudflare-Workers; +https://example.com/)`);
assertEquals(getUserAgent({
software: "MyApp/1.0.0",
url: new URL("https://example.com/")
}), `MyApp/1.0.0 (Fedify/${deno_default.version}; Cloudflare-Workers; +https://example.com/)`);
} else {
assertEquals(getUserAgent(), `Fedify/${deno_default.version} (Node.js/${process.versions.node})`);
assertEquals(getUserAgent({ software: "MyApp/1.0.0" }), `MyApp/1.0.0 (Fedify/${deno_default.version}; Node.js/${process.versions.node})`);
assertEquals(getUserAgent({ url: "https://example.com/" }), `Fedify/${deno_default.version} (Node.js/${process.versions.node}; +https://example.com/)`);
assertEquals(getUserAgent({
software: "MyApp/1.0.0",
url: new URL("https://example.com/")
}), `MyApp/1.0.0 (Fedify/${deno_default.version}; Node.js/${process.versions.node}; +https://example.com/)`);
}
});
//#endregion