Skip to content

Commit

Permalink
refactor: split activitypub tests to subfolder files
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlam committed Apr 26, 2024
1 parent 5e77608 commit 94eafe1
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 196 deletions.
7 changes: 2 additions & 5 deletions src/activitypub/actors.js
Expand Up @@ -36,7 +36,7 @@ Actors.assert = async (ids, options = {}) => {
if (id.includes('@') && !(isUri && activitypub._constants.acceptedProtocols.includes(new URL(id).protocol.slice(0, -1)))) {
const host = isUri ? new URL(id).host : id.split('@')[1];
if (host === nconf.get('url_parsed').host) { // do not assert loopback ids
return null;
return 'loopback';
}

({ actorUri: id } = await activitypub.helpers.query(id));
Expand All @@ -53,10 +53,7 @@ Actors.assert = async (ids, options = {}) => {
}

// Filter out loopback uris
ids = ids.filter((uri) => {
const { host } = new URL(uri);
return host !== nconf.get('url_parsed').host;
});
ids = ids.filter(uri => uri !== 'loopback' && new URL(uri).host !== nconf.get('url_parsed').host);

// Filter out existing
if (!options.update) {
Expand Down
1 change: 1 addition & 0 deletions src/activitypub/helpers.js
Expand Up @@ -136,6 +136,7 @@ Helpers.resolveLocalId = async (input) => {

return { type: null, id: null };
} else if (String(input).indexOf('@') !== -1) { // Webfinger
input = decodeURIComponent(input);
const [slug] = input.replace(/^acct:/, '').split('@');
const uid = await user.getUidByUserslug(slug);
return { type: 'user', id: uid };
Expand Down
2 changes: 1 addition & 1 deletion src/activitypub/mocks.js
Expand Up @@ -228,7 +228,7 @@ Mocks.note = async (post) => {
inReplyTo = utils.isNumber(post.topic.mainPid) ? `${nconf.get('url')}/post/${post.topic.mainPid}` : post.topic.mainPid;
to.add(utils.isNumber(post.topic.uid) ? `${nconf.get('url')}/uid/${post.topic.uid}` : post.topic.uid);
} else { // new topic
name = await topics.getTitleByPid(post.pid);
({ titleRaw: name } = await topics.getTopicFields(post.tid, ['title']));
tag = post.topic.tags.map(tag => ({
type: 'Hashtag',
href: `${nconf.get('url')}/tags/${tag.valueEncoded}`,
Expand Down
211 changes: 21 additions & 190 deletions test/activitypub.js
@@ -1,21 +1,20 @@
'use strict';

const assert = require('assert');
const { createHash } = require('crypto');
const nconf = require('nconf');
const path = require('path');

const db = require('./mocks/databasemock');
const slugify = require('../src/slugify');
const utils = require('../src/utils');
const request = require('../src/request');

const file = require('../src/file');
const install = require('../src/install');
const meta = require('../src/meta');
const user = require('../src/user');
const categories = require('../src/categories');
const topics = require('../src/topics');
const posts = require('../src/posts');
const privileges = require('../src/privileges');
const activitypub = require('../src/activitypub');

describe('ActivityPub integration', () => {
Expand All @@ -28,59 +27,6 @@ describe('ActivityPub integration', () => {
delete meta.config.activitypubEnabled;
});

describe('WebFinger endpoint', () => {
let uid;
let slug;
const { host } = nconf.get('url_parsed');

beforeEach(async () => {
slug = slugify(utils.generateUUID().slice(0, 8));
uid = await user.create({ username: slug });
});

it('should return a 404 Not Found if no user exists by that username', async () => {
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3afoobar%40${host}`);

assert(response);
assert.strictEqual(response.statusCode, 404);
});

it('should return a 400 Bad Request if the request is malformed', async () => {
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3afoobar`);

assert(response);
assert.strictEqual(response.statusCode, 400);
});

it('should return 403 Forbidden if the calling user is not allowed to view the user list/profiles', async () => {
await privileges.global.rescind(['groups:view:users'], 'guests');
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3a${slug}%40${host}`);

assert(response);
assert.strictEqual(response.statusCode, 400);
await privileges.global.give(['groups:view:users'], 'guests');
});

it('should return a valid WebFinger response otherwise', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3a${slug}%40${host}`);

assert(response);
assert.strictEqual(response.statusCode, 200);

['subject', 'aliases', 'links'].forEach((prop) => {
assert(body.hasOwnProperty(prop));
assert(body[prop]);
});

assert.strictEqual(body.subject, `acct:${slug}@${host}`);

assert(Array.isArray(body.aliases));
assert([`${nconf.get('url')}/uid/${uid}`, `${nconf.get('url')}/user/${slug}`].every(url => body.aliases.includes(url)));

assert(Array.isArray(body.links));
});
});

describe('Helpers', () => {
describe('.query()', () => {

Expand Down Expand Up @@ -300,139 +246,6 @@ describe('ActivityPub integration', () => {
});
});

describe('http signature signing and verification', () => {
describe('.sign()', () => {
let uid;

before(async () => {
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
});

it('should create a key-pair for a user if the user does not have one already', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', uid);
await activitypub.sign(keyData, endpoint);
const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`);

assert(publicKey);
assert(privateKey);
});

it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', uid);
const { date, digest, signature } = await activitypub.sign(keyData, endpoint);
const dateObj = new Date(date);

assert(signature);
assert(dateObj);
assert.strictEqual(digest, null);
});

it('should also return a digest hash if payload is passed in', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const payload = { foo: 'bar' };
const keyData = await activitypub.getPrivateKey('uid', uid);
const { digest } = await activitypub.sign(keyData, endpoint, payload);
const hash = createHash('sha256');
hash.update(JSON.stringify(payload));
const checksum = hash.digest('base64');

assert(digest);
assert.strictEqual(digest, `sha-256=${checksum}`);
});

it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', 0);
await activitypub.sign(keyData, endpoint);
const { publicKey, privateKey } = await db.getObject(`uid:0:keys`);

assert(publicKey);
assert(privateKey);
});

it('should return headers with an appropriate key id uri', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', uid);
const { signature } = await activitypub.sign(keyData, endpoint);
const [keyId] = signature.split(',');

assert(signature);
assert.strictEqual(keyId, `keyId="${nconf.get('url')}/uid/${uid}#key"`);
});

it('should return the instance key id when uid is 0', async () => {
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', 0);
const { signature } = await activitypub.sign(keyData, endpoint);
const [keyId] = signature.split(',');

assert(signature);
assert.strictEqual(keyId, `keyId="${nconf.get('url')}/actor#key"`);
});
});

describe('.verify()', () => {
let uid;
let username;
const baseUrl = nconf.get('relative_path');
const mockReqBase = {
method: 'GET',
// path: ...
baseUrl,
headers: {
// host: ...
// date: ...
// signature: ...
// digest: ...
},
};

before(async () => {
username = utils.generateUUID().slice(0, 10);
uid = await user.create({ username });
});

it('should return true when the proper signature and relevant headers are passed in', async () => {
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
const path = `/user/${username}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', uid);
const signature = await activitypub.sign(keyData, endpoint);
const { host } = nconf.get('url_parsed');
const req = {
...mockReqBase,
...{
path,
headers: { ...signature, host },
},
};

const verified = await activitypub.verify(req);
assert.strictEqual(verified, true);
});

it('should return true when a digest is also passed in', async () => {
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
const path = `/user/${username}/inbox`;
const keyData = await activitypub.getPrivateKey('uid', uid);
const signature = await activitypub.sign(keyData, endpoint, { foo: 'bar' });
const { host } = nconf.get('url_parsed');
const req = {
...mockReqBase,
...{
method: 'POST',
path,
headers: { ...signature, host },
},
};

const verified = await activitypub.verify(req);
assert.strictEqual(verified, true);
});
});
});

describe('Receipt of ActivityPub events to inboxes (federating IN)', () => {
describe('Create', () => {
describe('Note', () => {
Expand Down Expand Up @@ -526,7 +339,7 @@ describe('ActivityPub integration', () => {
({ postData, topicData } = await topics.post({
uid,
cid: category.cid,
title: 'Lipsum title',
title: 'Lorem "Lipsum" Ipsum',
content: 'Lorem ipsum dolor sit amet',
}));
});
Expand Down Expand Up @@ -560,6 +373,10 @@ describe('ActivityPub integration', () => {
it('should return the expected Content-Type header', () => {
assert.strictEqual(response.headers['content-type'], 'application/activity+json; charset=utf-8');
});

it('Topic title (`name`) should not be escaped', () => {
assert.strictEqual(body.name, 'Lorem "Lipsum" Ipsum');
});
});
});

Expand Down Expand Up @@ -637,4 +454,18 @@ describe('ActivityPub integration', () => {
});
});
});

describe('ActivityPub', async () => {
let files;

before(async () => {
files = await file.walk(path.resolve(__dirname, './activitypub'));
});

it('subfolder tests', () => {
files.forEach((filePath) => {
require(filePath);
});
});
});
});

0 comments on commit 94eafe1

Please sign in to comment.