UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

499 lines (495 loc) • 17.6 kB
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