refactor: split activitypub tests to subfolder files

This commit is contained in:
Julian Lam
2024-04-26 11:30:08 -04:00
parent 5e776088c9
commit 94eafe1df3
6 changed files with 231 additions and 196 deletions

View File

@@ -36,7 +36,7 @@ Actors.assert = async (ids, options = {}) => {
if (id.includes('@') && !(isUri && activitypub._constants.acceptedProtocols.includes(new URL(id).protocol.slice(0, -1)))) { 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]; const host = isUri ? new URL(id).host : id.split('@')[1];
if (host === nconf.get('url_parsed').host) { // do not assert loopback ids if (host === nconf.get('url_parsed').host) { // do not assert loopback ids
return null; return 'loopback';
} }
({ actorUri: id } = await activitypub.helpers.query(id)); ({ actorUri: id } = await activitypub.helpers.query(id));
@@ -53,10 +53,7 @@ Actors.assert = async (ids, options = {}) => {
} }
// Filter out loopback uris // Filter out loopback uris
ids = ids.filter((uri) => { ids = ids.filter(uri => uri !== 'loopback' && new URL(uri).host !== nconf.get('url_parsed').host);
const { host } = new URL(uri);
return host !== nconf.get('url_parsed').host;
});
// Filter out existing // Filter out existing
if (!options.update) { if (!options.update) {

View File

@@ -136,6 +136,7 @@ Helpers.resolveLocalId = async (input) => {
return { type: null, id: null }; return { type: null, id: null };
} else if (String(input).indexOf('@') !== -1) { // Webfinger } else if (String(input).indexOf('@') !== -1) { // Webfinger
input = decodeURIComponent(input);
const [slug] = input.replace(/^acct:/, '').split('@'); const [slug] = input.replace(/^acct:/, '').split('@');
const uid = await user.getUidByUserslug(slug); const uid = await user.getUidByUserslug(slug);
return { type: 'user', id: uid }; return { type: 'user', id: uid };

View File

@@ -228,7 +228,7 @@ Mocks.note = async (post) => {
inReplyTo = utils.isNumber(post.topic.mainPid) ? `${nconf.get('url')}/post/${post.topic.mainPid}` : post.topic.mainPid; 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); to.add(utils.isNumber(post.topic.uid) ? `${nconf.get('url')}/uid/${post.topic.uid}` : post.topic.uid);
} else { // new topic } else { // new topic
name = await topics.getTitleByPid(post.pid); ({ titleRaw: name } = await topics.getTopicFields(post.tid, ['title']));
tag = post.topic.tags.map(tag => ({ tag = post.topic.tags.map(tag => ({
type: 'Hashtag', type: 'Hashtag',
href: `${nconf.get('url')}/tags/${tag.valueEncoded}`, href: `${nconf.get('url')}/tags/${tag.valueEncoded}`,

View File

@@ -1,21 +1,20 @@
'use strict'; 'use strict';
const assert = require('assert'); const assert = require('assert');
const { createHash } = require('crypto');
const nconf = require('nconf'); const nconf = require('nconf');
const path = require('path');
const db = require('./mocks/databasemock'); const db = require('./mocks/databasemock');
const slugify = require('../src/slugify'); const slugify = require('../src/slugify');
const utils = require('../src/utils'); const utils = require('../src/utils');
const request = require('../src/request'); const request = require('../src/request');
const file = require('../src/file');
const install = require('../src/install'); const install = require('../src/install');
const meta = require('../src/meta'); const meta = require('../src/meta');
const user = require('../src/user'); const user = require('../src/user');
const categories = require('../src/categories'); const categories = require('../src/categories');
const topics = require('../src/topics'); const topics = require('../src/topics');
const posts = require('../src/posts');
const privileges = require('../src/privileges');
const activitypub = require('../src/activitypub'); const activitypub = require('../src/activitypub');
describe('ActivityPub integration', () => { describe('ActivityPub integration', () => {
@@ -28,59 +27,6 @@ describe('ActivityPub integration', () => {
delete meta.config.activitypubEnabled; 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('Helpers', () => {
describe('.query()', () => { describe('.query()', () => {
@@ -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('Receipt of ActivityPub events to inboxes (federating IN)', () => {
describe('Create', () => { describe('Create', () => {
describe('Note', () => { describe('Note', () => {
@@ -526,7 +339,7 @@ describe('ActivityPub integration', () => {
({ postData, topicData } = await topics.post({ ({ postData, topicData } = await topics.post({
uid, uid,
cid: category.cid, cid: category.cid,
title: 'Lipsum title', title: 'Lorem "Lipsum" Ipsum',
content: 'Lorem ipsum dolor sit amet', content: 'Lorem ipsum dolor sit amet',
})); }));
}); });
@@ -560,6 +373,10 @@ describe('ActivityPub integration', () => {
it('should return the expected Content-Type header', () => { it('should return the expected Content-Type header', () => {
assert.strictEqual(response.headers['content-type'], 'application/activity+json; charset=utf-8'); 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');
});
}); });
}); });
@@ -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);
});
});
});
}); });

View File

@@ -0,0 +1,143 @@
'use strict';
const assert = require('assert');
const nconf = require('nconf');
const { createHash } = require('crypto');
const user = require('../../src/user');
const utils = require('../../src/utils');
const db = require('../../src/database');
const activitypub = require('../../src/activitypub');
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);
});
});
});

View File

@@ -0,0 +1,63 @@
'use strict';
const assert = require('assert');
const nconf = require('nconf');
const request = require('../../src/request');
const utils = require('../../src/utils');
const user = require('../../src/user');
const slugify = require('../../src/slugify');
const privileges = require('../../src/privileges');
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));
});
});