diff --git a/packages/relay/src/litepub.test.ts b/packages/relay/src/litepub.test.ts index 8404c8201..a0751770c 100644 --- a/packages/relay/src/litepub.test.ts +++ b/packages/relay/src/litepub.test.ts @@ -39,7 +39,7 @@ const mockDocumentLoader = async (url: string): Promise => { id: url, type: "Person", preferredUsername: "alice", - inbox: "https://remote.example.com/users/alice/inbox", + inbox: aliceInbox, publicKey: { id: "https://remote.example.com/users/alice#main-key", owner: url.replace(/#main-key$/, ""), @@ -119,6 +119,7 @@ const davePublicKey = { // before the request is made. mastodon.test.ts uses a disjoint /48 so that // neither file's interceptor can swallow the other's deliveries. const INBOX_PREFIX = "https://[2001:db8:1::"; +const aliceInbox = `${INBOX_PREFIX}4]/users/alice/inbox`; const daveInbox = `${INBOX_PREFIX}1]/users/dave/inbox`; const pendingInbox = `${INBOX_PREFIX}2]/users/bob/inbox`; const acceptedInbox = `${INBOX_PREFIX}3]/users/carol/inbox`; @@ -141,6 +142,25 @@ function recordInbox(inbox: string): DeliveredRequest[] { return recorded; } +const aliceDeliveries = recordInbox(aliceInbox); + +function assertAcceptDelivered( + follow: Follow, + deliveries: readonly DeliveredRequest[] = aliceDeliveries, +): void { + const accepts = deliveries.filter(({ body }) => + body.type === "Accept" && body.object?.id === follow.id?.href + ); + strictEqual(accepts.length, 1, "Expected exactly one Accept for this Follow"); + strictEqual(accepts[0].method, "POST"); + strictEqual(accepts[0].body.actor, "https://relay.example.com/users/relay"); + strictEqual(accepts[0].body.object.type, "Follow"); + strictEqual( + accepts[0].body.object.actor.id, + follow.actorId?.href, + ); +} + const nextFetch = globalThis.fetch; globalThis.fetch = (async (input: URL | RequestInfo, init?: RequestInit) => { const request = input instanceof Request ? input : new Request(input, init); @@ -360,7 +380,10 @@ describe("LitePubRelay", () => { }); const followActivity = new Follow({ - id: new URL("https://remote.example.com/activities/follow/1"), + // Isolate delivery assertions from other tests and loop iterations. + id: new URL( + `https://remote.example.com/activities/follow/${crypto.randomUUID()}`, + ), actor: follower.id, object: new URL("https://relay.example.com/users/relay"), }); @@ -383,10 +406,8 @@ describe("LitePubRelay", () => { const deliveredActivities = recordInbox(daveInbox); const response = await relay.fetch(request); - ok( - response.status === 200 || response.status === 202, - `Unexpected inbox response status: ${response.status}`, - ); + strictEqual(response.status, 202); + assertAcceptDelivered(followActivity, deliveredActivities); // Verify handler was called strictEqual(handlerCalled, true); @@ -434,7 +455,7 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); const followActivity = new Follow({ @@ -459,7 +480,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower was NOT stored const followerData = await kv.get([ @@ -483,12 +505,15 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); // Public follow activity const followActivity = new Follow({ - id: new URL("https://remote.example.com/activities/follow/1"), + // Isolate delivery assertions from other tests and loop iterations. + id: new URL( + `https://remote.example.com/activities/follow/${crypto.randomUUID()}`, + ), actor: follower.id, object: new URL("https://www.w3.org/ns/activitystreams#Public"), }); @@ -509,7 +534,9 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); + assertAcceptDelivered(followActivity); // Verify follower was stored with "pending" state const followerData = await kv.get([ @@ -553,7 +580,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower was NOT stored const followerData = await kv.get([ @@ -593,7 +621,10 @@ describe("LitePubRelay", () => { strictEqual(await relay.getFollower(followerId), null); const followActivity = new Follow({ - id: new URL("https://remote.example.com/activities/follow/1"), + // Isolate delivery assertions from other tests and loop iterations. + id: new URL( + `https://remote.example.com/activities/follow/${crypto.randomUUID()}`, + ), actor: new URL(followerId), object: new URL("https://relay.example.com/users/relay"), }); @@ -610,7 +641,9 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); + assertAcceptDelivered(followActivity); strictEqual(handlerCallCount, 1); const follower = await relay.getFollower(followerId); @@ -639,7 +672,7 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( ["follower", "https://remote.example.com/users/alice"], @@ -667,7 +700,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); strictEqual(handlerCallCount, 0); const followerData = await kv.get([ @@ -685,7 +719,7 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); // Pre-populate with pending follower @@ -730,7 +764,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower state changed to "accepted" const followerData = await kv.get([ @@ -791,7 +826,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); deepStrictEqual(await kv.get(["follower", followerId]), invalidRow); } @@ -805,7 +841,7 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -849,7 +885,8 @@ describe("LitePubRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); const followerData = await kv.get(["follower", followerId]); strictEqual(followerData, undefined); @@ -864,7 +901,7 @@ describe("LitePubRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -1314,7 +1351,7 @@ describe("LitePubRelay", () => { id: new URL(followerId), preferredUsername: "alice", name: "Alice Wonderland", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( diff --git a/packages/relay/src/mastodon.test.ts b/packages/relay/src/mastodon.test.ts index 23fe1b51b..ffd5d57b2 100644 --- a/packages/relay/src/mastodon.test.ts +++ b/packages/relay/src/mastodon.test.ts @@ -38,7 +38,7 @@ const mockDocumentLoader = async (url: string): Promise => { id: url, type: "Person", preferredUsername: "alice", - inbox: "https://remote.example.com/users/alice/inbox", + inbox: aliceInbox, publicKey: { id: "https://remote.example.com/users/alice#main-key", owner: url.replace(/#main-key$/, ""), @@ -87,6 +87,7 @@ const rsaPublicKey = { // before the request is made. litepub.test.ts uses a disjoint /48 so that // neither file's interceptor can swallow the other's deliveries. const INBOX_PREFIX = "https://[2001:db8:2::"; +const aliceInbox = `${INBOX_PREFIX}2]/users/alice/inbox`; const followerInbox = `${INBOX_PREFIX}1]/users/bob/inbox`; interface DeliveredRequest { @@ -107,6 +108,25 @@ function recordInbox(inbox: string): DeliveredRequest[] { return recorded; } +const aliceDeliveries = recordInbox(aliceInbox); + +function assertAcceptDelivered( + follow: Follow, + deliveries: readonly DeliveredRequest[] = aliceDeliveries, +): void { + const accepts = deliveries.filter(({ body }) => + body.type === "Accept" && body.object?.id === follow.id?.href + ); + strictEqual(accepts.length, 1, "Expected exactly one Accept for this Follow"); + strictEqual(accepts[0].method, "POST"); + strictEqual(accepts[0].body.actor, "https://relay.example.com/users/relay"); + strictEqual(accepts[0].body.object.type, "Follow"); + strictEqual( + accepts[0].body.object.actor.id, + follow.actorId?.href, + ); +} + const nextFetch = globalThis.fetch; globalThis.fetch = (async (input: URL | RequestInfo, init?: RequestInit) => { const request = input instanceof Request ? input : new Request(input, init); @@ -280,7 +300,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); // Simulate the relay's internal logic @@ -302,7 +322,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -401,11 +421,14 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); const followActivity = new Follow({ - id: new URL("https://remote.example.com/activities/follow/1"), + // Isolate delivery assertions from other tests and loop iterations. + id: new URL( + `https://remote.example.com/activities/follow/${crypto.randomUUID()}`, + ), actor: follower.id, object: new URL("https://relay.example.com/users/relay"), }); @@ -426,7 +449,9 @@ describe("MastodonRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); + assertAcceptDelivered(followActivity); // Verify handler was called strictEqual(handlerCalled, true); @@ -457,7 +482,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); const followActivity = new Follow({ @@ -482,7 +507,8 @@ describe("MastodonRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower was NOT stored const followerData = await kv.get([ @@ -500,7 +526,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); const followActivityId = "https://remote.example.com/activities/follow/1"; @@ -545,7 +571,8 @@ describe("MastodonRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower was removed const followerData = await kv.get(["follower", followerId]); @@ -560,7 +587,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL(followerId), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -824,7 +851,8 @@ describe("MastodonRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); // Verify follower was NOT stored const followerData = await kv.get([ @@ -848,12 +876,15 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); // Public follow activity const followActivity = new Follow({ - id: new URL("https://remote.example.com/activities/follow/1"), + // Isolate delivery assertions from other tests and loop iterations. + id: new URL( + `https://remote.example.com/activities/follow/${crypto.randomUUID()}`, + ), actor: follower.id, object: new URL("https://www.w3.org/ns/activitystreams#Public"), }); @@ -874,7 +905,9 @@ describe("MastodonRelay", () => { rsaPublicKey.id, ); - await relay.fetch(request); + const response = await relay.fetch(request); + strictEqual(response.status, 202); + assertAcceptDelivered(followActivity); // Verify follower was stored const followerData = await kv.get([ @@ -976,7 +1009,7 @@ describe("MastodonRelay", () => { id: new URL(followerId), preferredUsername: "alice", name: "Alice Wonderland", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -1009,12 +1042,12 @@ describe("MastodonRelay", () => { const follower1 = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); const follower2 = new Person({ id: new URL("https://remote.example.com/users/bob"), preferredUsername: "bob", - inbox: new URL("https://remote.example.com/users/bob/inbox"), + inbox: new URL(`${INBOX_PREFIX}3]/users/bob/inbox`), }); await kv.set( @@ -1075,7 +1108,7 @@ describe("MastodonRelay", () => { const follower = new Person({ id: new URL("https://remote.example.com/users/alice"), preferredUsername: "alice", - inbox: new URL("https://remote.example.com/users/alice/inbox"), + inbox: new URL(aliceInbox), }); await kv.set( @@ -1094,7 +1127,7 @@ describe("MastodonRelay", () => { strictEqual(result.actor.preferredUsername, "alice"); strictEqual( result.actor.inboxId?.href, - "https://remote.example.com/users/alice/inbox", + aliceInbox, ); // Test non-existent follower