From b71dd26731fed6b61773c48f5ee6674446d6efb3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 27 Feb 2025 02:06:55 +0000 Subject: [PATCH 1/4] chore: incrementing version number - v4.0.6 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 74801adb49..d04cda689c 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.0.5", + "version": "4.0.6", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 78bbea301d6b617166206f5f5f50cc722caf47d5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 27 Feb 2025 02:06:56 +0000 Subject: [PATCH 2/4] chore: update changelog for v4.0.6 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34402b38c9..273e8de4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +#### v4.0.6 (2025-02-27) + +##### Chores + +* up dbsearch (9e1a0a13) +* add sourceContent to schema (b2cdd5fd) +* up harmony (f94cf5ce) +* up deps (a9563d75) +* up harmony (08014e7c) +* up harmony (8385d4ae) +* up harmony (0a9d28d5) +* up harmony (c920836a) +* up esbuild (2ab6a368) +* up widgets (02a8d9b6) +* up harmony (05bbefd1) +* incrementing version number - v4.0.5 (1792a62b) +* update changelog for v4.0.5 (f84b9fc7) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* match events and parent style (e38d1531) +* merge consecutive share events (9153f8cf) +* **config:** add `acpPluginInstallDisabled` option (#13189) (577eee2f) + +##### Bug Fixes + +* add missing file (04d5edbb) +* #13202, private note assertion failure when cc property is missing (c65e1ebb) +* tests for public and private note assertion, failing test for private note assertion with missing cc prop (efb27ce0) +* closes #13206, truncate long usernames (51872d54) +* closes #13207, add localComments (13a13e1d) +* handle multiple types in remote actor payload (d91b80d2) +* don't send validation email for pending emails (07957e82) +* missing db (52b23313) +* #13198, use email from confirmObj (b4dfc48b) +* summary post parse (e9d4c7b9) +* add sourceContent to getPostSummaryByPids (ad92e931) +* #13193, add lang strings (2c830567) +* #13194, dont notify about cid=-1 (b8c8ae09) +* old upgrade script so it doesn't create settings objects (fe980688) +* escape confirm email in acp manage users (9bfa8853) +* add missing await on filter:user.logout (c6b8256f) +* relaxing strict allowedTags configuration for incoming AP content (allowing picture, source, and additional attributes for img) (2ad48f17) + +##### Other Changes + +* fix (ec11b0c2) + +##### Refactors + +* use sortedSetRemoveBulk (349084d8) +* don't make db request if there is no code (53a2be9d) + +##### Tests + +* remove extra .only, add basic tests for public note assertion (64267f7d) + #### v4.0.5 (2025-02-20) ##### Chores From 01be4d790854ba62bce398b42e801a91193176a8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 27 Feb 2025 13:25:57 -0500 Subject: [PATCH 3/4] test: moved AP actor tests to separate actors.js file, added failing test for scheduled topics --- test/activitypub.js | 239 --------------------------- test/activitypub/actors.js | 328 +++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 239 deletions(-) create mode 100644 test/activitypub/actors.js diff --git a/test/activitypub.js b/test/activitypub.js index d05f50e4e6..57942ffe72 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -308,168 +308,6 @@ describe('ActivityPub integration', () => { }); }); - describe('User Actor endpoint', () => { - let uid; - let slug; - - beforeEach(async () => { - slug = slugify(utils.generateUUID().slice(0, 8)); - uid = await user.create({ username: slug }); - }); - - it('should return a valid ActivityPub Actor JSON-LD payload', async () => { - const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - }); - - assert(response); - assert.strictEqual(response.statusCode, 200); - assert(body.hasOwnProperty('@context')); - assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); - - ['id', 'url', 'followers', 'following', 'inbox', 'outbox'].forEach((prop) => { - assert(body.hasOwnProperty(prop)); - assert(body[prop]); - }); - - assert.strictEqual(body.id, `${nconf.get('url')}/uid/${uid}`); - assert.strictEqual(body.type, 'Person'); - }); - - it('should contain a `publicKey` property with a public key', async () => { - const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - }); - - assert(body.hasOwnProperty('publicKey')); - assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); - }); - }); - - describe('Category Actor endpoint', () => { - let cid; - let slug; - let description; - - beforeEach(async () => { - slug = slugify(utils.generateUUID().slice(0, 8)); - description = utils.generateUUID(); - ({ cid } = await categories.create({ - name: slug, - description, - })); - }); - - it('should return a valid ActivityPub Actor JSON-LD payload', async () => { - const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - }); - - assert(response); - assert.strictEqual(response.statusCode, 200); - assert(body.hasOwnProperty('@context')); - assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); - - ['id', 'url', /* 'followers', 'following', */ 'inbox', 'outbox'].forEach((prop) => { - assert(body.hasOwnProperty(prop)); - assert(body[prop]); - }); - - assert.strictEqual(body.id, `${nconf.get('url')}/category/${cid}`); - assert.strictEqual(body.type, 'Group'); - assert.strictEqual(body.summary, description); - assert.deepStrictEqual(body.icon, { - type: 'Image', - mediaType: 'image/png', - url: `${nconf.get('url')}/assets/uploads/category/category-${cid}-icon.png`, - }); - }); - - it('should contain a `publicKey` property with a public key', async () => { - const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - }); - - assert(body.hasOwnProperty('publicKey')); - assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); - }); - - it('should serve the the backgroundImage in `icon` if set', async () => { - const payload = {}; - payload[cid] = { - backgroundImage: `/assets/uploads/files/test.png`, - }; - await categories.update(payload); - - const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - }); - - assert.deepStrictEqual(body.icon, { - type: 'Image', - mediaType: 'image/png', - url: `${nconf.get('url')}/assets/uploads/files/test.png`, - }); - }); - }); - - describe('Instance Actor endpoint', () => { - let response; - let body; - - before(async () => { - ({ response, body } = await request.get(`${nconf.get('url')}/actor`, { - headers: { - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - })); - }); - - it('should respond properly', async () => { - assert(response); - assert.strictEqual(response.statusCode, 200); - }); - - it('should return a valid ActivityPub Actor JSON-LD payload', async () => { - assert(body.hasOwnProperty('@context')); - assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); - - ['id', 'url', 'inbox', 'outbox', 'name', 'preferredUsername'].forEach((prop) => { - assert(body.hasOwnProperty(prop)); - assert(body[prop]); - }); - - assert.strictEqual(body.id, body.url); - assert.strictEqual(body.type, 'Application'); - assert.strictEqual(body.name, meta.config.site_title || 'NodeBB'); - assert.strictEqual(body.preferredUsername, nconf.get('url_parsed').hostname); - }); - - it('should contain a `publicKey` property with a public key', async () => { - assert(body.hasOwnProperty('publicKey')); - assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); - }); - - it('should also have a valid WebFinger response tied to `preferredUsername`', async () => { - const { response, body: body2 } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3a${body.preferredUsername}@${nconf.get('url_parsed').host}`); - - assert.strictEqual(response.statusCode, 200); - assert(body2 && body2.aliases && body2.links); - assert(body2.aliases.includes(nconf.get('url'))); - assert(body2.links.some(item => item.rel === 'self' && item.type === 'application/activity+json' && item.href === `${nconf.get('url')}/actor`)); - }); - }); - describe('Receipt of ActivityPub events to inboxes (federating IN)', () => { describe('Create', () => { describe('Note', () => { @@ -648,83 +486,6 @@ describe('ActivityPub integration', () => { }); }); - describe('Actor asserton', () => { - describe('happy path', () => { - let uid; - let actorUri; - - before(async () => { - uid = utils.generateUUID().slice(0, 8); - actorUri = `https://example.org/user/${uid}`; - activitypub._cache.set(`0;${actorUri}`, { - '@context': 'https://www.w3.org/ns/activitystreams', - id: actorUri, - url: actorUri, - - type: 'Person', - name: 'example', - preferredUsername: 'example', - inbox: `https://example.org/user/${uid}/inbox`, - outbox: `https://example.org/user/${uid}/outbox`, - - publicKey: { - id: `${actorUri}#key`, - owner: actorUri, - publicKeyPem: 'somekey', - }, - }); - }); - - it('should return true if successfully asserted', async () => { - const result = await activitypub.actors.assert([actorUri]); - assert(result && result.length); - }); - - it('should contain a representation of that remote user in the database', async () => { - const exists = await db.exists(`userRemote:${actorUri}`); - assert(exists); - - const userData = await user.getUserData(actorUri); - assert(userData); - assert.strictEqual(userData.uid, actorUri); - }); - - it('should save the actor\'s publicly accessible URL in the hash as well', async () => { - const url = await user.getUserField(actorUri, 'url'); - assert.strictEqual(url, actorUri); - }); - }); - - describe('edge case: loopback handles and uris', () => { - let uid; - const userslug = utils.generateUUID().slice(0, 8); - before(async () => { - uid = await user.create({ username: userslug }); - }); - - it('should return true but not actually assert the handle into the database', async () => { - const handle = `${userslug}@${nconf.get('url_parsed').host}`; - const result = await activitypub.actors.assert([handle]); - assert(result); - - const handleExists = await db.isObjectField('handle:uid', handle); - assert.strictEqual(handleExists, false); - - const userRemoteHashExists = await db.exists(`userRemote:${nconf.get('url')}/uid/${uid}`); - assert.strictEqual(userRemoteHashExists, false); - }); - - it('should return true but not actually assert the uri into the database', async () => { - const uri = `${nconf.get('url')}/uid/${uid}`; - const result = await activitypub.actors.assert([uri]); - assert(result); - - const userRemoteHashExists = await db.exists(`userRemote:${uri}`); - assert.strictEqual(userRemoteHashExists, false); - }); - }); - }); - describe('ActivityPub', async () => { let files; diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js new file mode 100644 index 0000000000..08c109eb19 --- /dev/null +++ b/test/activitypub/actors.js @@ -0,0 +1,328 @@ +'use strict'; + +const assert = require('assert'); +const nconf = require('nconf'); + +const db = require('../mocks/databasemock'); +const meta = require('../../src/meta'); +const categories = require('../../src/categories'); +const user = require('../../src/user'); +const topics = require('../../src/topics'); +const activitypub = require('../../src/activitypub'); +const utils = require('../../src/utils'); +const request = require('../../src/request'); +const slugify = require('../../src/slugify'); + +describe('Actor asserton', () => { + describe('happy path', () => { + let uid; + let actorUri; + + before(async () => { + uid = utils.generateUUID().slice(0, 8); + actorUri = `https://example.org/user/${uid}`; + activitypub._cache.set(`0;${actorUri}`, { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actorUri, + url: actorUri, + + type: 'Person', + name: 'example', + preferredUsername: 'example', + inbox: `https://example.org/user/${uid}/inbox`, + outbox: `https://example.org/user/${uid}/outbox`, + + publicKey: { + id: `${actorUri}#key`, + owner: actorUri, + publicKeyPem: 'somekey', + }, + }); + }); + + it('should return true if successfully asserted', async () => { + const result = await activitypub.actors.assert([actorUri]); + assert(result && result.length); + }); + + it('should contain a representation of that remote user in the database', async () => { + const exists = await db.exists(`userRemote:${actorUri}`); + assert(exists); + + const userData = await user.getUserData(actorUri); + assert(userData); + assert.strictEqual(userData.uid, actorUri); + }); + + it('should save the actor\'s publicly accessible URL in the hash as well', async () => { + const url = await user.getUserField(actorUri, 'url'); + assert.strictEqual(url, actorUri); + }); + }); + + describe('edge case: loopback handles and uris', () => { + let uid; + const userslug = utils.generateUUID().slice(0, 8); + before(async () => { + uid = await user.create({ username: userslug }); + }); + + it('should return true but not actually assert the handle into the database', async () => { + const handle = `${userslug}@${nconf.get('url_parsed').host}`; + const result = await activitypub.actors.assert([handle]); + assert(result); + + const handleExists = await db.isObjectField('handle:uid', handle); + assert.strictEqual(handleExists, false); + + const userRemoteHashExists = await db.exists(`userRemote:${nconf.get('url')}/uid/${uid}`); + assert.strictEqual(userRemoteHashExists, false); + }); + + it('should return true but not actually assert the uri into the database', async () => { + const uri = `${nconf.get('url')}/uid/${uid}`; + const result = await activitypub.actors.assert([uri]); + assert(result); + + const userRemoteHashExists = await db.exists(`userRemote:${uri}`); + assert.strictEqual(userRemoteHashExists, false); + }); + }); +}); + +describe('Controllers', () => { + describe('User Actor endpoint', () => { + let uid; + let slug; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(body.hasOwnProperty('@context')); + assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', 'followers', 'following', 'inbox', 'outbox'].forEach((prop) => { + assert(body.hasOwnProperty(prop)); + assert(body[prop]); + }); + + assert.strictEqual(body.id, `${nconf.get('url')}/uid/${uid}`); + assert.strictEqual(body.type, 'Person'); + }); + + it('should contain a `publicKey` property with a public key', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); + }); + }); + + describe('Category Actor endpoint', () => { + let cid; + let slug; + let description; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + description = utils.generateUUID(); + ({ cid } = await categories.create({ + name: slug, + description, + })); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(body.hasOwnProperty('@context')); + assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', /* 'followers', 'following', */ 'inbox', 'outbox'].forEach((prop) => { + assert(body.hasOwnProperty(prop)); + assert(body[prop]); + }); + + assert.strictEqual(body.id, `${nconf.get('url')}/category/${cid}`); + assert.strictEqual(body.type, 'Group'); + assert.strictEqual(body.summary, description); + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/category/category-${cid}-icon.png`, + }); + }); + + it('should contain a `publicKey` property with a public key', async () => { + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); + }); + + it('should serve the the backgroundImage in `icon` if set', async () => { + const payload = {}; + payload[cid] = { + backgroundImage: `/assets/uploads/files/test.png`, + }; + await categories.update(payload); + + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/files/test.png`, + }); + }); + }); + + describe('Instance Actor endpoint', () => { + let response; + let body; + + before(async () => { + ({ response, body } = await request.get(`${nconf.get('url')}/actor`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + })); + }); + + it('should respond properly', async () => { + assert(response); + assert.strictEqual(response.statusCode, 200); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + assert(body.hasOwnProperty('@context')); + assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', 'inbox', 'outbox', 'name', 'preferredUsername'].forEach((prop) => { + assert(body.hasOwnProperty(prop)); + assert(body[prop]); + }); + + assert.strictEqual(body.id, body.url); + assert.strictEqual(body.type, 'Application'); + assert.strictEqual(body.name, meta.config.site_title || 'NodeBB'); + assert.strictEqual(body.preferredUsername, nconf.get('url_parsed').hostname); + }); + + it('should contain a `publicKey` property with a public key', async () => { + assert(body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); + }); + + it('should also have a valid WebFinger response tied to `preferredUsername`', async () => { + const { response, body: body2 } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct%3a${body.preferredUsername}@${nconf.get('url_parsed').host}`); + + assert.strictEqual(response.statusCode, 200); + assert(body2 && body2.aliases && body2.links); + assert(body2.aliases.includes(nconf.get('url'))); + assert(body2.links.some(item => item.rel === 'self' && item.type === 'application/activity+json' && item.href === `${nconf.get('url')}/actor`)); + }); + }); + + describe.only('Topic', () => { + let cid; + let uid; + + before(async () => { + ({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); + const slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + describe('Live', () => { + let topicData; + let postData; + let response; + let body; + + before(async () => { + ({ topicData, postData } = await topics.post({ + uid, + cid, + title: 'Lorem "Lipsum" Ipsum', + content: 'Lorem ipsum dolor sit amet', + })); + + ({ response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + })); + }); + + it('should respond properly', async () => { + assert(response); + assert.strictEqual(response.statusCode, 200); + }); + + it('should return an OrderedCollection with one item', () => { + assert.strictEqual(body.type, 'OrderedCollection'); + assert.strictEqual(body.totalItems, 1); + assert(Array.isArray(body.orderedItems)); + assert.strictEqual(body.orderedItems[0], `${nconf.get('url')}/post/${topicData.mainPid}`); + }); + }); + + describe('Scheduled', () => { + let topicData; + let postData; + let response; + let body; + + before(async () => { + ({ topicData, postData } = await topics.post({ + uid, + cid, + title: 'Lorem "Lipsum" Ipsum', + content: 'Lorem ipsum dolor sit amet', + timestamp: Date.now() + (1000 * 60 * 60), // 1 hour in the future + })); + + ({ response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + })); + }); + + it('should respond with a 404 Not Found', async () => { + assert(response); + assert.strictEqual(response.statusCode, 404); + }); + }); + }); +}); From 428300de4f917865d45f415d4fcafbd3836d75a1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 27 Feb 2025 13:34:11 -0500 Subject: [PATCH 4/4] fix: scheduled topics and posts should return 404 on AP request --- src/controllers/activitypub/actors.js | 14 +++-- test/activitypub/actors.js | 77 +++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index c80f6e2867..8135fdcc73 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -55,12 +55,12 @@ Actors.userBySlug = async function (req, res) { Actors.user(req, res); }; -Actors.note = async function (req, res) { +Actors.note = async function (req, res, next) { // technically a note isn't an actor, but it is here purely for organizational purposes. // but also, wouldn't it be wild if you could follow a note? lol. const allowed = await privileges.posts.can('topics:read', req.params.pid, activitypub._constants.uid); if (!allowed) { - return res.sendStatus(404); + return next(); } // Handle requests for remote content @@ -72,8 +72,8 @@ Actors.note = async function (req, res) { parse: false, extraFields: ['edited'], })).pop(); - if (!post) { - return res.sendStatus(404); + if (!post || post.timestamp > Date.now()) { + return next(); } const payload = await activitypub.mocks.notes.public(post); @@ -120,8 +120,12 @@ Actors.topic = async function (req, res, next) { const page = parseInt(req.query.page, 10) || undefined; const perPage = meta.config.postsPerPage; - const { cid, titleRaw: name, mainPid, slug } = await topics.getTopicFields(req.params.tid, ['cid', 'title', 'mainPid', 'slug']); + const { cid, titleRaw: name, mainPid, slug, timestamp } = await topics.getTopicFields(req.params.tid, ['cid', 'title', 'mainPid', 'slug', 'timestamp']); try { + if (timestamp > Date.now()) { // Scheduled topic, no response + return next(); + } + let [collection, pids] = await Promise.all([ activitypub.helpers.generateCollection({ set: `tid:${req.params.tid}:posts`, diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index 08c109eb19..9da0380c27 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -253,7 +253,7 @@ describe('Controllers', () => { }); }); - describe.only('Topic', () => { + describe('Topic Collection endpoint', () => { let cid; let uid; @@ -265,12 +265,11 @@ describe('Controllers', () => { describe('Live', () => { let topicData; - let postData; let response; let body; before(async () => { - ({ topicData, postData } = await topics.post({ + ({ topicData } = await topics.post({ uid, cid, title: 'Lorem "Lipsum" Ipsum', @@ -299,12 +298,11 @@ describe('Controllers', () => { describe('Scheduled', () => { let topicData; - let postData; let response; let body; before(async () => { - ({ topicData, postData } = await topics.post({ + ({ topicData } = await topics.post({ uid, cid, title: 'Lorem "Lipsum" Ipsum', @@ -325,4 +323,73 @@ describe('Controllers', () => { }); }); }); + + describe('Post Object endpoint', () => { + let cid; + let uid; + + before(async () => { + ({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); + const slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + describe('Live', () => { + let postData; + let response; + let body; + + before(async () => { + ({ postData } = await topics.post({ + uid, + cid, + title: 'Lorem "Lipsum" Ipsum', + content: 'Lorem ipsum dolor sit amet', + })); + + ({ response, body } = await request.get(`${nconf.get('url')}/post/${postData.pid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + })); + }); + + it('should respond properly', async () => { + assert(response); + assert.strictEqual(response.statusCode, 200); + }); + + it('should return a Note type object', () => { + assert.strictEqual(body.type, 'Note'); + }); + }); + + describe('Scheduled', () => { + let topicData; + let postData; + let response; + let body; + + before(async () => { + ({ topicData, postData } = await topics.post({ + uid, + cid, + title: 'Lorem "Lipsum" Ipsum', + content: 'Lorem ipsum dolor sit amet', + timestamp: Date.now() + (1000 * 60 * 60), // 1 hour in the future + })); + + ({ response, body } = await request.get(`${nconf.get('url')}/post/${postData.pid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + })); + }); + + it('should respond with a 404 Not Found', async () => { + assert(response); + assert.strictEqual(response.statusCode, 404); + }); + }); + }); });