From ea1e4c7dff4b60ff82467bfddeba7e940b8655b9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 11 Dec 2025 15:32:18 -0500 Subject: [PATCH] feat: disallow moving topics to and from remote categories, + basic tests for topic moving --- public/language/en-GB/error.json | 1 + src/topics/tools.js | 6 +- test/topics/tools.js | 109 +++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 test/topics/tools.js diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 140ec2bb76..b32ccfedfb 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -264,6 +264,7 @@ "no-topics-selected": "No topics selected!", "cant-move-to-same-topic": "Can't move post to same topic!", "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cant-move-topic-to-from-remote-categories": "You cannot move topics in or out of remote categories; consider cross-posting instead.", "cannot-block-self": "You cannot block yourself!", "cannot-block-privileged": "You cannot block administrators or global moderators", diff --git a/src/topics/tools.js b/src/topics/tools.js index 41db793168..b9fdde671f 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -235,7 +235,7 @@ module.exports = function (Topics) { }; topicTools.move = async function (tid, data) { - const cid = utils.isNumber(data.cid) ? parseInt(data.cid, 10) : data.cid; + const cid = parseInt(data.cid, 10); const topicData = await Topics.getTopicData(tid); if (!topicData) { throw new Error('[[error:no-topic]]'); @@ -243,6 +243,10 @@ module.exports = function (Topics) { if (cid === topicData.cid) { throw new Error('[[error:cant-move-topic-to-same-category]]'); } + if (!utils.isNumber(cid) || !utils.isNumber(topicData.cid)) { + throw new Error('[[error:cant-move-topic-to-from-remote-categories]]'); + } + const tags = await Topics.getTopicTags(tid); await db.sortedSetsRemove([ `cid:${topicData.cid}:tids`, diff --git a/test/topics/tools.js b/test/topics/tools.js new file mode 100644 index 0000000000..879de87b7d --- /dev/null +++ b/test/topics/tools.js @@ -0,0 +1,109 @@ +'use strict'; + +const assert = require('assert'); + +const db = require('../mocks/databasemock'); + +const user = require('../../src/user'); +const categories = require('../../src/categories'); +const topics = require('../../src/topics'); +const utils = require('../../src/utils'); + +describe('Topic tools', () => { + describe('Topic moving', () => { + let cid1; + let cid2; + let tid; + let uid; + + before(async () => { + ({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); + ({ cid: cid2 } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); + + uid = await user.create({ username: utils.generateUUID().slice(0, 8) }); + const { topicData } = await topics.post({ + uid, + cid: cid1, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); + tid = topicData.tid; + }); + + it('should not error when moving a topic from one cid to another', async () => { + await topics.tools.move(tid, { + cid: cid2, + uid, + }); + }); + + it('should reflect the topic in the new category', async () => { + const tids = await categories.getTopicIds({ + uid, + cid: cid2, + start: 0, + stop: 1, + }); + + assert(Array.isArray(tids)); + assert.deepStrictEqual(tids, [String(tid)]); + }); + + it('should NOT reflect the topic in the old category', async () => { + const tids = await categories.getTopicIds({ + uid, + cid: cid1, + start: 0, + stop: 1, + }); + + assert(Array.isArray(tids)); + assert.deepStrictEqual(tids, []); + }); + }); + + describe('with remote categories', () => { + let remoteCid; + let localCid; + let tid1; + let tid2; + + before(async () => { + const helpers = require('../activitypub/helpers'); + ({ id: remoteCid } = helpers.mocks.group()); + ({ cid: localCid } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); + + ({ id: tid1 } = helpers.mocks.note({ + audience: remoteCid, + })); + const uid = await user.create({ username: utils.generateUUID().slice(0, 8) }); + const { topicData } = await topics.post({ + uid, + cid: localCid, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); + tid2 = topicData.tid; + }); + + it('should throw when attempting to move a topic from a remote category', async () => { + assert.rejects( + topics.tools.move(tid1, { + cid: localCid, + uid: 'system', + }), + '[[error:cant-move-topic-to-from-remote-categories]]' + ); + }); + + it('should throw when attempting to move a topic to a remote category', async () => { + assert.rejects( + topics.tools.move(tid2, { + cid: remoteCid, + uid: 'system', + }), + '[[error:cant-move-topic-to-from-remote-categories]]' + ); + }); + }); +}); \ No newline at end of file