From e32481da38cc60b5a9d920f05789639d385cdbb4 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Fri, 30 Aug 2024 08:57:16 +0100 Subject: [PATCH] Add unit test for outbox --- docker-compose.yml | 2 - features/outbox.feature | 27 ---------- features/step_definitions/stepdefs.js | 76 ++------------------------- src/dispatchers.unit.test.ts | 75 +++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 101 deletions(-) delete mode 100644 features/outbox.feature diff --git a/docker-compose.yml b/docker-compose.yml index 422c86da..e0e5fc8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,8 +91,6 @@ services: test: "mysql -ughost -ppassword activitypub -e 'select 1'" interval: 1s retries: 120 - ports: - - "3308:3306" wiremock: image: wiremock/wiremock:latest diff --git a/features/outbox.feature b/features/outbox.feature deleted file mode 100644 index 7616da1c..00000000 --- a/features/outbox.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Outbox - In order to view the activities performed by an actor - As a fediverse server - I want be able to retrieve an actor's activities from their outbox - - Scenario: outbox contains relevant activities - Given an Actor "Alice" - And a "Create(Article)" Activity "C" by "Alice" - And a "Announce(C)" Activity "An" by "Alice" - And a "Follow(Us)" Activity "F" by "Alice" - And a "Accept(F)" Activity "A" by "Alice" - And "Alice" adds "C" to the Outbox - And "Alice" adds "An" to the Outbox - And "Alice" adds "F" to the Outbox - And "Alice" adds "A" to the Outbox - When the contents of the outbox is requested - Then the outbox contains 2 activities - - Scenario: outbox contains is ordered reverse chronologically - Given an Actor "Alice" - And a "Create(Article)" Activity "A1" by "Alice" - And a "Create(Article)" Activity "A2" by "Alice" - And "Alice" adds "A1" to the Outbox - And "Alice" adds "A2" to the Outbox - When the contents of the outbox is requested - Then the outbox contains 2 activities - And the items in the outbox are in the order: "A2, A1" diff --git a/features/step_definitions/stepdefs.js b/features/step_definitions/stepdefs.js index eace8ceb..d39b0b2d 100644 --- a/features/step_definitions/stepdefs.js +++ b/features/step_definitions/stepdefs.js @@ -4,7 +4,7 @@ import { BeforeAll, AfterAll, Before, After, Given, When, Then } from '@cucumber import { v4 as uuidv4 } from 'uuid'; import { WireMock } from 'wiremock-captain'; -async function createActivity(activityType, object, actor, name) { +async function createActivity(activityType, object, actor, remote = true) { if (activityType === 'Follow') { return { '@context': [ @@ -40,7 +40,7 @@ async function createActivity(activityType, object, actor, name) { 'https://w3id.org/security/data-integrity/v1', ], 'type': 'Create', - 'id': `http://wiremock:8080/create/${name || '1'}`, + 'id': 'http://wiremock:8080/create/1', 'to': 'as:Public', 'object': object, actor: actor, @@ -166,6 +166,8 @@ BeforeAll(async function () { database: process.env.MYSQL_DATABASE } }); + + await client('key_value').truncate(); }); BeforeAll(async function () { @@ -178,7 +180,6 @@ AfterAll(async function () { Before(async function () { await captain.clearAllRequests(); - await client('key_value').truncate(); }); Before(async function () { @@ -205,7 +206,7 @@ Given('a {string} Activity {string} by {string}', async function (activityDef, n const object = this.actors[objectName] ?? this.activities[objectName] ?? await createObject(objectName); const actor = this.actors[actorName]; - const activity = await createActivity(activityType, object, actor, name); + const activity = await createActivity(activityType, object, actor); this.activities[name] = activity; }); @@ -359,70 +360,3 @@ Then('{string} is in our Followers once only', async function (actorName) { assert.equal(found.length, 1); }); - -When('the contents of the outbox is requested', async function () { - const response = await fetch('http://activitypub-testing:8083/.ghost/activitypub/outbox/index', { - headers: { - 'Content-Type': 'application/ld+json' - }, - }); - - this.response = await response.json(); -}); - -When('{string} adds {string} to the Outbox', async function (actorName, activityName) { - if (!this.actors[actorName]) { - throw new Error(`Could not find Actor ${actorName}`); - } - if (!this.activities[activityName]) { - throw new Error(`Could not find Activity ${activityName}`); - } - - const activity = this.activities[activityName]; - - // Add activity to the db - const activityKey = JSON.stringify([activity.id]); - - if ( - (await client('key_value').select('value').where({ key: activityKey })).length === 0 - ) { - await client('key_value').insert({ - key: activityKey, - value: JSON.stringify(activity) - }) - } - - // Add activity to the outbox - const outboxKey = JSON.stringify(["sites","activitypub-testing:8083","outbox"]); - const outbox = ( - await client('key_value').select('value').where('key', outboxKey) - ).map((item) => item.value)[0] || []; - - outbox.push(activity.id) - - if (outbox.length === 1) { - await client('key_value').insert({ - key: outboxKey, - value: JSON.stringify(outbox) - }) - } else { - await client('key_value') - .where({ key: outboxKey }) - .update({ - value: JSON.stringify(outbox) - }) - } -}); - -Then(/^the outbox contains ([0-9]+) (activity|activities)$/, function (count, word) { - assert.equal(this.response.totalItems, count); -}); - -Then('the items in the outbox are in the order: {string}', function (order) { - order.split(",").map((item) => item.trim()).forEach((activityName, index) => { - assert.ok( - this.response.orderedItems[index].id.endsWith(activityName), - `${this.response.orderedItems[index].id} does not end with ${activityName} as expected` - ) - }) -}); diff --git a/src/dispatchers.unit.test.ts b/src/dispatchers.unit.test.ts index c424590a..ff3678a8 100644 --- a/src/dispatchers.unit.test.ts +++ b/src/dispatchers.unit.test.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import sinon from 'sinon'; import { actorDispatcher, keypairDispatcher, @@ -18,7 +19,7 @@ import { acceptDispatcher, createDispatcher, } from './dispatchers'; -import { RequestContext } from '@fedify/fedify'; +import { Activity, RequestContext } from '@fedify/fedify'; import { ACTOR_DEFAULT_HANDLE } from './constants'; describe('dispatchers', function () { @@ -35,4 +36,76 @@ describe('dispatchers', function () { }); describe('keypairDispatcher', function () {}); describe('handleFollow', function () {}); + + describe('outboxDispatcher', function () { + const outboxActivities: Record = { + 'https://example.com/create/123': { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/data-integrity/v1', + ], + id: 'https://example.com/create/123', + type: 'Create', + }, + 'https://example.com/announce/456': { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/data-integrity/v1', + ], + type: 'Announce', + id: 'https://example.com/announce/456', + }, + 'https://example.com/accept/789': { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/data-integrity/v1', + ], + type: 'Accept', + id: 'https://example.com/accept/789', + }, + 'https://example.com/like/987': { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/data-integrity/v1', + ], + type: 'Like', + id: 'https://example.com/like/987', + }, + }; + + const ctx = { + data: { + db: { + get: sinon.stub(), + }, + globaldb: { + get: sinon.stub(), + }, + }, + } as RequestContext; + + beforeEach(function () { + ctx.data.db.get.withArgs(['outbox']).resolves(Object.keys(outboxActivities)); + + Object.keys(outboxActivities).forEach(key => { + ctx.data.globaldb.get.withArgs([key]).resolves(outboxActivities[key]); + }); + }); + + it('returns relevant items from the outbox in the correct order', async function () { + const result = await outboxDispatcher(ctx, ACTOR_DEFAULT_HANDLE); + + // Check items exist + assert.ok(result.items); + + // Check correct items are returned in the correct order + assert.equal(result.items.length, 2); + assert.equal(result.items[0] instanceof Activity, true); + assert.equal(result.items[1] instanceof Activity, true); + // @ts-ignore: We know that this is the correct type because of the above assertions + assert.equal(result.items[0].id.toString(), 'https://example.com/announce/456'); + // @ts-ignore: We know that this is the correct type because of the above assertions + assert.equal(result.items[1].id.toString(), 'https://example.com/create/123'); + }); + }); });