test: activitypub third-party blocklists

This commit is contained in:
Julian Lam
2026-04-13 12:54:50 -04:00
parent 5709a7b317
commit 30774a8424
3 changed files with 315 additions and 11 deletions

3
.gitignore vendored
View File

@@ -72,4 +72,5 @@ link-plugins.sh
test.sh
.docker/**
!**/.gitkeep
!**/.gitkeep
.aider*

View File

@@ -6,9 +6,9 @@ const db = require('../database');
const request = require('../request');
const activitypub = module.parent.exports;
const Blocklists = module.exports;
const blocklists = module.exports;
Blocklists.list = async () => {
blocklists.list = async () => {
const blocklists = await db.getSortedSetMembers('blocklists');
const counts = await db.sortedSetsCard(blocklists.map(blocklist => `blocklist:${blocklist}`));
@@ -17,7 +17,7 @@ Blocklists.list = async () => {
});
};
Blocklists.get = async (url) => {
blocklists.get = async (url) => {
const domains = await db.getSortedSetMembers(`blocklist:${url}`);
return {
@@ -26,23 +26,23 @@ Blocklists.get = async (url) => {
};
};
Blocklists.add = async (url) => {
blocklists.add = async (url) => {
const now = Date.now();
await Promise.all([
db.sortedSetAdd('blocklists', now, url),
Blocklists.refresh(url),
blocklists.refresh(url),
]);
};
Blocklists.remove = async (url) => {
blocklists.remove = async (url) => {
await Promise.all([
db.sortedSetRemove('blocklists', url),
db.delete(`blocklist:${url}`),
]);
};
Blocklists.refresh = async (url) => {
blocklists.refresh = async (url) => {
activitypub.helpers.log(`[blocklists/refresh] Processing ${url}`);
const { body: csv } = await request.get(url);
@@ -69,10 +69,10 @@ Blocklists.refresh = async (url) => {
return records.length;
};
Blocklists.check = async (domain) => {
const blocklists = await Blocklists.list();
blocklists.check = async (domain) => {
const blocklists = await blocklists.list();
let present = await db.isMemberOfSortedSets(blocklists.map(({ url }) => `blocklist:${url}`), domain);
present = present.reduce((memo, present) => memo || present, false);
return !present;
};
};

View File

@@ -0,0 +1,303 @@
'use strict';
const assert = require('assert');
const nconf = require('nconf');
const path = require('path');
const db = require('../mocks/databasemock');
const request = require('../../src/request');
const activitypub = require('../../src/activitypub');
describe('ActivityPub blocklists', () => {
before(async () => {
meta.config.activitypubEnabled = 1;
meta.config.activitypubAllowLoopback = 1;
await install.giveWorldPrivileges();
});
after(() => {
delete meta.config.activitypubEnabled;
});
describe('blocklists.list()', () => {
it('should return an empty list when no blocklists exist', async () => {
const result = await activitypub.blocklists.list();
assert(Array.isArray(result));
assert.strictEqual(result.length, 0);
});
it('should return blocklists with their counts', async () => {
const url1 = 'https://example.com/blocklist1.csv';
const url2 = 'https://example.com/blocklist2.csv';
await activitypub.blocklists.add(url1);
await activitypub.blocklists.add(url2);
const result = await activitypub.blocklists.list();
assert.strictEqual(result.length, 2);
assert(result.some(r => r.url === url1));
assert(result.some(r => r.url === url2));
});
it('should return blocklists sorted by timestamp', async () => {
const url1 = 'https://example.com/blocklist1.csv';
const url2 = 'https://example.com/blocklist2.csv';
await activitypub.blocklists.add(url1);
await new Promise(resolve => setTimeout(resolve, 10));
await activitypub.blocklists.add(url2);
const result = await activitypub.blocklists.list();
assert.strictEqual(result[0].url, url1);
assert.strictEqual(result[1].url, url2);
});
});
describe('blocklists.get()', () => {
it('should return empty domains when blocklist does not exist', async () => {
const result = await activitypub.blocklists.get('https://nonexistent.com/blocklist.csv');
assert.strictEqual(result.domains.length, 0);
assert.strictEqual(result.count, 0);
});
it('should return domains from an existing blocklist', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const result = await activitypub.blocklists.get(url);
assert.strictEqual(result.count, 0);
assert.strictEqual(result.domains.length, 0);
});
it('should return domains after refresh', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
// Mock the CSV data
const csvData = '#domain,#severity\nexample.com,1\nexample.org,1';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
await activitypub.blocklists.refresh(url);
const result = await activitypub.blocklists.get(url);
assert.strictEqual(result.count, 2);
assert(result.domains.includes('example.com'));
assert(result.domains.includes('example.org'));
});
});
describe('blocklists.add()', () => {
it('should add a new blocklist', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const result = await activitypub.blocklists.list();
assert(result.some(r => r.url === url));
});
it('should refresh the blocklist after adding', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
// Verify the blocklist was added and refreshed
const result = await activitypub.blocklists.get(url);
assert.strictEqual(result.count, 0); // Empty initially
});
});
describe('blocklists.remove()', () => {
it('should remove a blocklist', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
await activitypub.blocklists.remove(url);
const result = await activitypub.blocklists.list();
assert(!result.some(r => r.url === url));
});
it('should delete the blocklist data', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
await activitypub.blocklists.refresh(url);
await activitypub.blocklists.remove(url);
const result = await activitypub.blocklists.get(url);
assert.strictEqual(result.count, 0);
});
});
describe('blocklists.refresh()', () => {
it('should process a valid CSV', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const csvData = '#domain,#severity\nexample.com,1\nexample.org,2\nsilence.example.com,2';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
const result = await activitypub.blocklists.refresh(url);
assert.strictEqual(result, 3);
});
it('should return 0 for empty CSV', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const csvData = '';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
const result = await activitypub.blocklists.refresh(url);
assert.strictEqual(result, 0);
});
it('should return 0 on parse error', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const csvData = 'invalid,csv,data';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
const result = await activitypub.blocklists.refresh(url);
assert.strictEqual(result, 0);
});
it('should handle severity levels correctly', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const csvData = '#domain,#severity\nexample.com,1\nsilence.example.com,2';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
await activitypub.blocklists.refresh(url);
const result = await activitypub.blocklists.get(url);
assert.strictEqual(result.count, 2);
});
});
describe('blocklists.check()', () => {
it('should return true when domain is not blocked', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const result = await activitypub.blocklists.check('example.com');
assert.strictEqual(result, true);
});
it('should return false when domain is blocked', async () => {
const url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
const csvData = '#domain,#severity\nexample.com,1';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
await activitypub.blocklists.refresh(url);
const result = await activitypub.blocklists.check('example.com');
assert.strictEqual(result, false);
});
it('should return true when domain is not in any blocklist', async () => {
const url1 = 'https://example.com/blocklist1.csv';
const url2 = 'https://example.com/blocklist2.csv';
await activitypub.blocklists.add(url1);
await activitypub.blocklists.add(url2);
const csvData1 = '#domain,#severity\nblocked.com,1';
const csvData2 = '#domain,#severity\nblocked.org,1';
const mockResponse1 = { body: csvData1 };
const mockResponse2 = { body: csvData2 };
request.get = () => mockResponse1;
await activitypub.blocklists.refresh(url1);
request.get = () => mockResponse2;
await activitypub.blocklists.refresh(url2);
const result = await activitypub.blocklists.check('example.com');
assert.strictEqual(result, true);
});
it('should return false when domain is in any blocklist', async () => {
const url1 = 'https://example.com/blocklist1.csv';
const url2 = 'https://example.com/blocklist2.csv';
await activitypub.blocklists.add(url1);
await activitypub.blocklists.add(url2);
const csvData1 = '#domain,#severity\nexample.com,1';
const csvData2 = '#domain,#severity\nblocked.org,1';
const mockResponse1 = { body: csvData1 };
const mockResponse2 = { body: csvData2 };
request.get = () => mockResponse1;
await activitypub.blocklists.refresh(url1);
request.get = () => mockResponse2;
await activitypub.blocklists.refresh(url2);
const result = await activitypub.blocklists.check('example.com');
assert.strictEqual(result, false);
});
});
describe('Integration tests', () => {
let url;
before(async () => {
url = 'https://example.com/blocklist.csv';
await activitypub.blocklists.add(url);
});
after(async () => {
await activitypub.blocklists.remove(url);
});
it('should handle full lifecycle of a blocklist', async () => {
// Add
await activitypub.blocklists.add(url);
const list = await activitypub.blocklists.list();
assert(list.some(r => r.url === url));
// Refresh with data
const csvData = '#domain,#severity\nblocked.com,1\nblocked.org,1';
const mockResponse = { body: csvData };
request.get = () => mockResponse;
await activitypub.blocklists.refresh(url);
// Check
assert.strictEqual(await activitypub.blocklists.check('blocked.com'), false);
assert.strictEqual(await activitypub.blocklists.check('allowed.com'), true);
// Remove
await activitypub.blocklists.remove(url);
// Verify removed
const listAfter = await activitypub.blocklists.list();
assert(!listAfter.some(r => r.url === url));
});
});
});