tests/federation.test.ts
Rama main · réplica de lectura del repositorio original.
// NOTHING PRIVATE GOES OUT (the audit's test, 3.24 Part 3 item 6).
//
// Webmentions and the fediverse are the two features that talk to other
// servers about this site, and both are held to one rule
// (server/federation.ts::federablePosts): published, LISTED (not a template,
// not a library lesson), and not curated away by the language filter as the
// site speaks without a reader. This file builds a vault with one note of
// each kind that must never leave — a draft, a published template, a
// published library lesson, a published note the language filter hides —
// and one public note that LINKS to all of them, then turns every switch on
// and asks every door: may it be a webmention's target, does it send one,
// is it in the outbox, does its address answer as an object, does a like of
// it file anything, is a Create delivered for it. The answer is no, each
// time, for each. And with PUBLIC=false nothing at all is federable.
import assert from "node:assert/strict";
import { generateKeyPairSync } from "node:crypto";
import { writeFileSync } from "node:fs";
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import path from "node:path";
import { after, before, describe, it } from "node:test";
import { closeActivityPub, drainDeliveries, handleActivity, initActivityPub, outbox, rememberActorForTests, type RemoteActor } from "../server/activitypub.ts";
import { activitypubPublic } from "../server/activitypubRoutes.ts";
import { initAuth } from "../server/auth.ts";
import { initComments, listInteractions } from "../server/comments.ts";
import { closeFederation, federablePosts, pathForUrl, reconcile } from "../server/federation.ts";
import { indexFile, initIndexer } from "../server/indexer.ts";
import { allowPrivateAddressesForTests } from "../server/safeFetch.ts";
import { patchSettings } from "../server/settings.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { acceptWebmention, closeWebmentions, drainWebmentions, initWebmentions, sentMentions, setWebmentionBackoffForTests } from "../server/webmentions.ts";
import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts";
const SITE = "https://blog.example";
const ARABIC = "فقرة عربية كاملة تكفي لتصنيف اللغة بلا لبس، وتكفي أيضاً ليعرف الفلتر أنها ليست إنجليزية.";
let server: Server;
let local = "";
const posted: { path: string; body: string }[] = [];
let root = "";
const data = makeDir();
/** Every note that must never leave, by why. */
const HIDDEN: Record<string, string> = {
private: "Draft.md",
template: "Templates/Stencil.md",
lesson: "Books/Optics/Chapter 1.md",
filtered: "مقالة.md",
};
const PUBLIC = "Essay.md";
const url = (notePath: string): string => `${SITE}/${notePath.replace(/\.md$/, "").split("/").map(encodeURIComponent).join("/")}`;
const follower: RemoteActor = {
id: "",
inbox: "",
sharedInbox: null,
name: "Ada",
url: null,
icon: null,
keyId: "",
pem: generateKeyPairSync("rsa", { modulusLength: 2048 }).publicKey.export({ type: "spki", format: "pem" }) as string,
};
before(async () => {
server = createServer((req, res) => {
let body = "";
req.on("data", (c: Buffer) => (body += c.toString()));
req.on("end", () => {
if (req.method === "POST") posted.push({ path: req.url ?? "/", body });
// Every page advertises an endpoint, so any send that was made would land.
res.writeHead(req.method === "POST" ? 202 : 200, { "Content-Type": "text/html", Link: '</wm>; rel="webmention"' });
res.end("<p>ok</p>");
});
});
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
local = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
allowPrivateAddressesForTests(true);
follower.id = `${local}/users/ada`;
follower.inbox = `${local}/users/ada/inbox`;
follower.keyId = `${follower.id}#main-key`;
const external = (tag: string): string => `See [elsewhere](${local}/from/${tag}).\n`;
root = makeVault({
[PUBLIC]: note({ publish: "true", date: "2026-09-01" }, `A public essay. It links to [the draft](${url(HIDDEN.private)}), [the stencil](${url(HIDDEN.template)}), [the lesson](${url(HIDDEN.lesson)}) and [the Arabic](${url(HIDDEN.filtered)}).\n`),
[HIDDEN.private]: note({ publish: "false" }, external("private")),
[HIDDEN.template]: note({ publish: "true" }, external("template")),
[HIDDEN.lesson]: note({ publish: "true" }, external("lesson")),
[HIDDEN.filtered]: note({ publish: "true" }, `${ARABIC}\n\n${external("filtered")}`),
});
initSite({ ASTROLABE_DATA: data, SITE_URL: SITE });
initVault(root);
await initIndexer();
patchSettings({
languageFilter: "en",
library: { enabled: true, paths: [{ id: "optics", slug: "optics", folder: "Books/Optics", kind: "book", title: "Optics" }] },
webmentions: { accept: true, send: true },
fediverse: { enabled: true },
});
initComments({ ASTROLABE_DATA: data });
initWebmentions({ dbFile: path.join(data, "webmentions.db") });
setWebmentionBackoffForTests([0, 0, 0]);
initActivityPub({ dbFile: path.join(data, "activitypub.db"), keyFile: path.join(data, "activitypub-key.pem") });
rememberActorForTests(follower);
await handleActivity({ id: `${follower.id}#f`, type: "Follow", actor: follower.id, object: `${SITE}/actor` }, follower, SITE);
// The baseline, taken with nothing published but the essay.
for (const rel of [PUBLIC, ...Object.values(HIDDEN)]) {
writeFileSync(path.join(root, rel), note({ publish: "false" }, "held back\n"));
await indexFile(rel);
}
await reconcile();
await drainDeliveries();
posted.length = 0;
});
after(async () => {
closeWebmentions();
closeActivityPub();
closeFederation();
allowPrivateAddressesForTests(false);
await new Promise<void>((r) => server.close(() => r()));
removeVault(root);
removeVault(data);
});
describe("nothing private, unlisted or filtered by language is ever a source or a target", () => {
before(async () => {
// Now publish them all at once, the way a git pull would.
const external = (tag: string): string => `See [elsewhere](${local}/from/${tag}).\n`;
const files: Record<string, string> = {
[PUBLIC]: note({ publish: "true", date: "2026-09-01" }, `A public essay. It links to [the draft](${url(HIDDEN.private)}), [the stencil](${url(HIDDEN.template)}), [the lesson](${url(HIDDEN.lesson)}), [the Arabic](${url(HIDDEN.filtered)}) and [a friend](${local}/from/public).\n`),
[HIDDEN.private]: note({ publish: "false" }, external("private")),
[HIDDEN.template]: note({ publish: "true" }, external("template")),
[HIDDEN.lesson]: note({ publish: "true" }, external("lesson")),
[HIDDEN.filtered]: note({ publish: "true" }, `${ARABIC}\n\n${external("filtered")}`),
};
for (const [rel, text] of Object.entries(files)) {
writeFileSync(path.join(root, rel), text);
await indexFile(rel);
}
await reconcile();
await drainWebmentions();
await drainDeliveries();
});
it("the federable set is the public essay alone", () => {
assert.deepEqual(federablePosts().map((p) => p.path), [PUBLIC]);
});
for (const [why, rel] of Object.entries(HIDDEN)) {
it(`the ${why} note is no webmention target, and no Like of it files`, async () => {
assert.equal(pathForUrl(url(rel), SITE), null);
assert.equal(acceptWebmention(`${local}/src`, url(rel), SITE), "badTarget");
await handleActivity({ id: `${follower.id}#like-${why}`, type: "Like", actor: follower.id, object: url(rel) }, follower, SITE);
assert.equal(listInteractions(rel, true).length, 0);
});
it(`the ${why} note sends no webmention, and none is sent to it`, () => {
const sent = sentMentions(500);
assert.equal(sent.some((s) => s.notePath === rel), false);
assert.equal(sent.some((s) => s.target === url(rel)), false);
assert.equal(posted.some((p) => p.body.includes(encodeURIComponent(`/from/${why}`))), false);
});
it(`the ${why} note is in no outbox, answers as no object, and no follower is told of it`, async () => {
const page = await outbox(SITE, 1);
assert.equal(JSON.stringify(page).includes(url(rel)), false);
const res = await activitypubPublic.request(url(rel), { headers: { Accept: "application/activity+json" } });
assert.equal(res.status, 404);
assert.equal(posted.some((p) => p.body.includes(url(rel))), false);
});
}
it("the public essay does all of it: sends its one outside link, is delivered, is in the outbox", async () => {
const sent = sentMentions(500);
assert.deepEqual(sent.map((s) => s.target), [`${local}/from/public`]);
assert.ok(posted.some((p) => p.path === "/users/ada/inbox" && JSON.parse(p.body).type === "Create"));
assert.ok(JSON.stringify(await outbox(SITE, 1)).includes(url(PUBLIC)));
});
it("the set follows the setting: a page the filter now hides is deleted from followers and sends nothing more", async () => {
posted.length = 0;
const sentBefore = sentMentions(500).length;
patchSettings({ languageFilter: "ar" });
await reconcile();
await drainWebmentions();
await drainDeliveries();
const bodies = posted.filter((p) => p.path.startsWith("/users/")).map((p) => JSON.parse(p.body) as { type?: string; object?: { id?: string } });
assert.ok(bodies.some((a) => a.type === "Delete" && a.object?.id === url(PUBLIC)), "the essay is deleted from the fediverse");
assert.equal(bodies.some((a) => a.object?.id === url(PUBLIC) && a.type !== "Delete"), false);
// …and the Arabic note, which the filter now shows, is public — and
// announced as what it is.
assert.ok(bodies.some((a) => a.type === "Create" && a.object?.id === url(HIDDEN.filtered)));
assert.equal(sentMentions(500).filter((s) => s.notePath === PUBLIC).length, 1, "the essay sent nothing on its way out");
assert.ok(sentMentions(500).length >= sentBefore);
patchSettings({ languageFilter: "en" });
});
it("with PUBLIC=false nothing at all is federable", () => {
initAuth({ PUBLIC: "false", ADMIN_PASSWORD_HASH: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$aGFzaGhhc2hoYXNoaGFzaA", SESSION_SECRET: "relsecret0123456789abcdef0123456789" });
try {
assert.deepEqual(federablePosts(), []);
assert.equal(pathForUrl(url(PUBLIC), SITE), null);
} finally {
initAuth({});
}
});
});