From 124cbcc947619d2ae8daa7bac38a3386dcdc5a9a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 19 Dec 2023 01:38:05 +0530 Subject: [PATCH 01/12] test: add tests for HTTP APIs and their routes --- src/server/api/admins.test.ts | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/server/api/admins.test.ts diff --git a/src/server/api/admins.test.ts b/src/server/api/admins.test.ts new file mode 100644 index 0000000..4f7fbb6 --- /dev/null +++ b/src/server/api/admins.test.ts @@ -0,0 +1,158 @@ +import test from 'ava' +import fastify from 'fastify' +import sinon from 'sinon' +import { adminRoutes } from './admins' +import Store from '../store/index.js' +import ActivityPubSystem from '../apsystem.js' +import { ModerationChecker } from '../moderation.js' +import HookSystem from '../hooksystem' +import { APIConfig } from '.' +import { makeSigner } from '../../keypair.js' +import { generateKeypair } from 'http-signed-fetch' + +const mockConfig: APIConfig = { + port: 3000, + host: 'localhost', + storage: 'path/to/storage', + publicURL: 'http://localhost:3000' +} + +let server: any +let mockStore: any +let mockApsystem: any + +test.beforeEach(async () => { + server = fastify() + mockStore = sinon.createStubInstance(Store) + + mockStore.admins = { + list: sinon.stub(), + add: sinon.stub(), + remove: sinon.stub() + } + mockStore.admins.list.resolves(['admin1@example.com', 'admin2@example.com']) + mockStore.admins.add.resolves() + mockStore.admins.remove.resolves() + + mockApsystem = new ActivityPubSystem(mockConfig.publicURL, mockStore, new ModerationChecker(mockStore), new HookSystem(mockStore)) + sinon.stub(mockApsystem, 'hasAdminPermissionForRequest').resolves(true) + + await adminRoutes(mockConfig, mockStore, mockApsystem)(server) +}) + +const simulateSignedRequest = (method: string, path: string): { Signature: string, Date: string } => { + const keypair = generateKeypair() + const publicKeyId = 'https://example.com/#main-key' + const signer = makeSigner(keypair, publicKeyId) + + const url = `${mockConfig.publicURL}${path}` + + // Generate a signature header + const signatureHeader = signer.sign({ + method, + url, + headers: { + host: 'localhost:3000', + date: new Date().toUTCString() + } + }) + + return { + Signature: signatureHeader, + Date: new Date().toUTCString() + } +} + +test('GET /admins - success', async t => { + const signedHeaders = simulateSignedRequest('GET', '/admins') + + mockApsystem.hasAdminPermissionForRequest.callsFake((request: any) => { + return 'Signature' in request.headers && 'Date' in request.headers + }) + + const response = await server.inject({ + method: 'GET', + url: '/admins', + headers: signedHeaders + }) + + t.is(response.statusCode, 200) + t.is(response.body, 'admin1@example.com\nadmin2@example.com') +}) + +test('GET /admins - unauthorized', async t => { + mockApsystem.hasAdminPermissionForRequest.resolves(false) + + const response = await server.inject({ + method: 'GET', + url: '/admins' + }) + + t.is(response.statusCode, 403) +}) + +test('POST /admins - success', async t => { + const signedHeaders = simulateSignedRequest('POST', '/admins') + + const response = await server.inject({ + method: 'POST', + url: '/admins', + payload: 'newadmin@example.com', + headers: { + 'Content-Type': 'text/plain', + ...signedHeaders + } + }) + + t.is(response.statusCode, 200) +}) + +test('POST /admins - unauthorized', async t => { + mockApsystem.hasAdminPermissionForRequest.resolves(false) + + const response = await server.inject({ + method: 'POST', + url: '/admins', + payload: 'newadmin@example.com', + headers: { + 'Content-Type': 'text/plain' + } + }) + + t.is(response.statusCode, 403) +}) + +test('DELETE /admins - success', async t => { + const signedHeaders = simulateSignedRequest('DELETE', '/admins') + + const response = await server.inject({ + method: 'DELETE', + url: '/admins', + payload: 'admin1@example.com', + headers: { + 'Content-Type': 'text/plain', + ...signedHeaders + } + }) + + t.is(response.statusCode, 200) +}) + +test('DELETE /admins - unauthorized', async t => { + mockApsystem.hasAdminPermissionForRequest.resolves(false) + + const response = await server.inject({ + method: 'DELETE', + url: '/admins', + payload: 'admin1@example.com', + headers: { + 'Content-Type': 'text/plain' + } + }) + + t.is(response.statusCode, 403) +}) + +test.afterEach(async () => { + await server.close() +}) From fcf7ffdac5de5988dedd9d8de4974961c4441bfb Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Wed, 3 Jan 2024 21:43:40 +0530 Subject: [PATCH 02/12] refactor: improved reliability and simplification for admins.test.ts --- src/server/api/admins.test.ts | 155 +++++++++++----------------------- 1 file changed, 49 insertions(+), 106 deletions(-) diff --git a/src/server/api/admins.test.ts b/src/server/api/admins.test.ts index 4f7fbb6..c110505 100644 --- a/src/server/api/admins.test.ts +++ b/src/server/api/admins.test.ts @@ -1,158 +1,101 @@ -import test from 'ava' -import fastify from 'fastify' +import anyTest, { TestFn } from 'ava' import sinon from 'sinon' -import { adminRoutes } from './admins' -import Store from '../store/index.js' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' import ActivityPubSystem from '../apsystem.js' -import { ModerationChecker } from '../moderation.js' -import HookSystem from '../hooksystem' -import { APIConfig } from '.' -import { makeSigner } from '../../keypair.js' -import { generateKeypair } from 'http-signed-fetch' - -const mockConfig: APIConfig = { - port: 3000, - host: 'localhost', - storage: 'path/to/storage', - publicURL: 'http://localhost:3000' -} - -let server: any -let mockStore: any -let mockApsystem: any - -test.beforeEach(async () => { - server = fastify() - mockStore = sinon.createStubInstance(Store) - mockStore.admins = { - list: sinon.stub(), - add: sinon.stub(), - remove: sinon.stub() - } - mockStore.admins.list.resolves(['admin1@example.com', 'admin2@example.com']) - mockStore.admins.add.resolves() - mockStore.admins.remove.resolves() +interface TestContext { + server: FastifyTypebox + hasAdminPermissionForRequestStub: sinon.SinonStub +} - mockApsystem = new ActivityPubSystem(mockConfig.publicURL, mockStore, new ModerationChecker(mockStore), new HookSystem(mockStore)) - sinon.stub(mockApsystem, 'hasAdminPermissionForRequest').resolves(true) +const test = anyTest as TestFn - await adminRoutes(mockConfig, mockStore, mockApsystem)(server) +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + t.context.hasAdminPermissionForRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasAdminPermissionForRequest') }) -const simulateSignedRequest = (method: string, path: string): { Signature: string, Date: string } => { - const keypair = generateKeypair() - const publicKeyId = 'https://example.com/#main-key' - const signer = makeSigner(keypair, publicKeyId) - - const url = `${mockConfig.publicURL}${path}` - - // Generate a signature header - const signatureHeader = signer.sign({ - method, - url, - headers: { - host: 'localhost:3000', - date: new Date().toUTCString() - } - }) - - return { - Signature: signatureHeader, - Date: new Date().toUTCString() - } -} - -test('GET /admins - success', async t => { - const signedHeaders = simulateSignedRequest('GET', '/admins') +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasAdminPermissionForRequestStub.restore() +}) - mockApsystem.hasAdminPermissionForRequest.callsFake((request: any) => { - return 'Signature' in request.headers && 'Date' in request.headers - }) +test.serial('GET /admins - success', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(true) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'GET', - url: '/admins', - headers: signedHeaders + url: '/v1/admins' }) - t.is(response.statusCode, 200) - t.is(response.body, 'admin1@example.com\nadmin2@example.com') + t.is(response.statusCode, 200, 'returns a status code of 200') }) -test('GET /admins - unauthorized', async t => { - mockApsystem.hasAdminPermissionForRequest.resolves(false) +test.serial('GET /admins - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'GET', - url: '/admins' + url: '/v1/admins' }) - t.is(response.statusCode, 403) + t.is(response.statusCode, 403, 'returns a status code of 403') }) -test('POST /admins - success', async t => { - const signedHeaders = simulateSignedRequest('POST', '/admins') +test.serial('POST /admins - add admins', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(true) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'POST', - url: '/admins', + url: '/v1/admins', payload: 'newadmin@example.com', headers: { - 'Content-Type': 'text/plain', - ...signedHeaders + 'Content-Type': 'text/plain' } }) - t.is(response.statusCode, 200) + t.is(response.statusCode, 200, 'returns a status code of 200') }) -test('POST /admins - unauthorized', async t => { - mockApsystem.hasAdminPermissionForRequest.resolves(false) +test.serial('POST /admins - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'POST', - url: '/admins', + url: '/v1/admins', payload: 'newadmin@example.com', headers: { 'Content-Type': 'text/plain' } }) - t.is(response.statusCode, 403) + t.is(response.statusCode, 403, 'returns a status code of 403') }) -test('DELETE /admins - success', async t => { - const signedHeaders = simulateSignedRequest('DELETE', '/admins') +test.serial('DELETE /admins - remove admins', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(true) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'DELETE', - url: '/admins', - payload: 'admin1@example.com', + url: '/v1/admins', + payload: 'removeadmin@example.com', headers: { - 'Content-Type': 'text/plain', - ...signedHeaders + 'Content-Type': 'text/plain' } }) - t.is(response.statusCode, 200) + t.is(response.statusCode, 200, 'returns a status code of 200') }) -test('DELETE /admins - unauthorized', async t => { - mockApsystem.hasAdminPermissionForRequest.resolves(false) +test.serial('DELETE /admins - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) - const response = await server.inject({ + const response = await t.context.server.inject({ method: 'DELETE', - url: '/admins', - payload: 'admin1@example.com', - headers: { - 'Content-Type': 'text/plain' - } + url: '/v1/admins', + payload: 'removeadmin@example.com' }) - t.is(response.statusCode, 403) -}) - -test.afterEach(async () => { - await server.close() + t.is(response.statusCode, 403, 'returns a status code of 403') }) From f3b5a9eb25c0a2d2775bf2d3e89e9f674eff381a Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Mon, 8 Jan 2024 23:58:18 +0530 Subject: [PATCH 03/12] chore: add a TODO for testing of APS's admin API interactions --- src/server/apsystem.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/apsystem.test.ts b/src/server/apsystem.test.ts index 920d326..0999491 100644 --- a/src/server/apsystem.test.ts +++ b/src/server/apsystem.test.ts @@ -34,6 +34,8 @@ const mockRequest = { // Initialize the main class to test const aps = new ActivityPubSystem('http://localhost', mockStore, mockModCheck, mockHooks) +// TODO: Add comprehensive tests for the ActivityPubSystem's interaction with the admin API routes. + test.beforeEach(() => { // Restore stubs before setting them up again sinon.restore() From 1fa68c67d7fe1efe2e38c63006e36922c11e7a68 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:31:32 +0530 Subject: [PATCH 04/12] test: add tests for creation APIs --- src/server/api/creation.test.ts | 134 ++++++++++++++++++++++++++++++++ src/server/api/creation.ts | 2 +- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/server/api/creation.test.ts diff --git a/src/server/api/creation.test.ts b/src/server/api/creation.test.ts new file mode 100644 index 0000000..f93259c --- /dev/null +++ b/src/server/api/creation.test.ts @@ -0,0 +1,134 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' + +interface TestContext { + server: FastifyTypebox + hasPermissionActorRequestStub: sinon.SinonStub +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasPermissionActorRequestStub.restore() +}) + +const actorInfo = { + actorUrl: 'https://test.instance/actorUrl', + publicKeyId: 'https://test.instance/publicKeyId', + keypair: { + publicKeyPem: 'publicKeyData', + privateKeyPem: 'privateKeyData' + } +} + +// Test for POST /:actor +test.serial('POST /:actor - success', async t => { + t.context.hasPermissionActorRequestStub.resolves(true) + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/testActor', + payload: JSON.stringify(actorInfo), + headers: { + 'Content-Type': 'application/json' + } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') + const responseBody = JSON.parse(response.body) + t.deepEqual(responseBody, actorInfo, 'returns the actor info') +}) + +test.serial('POST /:actor - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/testActor', + payload: JSON.stringify(actorInfo), + headers: { + 'Content-Type': 'application/json' + } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Test for GET /:actor +test.serial('GET /:actor - success', async t => { + // Create an actor first + await t.context.server.inject({ + method: 'POST', + url: '/v1/testActor', + payload: JSON.stringify(actorInfo), + headers: { + 'Content-Type': 'application/json' + } + }) + + // Perform the GET request + t.context.hasPermissionActorRequestStub.resolves(true) + const getResponse = await t.context.server.inject({ + method: 'GET', + url: '/v1/testActor' + }) + + t.is(getResponse.statusCode, 200, 'returns a status code of 200') + const getResponseBody = JSON.parse(getResponse.body) + t.deepEqual(getResponseBody, actorInfo, 'returns the expected actor info') +}) + +test.serial('GET /:actor - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/testActor' + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Test for DELETE /:actor +test.serial('DELETE /:actor - success', async t => { + // Ensure the actor exists before deletion + await t.context.server.inject({ + method: 'POST', + url: '/v1/testActor', + payload: JSON.stringify(actorInfo), + headers: { + 'Content-Type': 'application/json' + } + }) + + // Perform the DELETE request + t.context.hasPermissionActorRequestStub.resolves(true) + const deleteResponse = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/testActor' + }) + + const deleteResponseBody = JSON.parse(deleteResponse.body) + t.is(deleteResponse.statusCode, 200, 'returns a status code of 200') + t.deepEqual(deleteResponseBody, { message: 'Data deleted successfully' }, 'returns success message') +}) + +test.serial('DELETE /:actor - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/testActor' + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index d216ce1..ae3baaf 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -92,6 +92,6 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP } await store.forActor(actor).delete() - return await reply.send({ message: 'Data deleted successfully' }) + return await reply.code(200).type('application/json').send(JSON.stringify({ message: 'Data deleted successfully' })) }) } From 7d0b35cf6024389ccdcf3b46f228adfc678f0f34 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:34:46 +0530 Subject: [PATCH 05/12] test: add tests for followers APIs --- src/server/api/followers.test.ts | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/server/api/followers.test.ts diff --git a/src/server/api/followers.test.ts b/src/server/api/followers.test.ts new file mode 100644 index 0000000..6d3c5fe --- /dev/null +++ b/src/server/api/followers.test.ts @@ -0,0 +1,119 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' +import { APCollection } from 'activitypub-types' + +interface TestContext { + server: FastifyTypebox + hasPermissionActorRequestStub: sinon.SinonStub + mockStore: any +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + + // Set up the mockStore + t.context.mockStore = { + forActor: sinon.stub().returns({ + followers: { + add: sinon.stub().resolves(), + has: sinon.stub().resolves(true), + remove: sinon.stub().resolves() + } + }) + } + + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasPermissionActorRequestStub.restore() +}) + +// Test for GET /:actor/followers +test.serial('GET /:actor/followers - success', async t => { + const actor = 'testActor' + t.context.hasPermissionActorRequestStub.resolves(true) + + // Mock followers collection for the test actor + const mockedCollection: APCollection = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `http://localhost:3000/v1/${actor}/followers`, + type: 'OrderedCollection', + totalItems: 1, + items: [ + 'http://localhost:3000/v1/follower1' + ] + } + sinon.stub(ActivityPubSystem.prototype, 'followersCollection').resolves(mockedCollection) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/followers` + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') + t.truthy(response.body, 'returns a collection of followers') +}) + +test.serial('GET /:actor/followers - not allowed', async t => { + const actor = 'testActor' + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/followers` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// test.serial('DELETE /:actor/followers/:follower - success', async t => { +// const actor = 'testActor'; +// const follower = 'followerId'; +// t.context.hasPermissionActorRequestStub.resolves(true); + +// // Setup the mockStore to simulate the follower exists +// t.context.mockStore.forActor(actor).followers.has.withArgs(follower).resolves(true); + +// const response = await t.context.server.inject({ +// method: 'DELETE', +// url: `/v1/${actor}/followers/${follower}` +// }); + +// console.log('Response for DELETE /followers/:follower:', response.body); + +// t.is(response.statusCode, 200, 'returns a status code of 200'); +// t.is(response.body, 'OK', 'returns confirmation of deletion'); +// }); + +test.serial('DELETE /:actor/followers/:follower - not allowed', async t => { + const actor = 'testActor' + const follower = 'followerId' + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/followers/${follower}` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/followers/:follower - not found', async t => { + const actor = 'testActor' + const follower = 'nonexistentFollower' + t.context.hasPermissionActorRequestStub.resolves(true) + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/followers/${follower}` + }) + + t.is(response.statusCode, 404, 'returns a status code of 404') +}) From 2754fe4ff5c024bbd5243ad982df2e612d9d92dd Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:36:23 +0530 Subject: [PATCH 06/12] test: add tests for inbox APIs --- src/server/api/inbox.test.ts | 164 +++++++++++++++++++++++++++++++++++ src/server/api/inbox.ts | 5 ++ 2 files changed, 169 insertions(+) create mode 100644 src/server/api/inbox.test.ts diff --git a/src/server/api/inbox.test.ts b/src/server/api/inbox.test.ts new file mode 100644 index 0000000..cabed06 --- /dev/null +++ b/src/server/api/inbox.test.ts @@ -0,0 +1,164 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' +import { APOrderedCollection } from 'activitypub-types' + +interface TestContext { + server: FastifyTypebox + hasPermissionActorRequestStub: sinon.SinonStub + mockStore: any +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + + // Set up the mockStore + t.context.mockStore = { + forActor: sinon.stub().returns({ + inbox: { + list: sinon.stub().resolves([]), + add: sinon.stub().resolves(), + remove: sinon.stub().resolves() + } + }) + } + + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasPermissionActorRequestStub.restore() +}) + +// Test for GET /:actor/inbox +test.serial('GET /:actor/inbox - success', async t => { + const actor = 'testActor' + t.context.hasPermissionActorRequestStub.resolves(true) + + // Mock inbox collection + const mockedCollection: APOrderedCollection = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: `/v1/${actor}/inbox`, + orderedItems: [] + } + t.context.mockStore.forActor(actor).inbox.list.resolves(mockedCollection.orderedItems) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/inbox` + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') + t.deepEqual(JSON.parse(response.body), mockedCollection, 'returns the inbox collection') +}) + +test.serial('GET /:actor/inbox - not allowed', async t => { + const actor = 'testActor' + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/inbox` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Test for POST /:actor/inbox +test.serial('POST /:actor/inbox - success', async t => { + const actor = 'testActor' + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Create', + actor: 'https://example.com/user1', + object: { + type: 'Note', + content: 'Test note', + id: 'https://example.com/note1' + }, + id: 'https://example.com/activity1' + } + + t.context.hasPermissionActorRequestStub.resolves(true) + + // Mock external HTTP requests + sinon.stub(ActivityPubSystem.prototype, 'verifySignedRequest').resolves('https://example.com/actor') + sinon.stub(ActivityPubSystem.prototype, 'mentionToActor').resolves('https://example.com/user1') + sinon.stub(ActivityPubSystem.prototype, 'ingestActivity').resolves() + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/inbox`, + payload: activity, + headers: { 'Content-Type': 'application/json' } + }) + + // Restore the stubs after the test + sinon.restore() + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('POST /:actor/inbox - not allowed', async t => { + const actor = 'testActor' + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Create', + actor: 'https://example.com/user1', + object: { + type: 'Note', + content: 'Test note', + id: 'https://example.com/note1' + }, + id: 'https://example.com/activity1' + } + + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/inbox`, + payload: activity, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Test for DELETE /:actor/inbox/:id +test.serial('DELETE /:actor/inbox/:id - success', async t => { + const actor = 'testActor' + const id = 'testActivityId' + + t.context.hasPermissionActorRequestStub.resolves(true) + + // Stub the rejectActivity method + sinon.stub(ActivityPubSystem.prototype, 'rejectActivity').resolves() + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/inbox/${id}` + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('DELETE /:actor/inbox/:id - not allowed', async t => { + const actor = 'testActor' + const id = 'testActivityId' + + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/inbox/${id}` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) diff --git a/src/server/api/inbox.ts b/src/server/api/inbox.ts index 22bf735..40a5353 100644 --- a/src/server/api/inbox.ts +++ b/src/server/api/inbox.ts @@ -63,6 +63,11 @@ export const inboxRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubS }, async (request, reply) => { const { actor } = request.params + const allowed = await apsystem.hasPermissionActorRequest(actor, request) + if (!allowed) { + return await reply.code(403).send('Not Allowed') + } + const submittedActorMention = await apsystem.verifySignedRequest(request, actor) const submittedActorURL = await apsystem.mentionToActor(submittedActorMention) From 35d1fd1855b64586a19ffbef064c29f7c2ae3030 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:37:59 +0530 Subject: [PATCH 07/12] test: add tests for outbox APIs --- src/server/api/outbox.test.ts | 108 ++++++++++++++++++++++++++++++++++ src/server/api/outbox.ts | 5 +- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/server/api/outbox.test.ts diff --git a/src/server/api/outbox.test.ts b/src/server/api/outbox.test.ts new file mode 100644 index 0000000..eace3ad --- /dev/null +++ b/src/server/api/outbox.test.ts @@ -0,0 +1,108 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' +import { APActivity } from 'activitypub-types' + +interface TestContext { + server: FastifyTypebox + hasPermissionActorRequestStub: sinon.SinonStub +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasPermissionActorRequestStub.restore() +}) + +// Test for POST /:actor/outbox +test.serial('POST /:actor/outbox - success', async t => { + const actor = 'testActor' + const activity: APActivity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Create', + actor: `http://localhost:3000/v1/${actor}`, + object: { + type: 'Note', + content: 'Hello world!' + } + } + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/outbox`, + payload: activity, + headers: { + 'Content-Type': 'application/json' + } + }) + + const responseBody = JSON.parse(response.body) + t.is(response.statusCode, 200, 'returns a status code of 200') + t.deepEqual(responseBody, { message: 'ok' }, 'returns success message') +}) + +test.serial('POST /:actor/outbox - not allowed', async t => { + const actor = 'testActor' + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/outbox`, + payload: {}, + headers: { + 'Content-Type': 'application/json' + } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Test for GET /:actor/outbox/:id +test.serial('GET /:actor/outbox/:id - success', async t => { + const actor = 'testActor' + const itemId = 'testItemId' + t.context.hasPermissionActorRequestStub.resolves(true) + + const activity: APActivity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Create', + id: `http://localhost:3000/v1/${actor}/outbox/${itemId}`, + actor: `http://localhost:3000/v1/${actor}`, + object: { + type: 'Note', + content: 'Hello world!' + } + } + + sinon.stub(ActivityPubSystem.prototype, 'getOutboxItem').withArgs(actor, itemId).resolves(activity) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/outbox/${itemId}` + }) + + const responseBody = JSON.parse(response.body) + t.is(response.statusCode, 200, 'returns a status code of 200') + t.deepEqual(responseBody, activity, 'returns the expected outbox item') +}) + +test.serial('GET /:actor/outbox/:id - not allowed', async t => { + const actor = 'testActor' + const itemId = 'testItemId' + t.context.hasPermissionActorRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/outbox/${itemId}` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) diff --git a/src/server/api/outbox.ts b/src/server/api/outbox.ts index 4d0e441..0ab8e22 100644 --- a/src/server/api/outbox.ts +++ b/src/server/api/outbox.ts @@ -39,7 +39,7 @@ export const outboxRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPub // TODO: logic for notifying specific followers of replies await apsystem.notifyFollowers(actor, activity) - return await reply.send({ message: 'ok' }) + return await reply.code(200).type('application/json').send(JSON.stringify({ message: 'ok' })) }) server.get<{ Params: { @@ -69,7 +69,6 @@ export const outboxRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPub } const activity = await apsystem.getOutboxItem(actor, id) - - return await reply.send(activity) + return await reply.code(200).type('application/json').send(JSON.stringify(activity)) }) } From 1f345bb2f57d30e635290112b3223183a3fd243f Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:39:29 +0530 Subject: [PATCH 08/12] test: add tests for hooks APIs --- src/server/api/hooks.test.ts | 312 +++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 src/server/api/hooks.test.ts diff --git a/src/server/api/hooks.test.ts b/src/server/api/hooks.test.ts new file mode 100644 index 0000000..9e06b79 --- /dev/null +++ b/src/server/api/hooks.test.ts @@ -0,0 +1,312 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' + +interface TestContext { + server: FastifyTypebox + hasPermissionActorRequestStub: sinon.SinonStub + mockStore: any +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + + // Set up the mockStore and other required stubs + t.context.mockStore = { + forActor: sinon.stub().returns({ + hooks: { + setModerationQueued: sinon.stub().resolves(), + getModerationQueued: sinon.stub().resolves(), + deleteModerationQueued: sinon.stub().resolves(), + setOnApproved: sinon.stub().resolves(), + getOnApproved: sinon.stub().resolves(), + deleteOnApproved: sinon.stub().resolves(), + setOnRejected: sinon.stub().resolves(), + getOnRejected: sinon.stub().resolves(), + deleteOnRejected: sinon.stub().resolves() + } + }) + } + + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) + // Mock setting hooks for onapproved and onrejected + t.context.mockStore.forActor('testActor').hooks.setOnApproved.resolves() + t.context.mockStore.forActor('testActor').hooks.setOnRejected.resolves() + + // Set hooks before each test + await t.context.server.inject({ + method: 'PUT', + url: '/v1/testActor/hooks/onapproved', + payload: onApprovedHookData, + headers: { 'Content-Type': 'application/json' } + }) + await t.context.server.inject({ + method: 'PUT', + url: '/v1/testActor/hooks/onrejected', + payload: onRejectedHookData, + headers: { 'Content-Type': 'application/json' } + }) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasPermissionActorRequestStub.restore() +}) + +const moderationQueuedHookData = { + url: 'https://example.com/moderationqueuedhook', + method: 'POST', + headers: { 'Content-Type': 'application/json' } +} + +const onApprovedHookData = { + url: 'https://example.com/onapprovedhook', + method: 'POST', + headers: { 'Content-Type': 'application/json' } +} + +const onRejectedHookData = { + url: 'https://example.com/onrejectedhook', + method: 'POST', + headers: { 'Content-Type': 'application/json' } +} + +// ModerationQueued success +test.serial('PUT /:actor/hooks/moderationqueued - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/moderationqueued`, + payload: moderationQueuedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 200, 'Hook set successfully') +}) + +test.serial('GET /:actor/hooks/moderationqueued - not found', async t => { + const actor = 'testActor' + + // Mock getModerationQueued to return null (hook not found) + t.context.mockStore.forActor(actor).hooks.getModerationQueued.resolves(null) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/moderationqueued` + }) + + t.is(response.statusCode, 404, 'returns a status code of 404') +}) + +test.serial('DELETE /:actor/hooks/moderationqueued - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/moderationqueued` + }) + + t.is(response.statusCode, 200, 'Hook deleted successfully') +}) + +// OnApprovedHook success +test.serial('PUT /:actor/hooks/onapproved - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/onapproved`, + payload: onApprovedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 200, 'Hook set successfully') +}) + +test.serial('GET /:actor/hooks/onapproved - success', async t => { + const actor = 'testActor' + + // Ensure the mockStore returns the onApprovedHookData + t.context.mockStore.forActor(actor).hooks.getOnApproved.resolves(onApprovedHookData) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/onapproved` + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') + t.deepEqual(JSON.parse(response.body), onApprovedHookData, 'returns the expected hook data') +}) + +test.serial('DELETE /:actor/hooks/onapproved - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/onapproved` + }) + + t.is(response.statusCode, 200, 'Hook deleted successfully') +}) + +// OnRejectedHook success +test.serial('PUT /:actor/hooks/onrejected - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/onrejected`, + payload: onRejectedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 200, 'Hook set successfully') +}) + +test.serial('GET /:actor/hooks/onrejected - success', async t => { + const actor = 'testActor' + + // Ensure the mockStore returns the onRejectedHookData + t.context.mockStore.forActor(actor).hooks.getOnRejected.resolves(onRejectedHookData) + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/onrejected` + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') + t.deepEqual(JSON.parse(response.body), onRejectedHookData, 'returns the expected hook data') +}) + +test.serial('DELETE /:actor/hooks/onrejected - success', async t => { + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/onrejected` + }) + + t.is(response.statusCode, 200, 'Hook deleted successfully') +}) + +// Negative cases for ModerationQueued Hook +test.serial('PUT /:actor/hooks/moderationqueued - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/moderationqueued`, + payload: moderationQueuedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('GET /:actor/hooks/moderationqueued - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/moderationqueued` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/hooks/moderationqueued - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/moderationqueued` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Negative cases for OnApprovedHook +test.serial('PUT /:actor/hooks/onapproved - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/onapproved`, + payload: onApprovedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('GET /:actor/hooks/onapproved - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/onapproved` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/hooks/onapproved - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/onapproved` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Negative cases for OnRejectedHook +test.serial('PUT /:actor/hooks/onrejected - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'PUT', + url: `/v1/${actor}/hooks/onrejected`, + payload: onRejectedHookData, + headers: { 'Content-Type': 'application/json' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('GET /:actor/hooks/onrejected - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/hooks/onrejected` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/hooks/onrejected - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/hooks/onrejected` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) From 7e34a685b8ace4c5aa84502830b87cc32678e197 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 9 Jan 2024 01:41:36 +0530 Subject: [PATCH 09/12] test: add tests for blockallowlist APIs --- src/server/api/blockallowlist.test.ts | 399 ++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 src/server/api/blockallowlist.test.ts diff --git a/src/server/api/blockallowlist.test.ts b/src/server/api/blockallowlist.test.ts new file mode 100644 index 0000000..c0a15d3 --- /dev/null +++ b/src/server/api/blockallowlist.test.ts @@ -0,0 +1,399 @@ +import anyTest, { TestFn } from 'ava' +import sinon from 'sinon' +import { spawnTestServer } from '../fixtures/spawnServer.js' +import { FastifyTypebox } from './index.js' +import ActivityPubSystem from '../apsystem.js' + +interface TestContext { + server: FastifyTypebox + hasAdminPermissionForRequestStub: sinon.SinonStub + hasPermissionActorRequestStub: sinon.SinonStub + mockStore: any +} + +const test = anyTest as TestFn + +test.beforeEach(async t => { + t.context.server = await spawnTestServer() + + // Set up the mockStore + t.context.mockStore = { + blocklist: { + list: sinon.stub(), + add: sinon.stub().resolves(), + remove: sinon.stub().resolves() + }, + allowlist: { + list: sinon.stub(), + add: sinon.stub().resolves(), + remove: sinon.stub().resolves() + }, + forActor: sinon.stub().callsFake((actor) => ({ + blocklist: { + list: sinon.stub(), + add: sinon.stub().resolves(), + remove: sinon.stub().resolves() + }, + allowlist: { + list: sinon.stub(), + add: sinon.stub().resolves(), + remove: sinon.stub().resolves() + } + })) + } + + // Setup mock responses + t.context.mockStore.blocklist.list.resolves(['blocked@example.com']) + t.context.mockStore.allowlist.list.resolves(['allowed@example.com']) + t.context.mockStore.forActor('testActor').blocklist.list.resolves(['user1@example.com', 'user2@example.com']) + t.context.mockStore.forActor('testActor').allowlist.list.resolves(['user5@example.com', 'user6@example.com']) + + t.context.hasAdminPermissionForRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasAdminPermissionForRequest').resolves(true) + t.context.hasPermissionActorRequestStub = sinon.stub(ActivityPubSystem.prototype, 'hasPermissionActorRequest').resolves(true) +}) + +test.afterEach.always(async t => { + await t.context.server?.close() + t.context.hasAdminPermissionForRequestStub.restore() + t.context.hasPermissionActorRequestStub.restore() +}) + +// Global Blocklist Tests +// test.serial('GET /blocklist - success', async t => { +// const response = await t.context.server.inject({ +// method: 'GET', +// url: '/v1/blocklist' +// }) + +// console.log(response.statusCode) +// console.log(response.body) + +// t.is(response.statusCode, 200, 'returns a status code of 200') +// t.is(response.body, 'blocked@example.com', 'returns the blocklist') +// }) + +test.serial('POST /blocklist - success', async t => { + const blocklistData = 'block@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/blocklist', + payload: blocklistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('DELETE /blocklist - success', async t => { + const blocklistData = 'unblock@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/blocklist', + payload: blocklistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +// Global Allowlist Tests +// test.serial('GET /allowlist - success', async t => { +// const response = await t.context.server.inject({ +// method: 'GET', +// url: '/v1/allowlist' +// }) + +// console.log(response.statusCode) +// console.log(response.body) + +// t.is(response.statusCode, 200, 'returns a status code of 200') +// t.is(response.body, 'allowed@example.com', 'returns the allowlist') +// }) + +test.serial('POST /allowlist - success', async t => { + const allowlistData = 'allow@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/allowlist', + payload: allowlistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('DELETE /allowlist - success', async t => { + const allowlistData = 'disallow@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/allowlist', + payload: allowlistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +// Negative cases for Global Blocklist +test.serial('GET /v1/blocklist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/blocklist' + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('POST /v1/blocklist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + const blocklistData = 'block@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/blocklist', + payload: blocklistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /v1/blocklist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + const blocklistData = 'unblock@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/blocklist', + payload: blocklistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Negative cases for Global Allowlist +test.serial('GET /v1/allowlist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/allowlist' + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('POST /v1/allowlist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + const allowlistData = 'allow@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: '/v1/allowlist', + payload: allowlistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /v1/allowlist - not allowed', async t => { + t.context.hasAdminPermissionForRequestStub.resolves(false) + const allowlistData = 'disallow@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: '/v1/allowlist', + payload: allowlistData, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Actor-specific Blocklist Tests +// test.serial('GET /:actor/blocklist - success', async t => { +// const actor = 'testActor' +// const blockedAccounts = ['user1@example.com', 'user2@example.com'] + +// t.context.mockStore.forActor(actor).blocklist.list.resolves(blockedAccounts) + +// const response = await t.context.server.inject({ +// method: 'GET', +// url: `/v1/${actor}/blocklist` +// }) + +// console.log(response.statusCode) +// console.log(response.body) + +// t.is(response.statusCode, 200, 'returns a status code of 200') +// t.deepEqual(response.body.split('\n'), blockedAccounts, 'returns the correct blocklist') +// }) + +test.serial('POST /:actor/blocklist - success', async t => { + const actor = 'testActor' + const accountsToAdd = ['user3@example.com', 'user4@example.com'].join('\n') + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/blocklist`, + payload: accountsToAdd, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('DELETE /:actor/blocklist - success', async t => { + const actor = 'testActor' + const accountsToRemove = ['user3@example.com', 'user4@example.com'].join('\n') + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/blocklist`, + payload: accountsToRemove, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +// Actor-specific Allowlist Tests +// test.serial('GET /:actor/allowlist - success', async t => { +// const actor = 'testActor' +// const allowedAccounts = ['user5@example.com', 'user6@example.com'] + +// t.context.mockStore.forActor(actor).allowlist.list.resolves(allowedAccounts) + +// const response = await t.context.server.inject({ +// method: 'GET', +// url: `/v1/${actor}/allowlist` +// }) + +// console.log(response.statusCode) +// console.log(response.body) + +// t.is(response.statusCode, 200, 'returns a status code of 200') +// t.deepEqual(response.body.split('\n'), allowedAccounts, 'returns the correct allowlist') +// }) + +test.serial('POST /:actor/allowlist - success', async t => { + const actor = 'testActor' + const accountsToAdd = ['user7@example.com', 'user8@example.com'].join('\n') + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/allowlist`, + payload: accountsToAdd, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +test.serial('DELETE /:actor/allowlist - success', async t => { + const actor = 'testActor' + const accountsToRemove = ['user7@example.com', 'user8@example.com'].join('\n') + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/allowlist`, + payload: accountsToRemove, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 200, 'returns a status code of 200') +}) + +// Negative cases for /:actor/blocklist +test.serial('GET /:actor/blocklist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/blocklist` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('POST /:actor/blocklist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + const accountsToAdd = 'user9@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/blocklist`, + payload: accountsToAdd, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/blocklist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + const accountsToRemove = 'user9@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/blocklist`, + payload: accountsToRemove, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +// Negative cases for /:actor/allowlist +test.serial('GET /:actor/allowlist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + + const response = await t.context.server.inject({ + method: 'GET', + url: `/v1/${actor}/allowlist` + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('POST /:actor/allowlist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + const accountsToAdd = 'user10@example.com' + + const response = await t.context.server.inject({ + method: 'POST', + url: `/v1/${actor}/allowlist`, + payload: accountsToAdd, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) + +test.serial('DELETE /:actor/allowlist - not allowed', async t => { + t.context.hasPermissionActorRequestStub.resolves(false) + const actor = 'testActor' + const accountsToRemove = 'user10@example.com' + + const response = await t.context.server.inject({ + method: 'DELETE', + url: `/v1/${actor}/allowlist`, + payload: accountsToRemove, + headers: { 'Content-Type': 'text/plain' } + }) + + t.is(response.statusCode, 403, 'returns a status code of 403') +}) From 16de91f0e5569d878abda5f51483e45b01aef9d1 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 10 Jan 2024 14:16:09 -0500 Subject: [PATCH 10/12] Use same node version as mauve for tests --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 76e4a2d..4548e57 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [19.9.x] steps: - uses: actions/checkout@v3 From d8119ecbccbb4762ec78f4e42af0c6355272f900 Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Fri, 12 Jan 2024 17:57:47 +0530 Subject: [PATCH 11/12] feat: validate admin list updates in admins.test.ts --- src/server/api/admins.test.ts | 39 +++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/server/api/admins.test.ts b/src/server/api/admins.test.ts index c110505..3d0c34d 100644 --- a/src/server/api/admins.test.ts +++ b/src/server/api/admins.test.ts @@ -46,16 +46,22 @@ test.serial('GET /admins - not allowed', async t => { test.serial('POST /admins - add admins', async t => { t.context.hasAdminPermissionForRequestStub.resolves(true) - const response = await t.context.server.inject({ + // Add a new admin + await t.context.server.inject({ method: 'POST', url: '/v1/admins', payload: 'newadmin@example.com', - headers: { - 'Content-Type': 'text/plain' - } + headers: { 'Content-Type': 'text/plain' } }) - t.is(response.statusCode, 200, 'returns a status code of 200') + // Fetch the list of admins to verify the addition + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/admins' + }) + + t.is(response.statusCode, 200, 'returns a status code of 200 after adding admin') + t.deepEqual(response.body.split('\n'), ['newadmin@example.com'], 'Admin list should contain only the new admin') }) test.serial('POST /admins - not allowed', async t => { @@ -65,9 +71,7 @@ test.serial('POST /admins - not allowed', async t => { method: 'POST', url: '/v1/admins', payload: 'newadmin@example.com', - headers: { - 'Content-Type': 'text/plain' - } + headers: { 'Content-Type': 'text/plain' } }) t.is(response.statusCode, 403, 'returns a status code of 403') @@ -76,16 +80,25 @@ test.serial('POST /admins - not allowed', async t => { test.serial('DELETE /admins - remove admins', async t => { t.context.hasAdminPermissionForRequestStub.resolves(true) - const response = await t.context.server.inject({ + // Remove an admin + await t.context.server.inject({ method: 'DELETE', url: '/v1/admins', payload: 'removeadmin@example.com', - headers: { - 'Content-Type': 'text/plain' - } + headers: { 'Content-Type': 'text/plain' } }) - t.is(response.statusCode, 200, 'returns a status code of 200') + // Fetch the list of admins to verify the removal + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/admins' + }) + + t.is(response.statusCode, 200, 'returns a status code of 200 after removing admin') + + // Filter out empty strings from the response body + const adminList = response.body.split('\n').filter(admin => admin.trim() !== '') + t.deepEqual(adminList, [], 'Admin list should be empty after removal') }) test.serial('DELETE /admins - not allowed', async t => { From c4a65934030d308e5602d8ff3601df07f7a5eeaf Mon Sep 17 00:00:00 2001 From: Akhilesh Thite Date: Tue, 23 Jan 2024 01:47:27 +0530 Subject: [PATCH 12/12] test: ensure blocklist and allowlist routes correctly handle and return data --- src/server/api/blockallowlist.test.ts | 70 ++++++++++++++++----------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/server/api/blockallowlist.test.ts b/src/server/api/blockallowlist.test.ts index c0a15d3..7d18e31 100644 --- a/src/server/api/blockallowlist.test.ts +++ b/src/server/api/blockallowlist.test.ts @@ -30,12 +30,12 @@ test.beforeEach(async t => { }, forActor: sinon.stub().callsFake((actor) => ({ blocklist: { - list: sinon.stub(), + list: sinon.stub().resolves([]), add: sinon.stub().resolves(), remove: sinon.stub().resolves() }, allowlist: { - list: sinon.stub(), + list: sinon.stub().resolves([]), add: sinon.stub().resolves(), remove: sinon.stub().resolves() } @@ -56,21 +56,31 @@ test.afterEach.always(async t => { await t.context.server?.close() t.context.hasAdminPermissionForRequestStub.restore() t.context.hasPermissionActorRequestStub.restore() + + // Reset the mock store for blocklist and allowlist + t.context.mockStore.blocklist.list.reset() + t.context.mockStore.allowlist.list.reset() }) // Global Blocklist Tests -// test.serial('GET /blocklist - success', async t => { -// const response = await t.context.server.inject({ -// method: 'GET', -// url: '/v1/blocklist' -// }) +test.serial('GET /blocklist - success', async t => { + // Add a new account to blocklist + await t.context.server.inject({ + method: 'POST', + url: '/v1/blocklist', + payload: 'blocked@example.com', + headers: { 'Content-Type': 'text/plain' } + }) -// console.log(response.statusCode) -// console.log(response.body) + // Fetch the updated list of blocked accounts + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/blocklist' + }) -// t.is(response.statusCode, 200, 'returns a status code of 200') -// t.is(response.body, 'blocked@example.com', 'returns the blocklist') -// }) + t.is(response.statusCode, 200, 'returns a status code of 200') + t.is(response.body, 'blocked@example.com', 'returns the blocklist') +}) test.serial('POST /blocklist - success', async t => { const blocklistData = 'block@example.com' @@ -99,18 +109,24 @@ test.serial('DELETE /blocklist - success', async t => { }) // Global Allowlist Tests -// test.serial('GET /allowlist - success', async t => { -// const response = await t.context.server.inject({ -// method: 'GET', -// url: '/v1/allowlist' -// }) +test.serial('GET /allowlist - success', async t => { + // Add a new account to allowlist + await t.context.server.inject({ + method: 'POST', + url: '/v1/allowlist', + payload: 'allowed@example.com', + headers: { 'Content-Type': 'text/plain' } + }) -// console.log(response.statusCode) -// console.log(response.body) + // Fetch the updated list of allowed accounts + const response = await t.context.server.inject({ + method: 'GET', + url: '/v1/allowlist' + }) -// t.is(response.statusCode, 200, 'returns a status code of 200') -// t.is(response.body, 'allowed@example.com', 'returns the allowlist') -// }) + t.is(response.statusCode, 200, 'returns a status code of 200') + t.is(response.body, 'allowed@example.com', 'returns the allowlist') +}) test.serial('POST /allowlist - success', async t => { const allowlistData = 'allow@example.com' @@ -218,11 +234,12 @@ test.serial('DELETE /v1/allowlist - not allowed', async t => { t.is(response.statusCode, 403, 'returns a status code of 403') }) -// Actor-specific Blocklist Tests +// Actor-specific Blocklist Test // test.serial('GET /:actor/blocklist - success', async t => { // const actor = 'testActor' // const blockedAccounts = ['user1@example.com', 'user2@example.com'] +// // Ensure the mockStore returns the blocked accounts // t.context.mockStore.forActor(actor).blocklist.list.resolves(blockedAccounts) // const response = await t.context.server.inject({ @@ -230,8 +247,7 @@ test.serial('DELETE /v1/allowlist - not allowed', async t => { // url: `/v1/${actor}/blocklist` // }) -// console.log(response.statusCode) -// console.log(response.body) +// console.log(response.statusCode, response.body) // t.is(response.statusCode, 200, 'returns a status code of 200') // t.deepEqual(response.body.split('\n'), blockedAccounts, 'returns the correct blocklist') @@ -270,6 +286,7 @@ test.serial('DELETE /:actor/blocklist - success', async t => { // const actor = 'testActor' // const allowedAccounts = ['user5@example.com', 'user6@example.com'] +// // Ensure the mockStore returns the allowed accounts // t.context.mockStore.forActor(actor).allowlist.list.resolves(allowedAccounts) // const response = await t.context.server.inject({ @@ -277,8 +294,7 @@ test.serial('DELETE /:actor/blocklist - success', async t => { // url: `/v1/${actor}/allowlist` // }) -// console.log(response.statusCode) -// console.log(response.body) +// console.log(response.statusCode, response.body) // t.is(response.statusCode, 200, 'returns a status code of 200') // t.deepEqual(response.body.split('\n'), allowedAccounts, 'returns the correct allowlist')