From 68f73fd0770ae6b810bc752a336a38dcd089cde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 2 Jan 2025 10:57:26 -0500 Subject: [PATCH 01/27] https://github.com/NodeBB/NodeBB/issues/13018 --- public/src/app.js | 2 +- public/src/modules/helpers.common.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index 599792838f..c0012de205 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -220,7 +220,7 @@ if (document.readyState === 'loading') { if (!isTouchDevice) { els = els || $('body'); els.tooltip({ - selector: '.avatar.avatar-tooltip', + selector: '.avatar-tooltip', placement: placement || 'top', container: '#content', animation: false, diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index a60c1ff034..32cda3b7ac 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -214,9 +214,9 @@ module.exports = function (utils, Benchpress, relative_path) { function renderTopicImage(topicObj) { if (topicObj.thumb) { - return ''; + return ''; } - return ''; + return ''; } function renderDigestAvatar(block) { @@ -313,7 +313,7 @@ module.exports = function (utils, Benchpress, relative_path) { let output = ''; if (userObj.picture) { - output += ``; + output += ``; } output += `${userObj['icon:text']}`; return output; From 48944a86beaa1e2ad4da1c63ad7cd48b25776eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 2 Jan 2025 11:07:15 -0500 Subject: [PATCH 02/27] chore: up deps --- install/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/package.json b/install/package.json index 2331e72ad7..b7e51f9d62 100644 --- a/install/package.json +++ b/install/package.json @@ -107,11 +107,11 @@ "nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-spam-be-gone": "2.3.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.94", + "nodebb-theme-harmony": "1.2.95", "nodebb-theme-lavender": "7.1.17", "nodebb-theme-peace": "2.2.33", - "nodebb-theme-persona": "13.3.62", - "nodebb-widget-essentials": "7.0.31", + "nodebb-theme-persona": "13.3.63", + "nodebb-widget-essentials": "7.0.32", "nodemailer": "6.9.16", "nprogress": "0.2.0", "passport": "0.7.0", From dcc2342cd1852c886119bbd61a768c62ead0372a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 3 Jan 2025 09:37:57 -0500 Subject: [PATCH 03/27] fix: closes #13019, use displayname in vote tooltip --- src/api/posts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/posts.js b/src/api/posts.js index 4e3917a008..efeec43ace 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -370,10 +370,10 @@ postsAPI.getUpvoters = async function (caller, data) { upvotedUids = upvotedUids.slice(0, cutoff - 1); } - const usernames = await user.getUsernamesByUids(upvotedUids); + const users = await user.getUsersFields(upvotedUids, ['username']); return { otherCount, - usernames, + usernames: users.map(user => user.displayname), cutoff, }; }; From 0a135d88b8352ce1180adea467f12a97f6d0ed7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 3 Jan 2025 09:52:26 -0500 Subject: [PATCH 04/27] chore: up markdown --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index b7e51f9d62..9cfb3f094a 100644 --- a/install/package.json +++ b/install/package.json @@ -102,7 +102,7 @@ "nodebb-plugin-dbsearch": "6.2.5", "nodebb-plugin-emoji": "5.1.15", "nodebb-plugin-emoji-android": "4.0.0", - "nodebb-plugin-markdown": "12.2.8", + "nodebb-plugin-markdown": "12.2.9", "nodebb-plugin-mentions": "4.4.5", "nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-spam-be-gone": "2.3.0", From fa24f1dd83dc6f92dc53ad49dcfaa6e0aefc2f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 3 Jan 2025 09:58:03 -0500 Subject: [PATCH 05/27] test: fix thumb test --- test/template-helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/template-helpers.js b/test/template-helpers.js index 00ae777f82..8f73d71712 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -169,9 +169,9 @@ describe('helpers', () => { }); it('should render thumb as topic image', (done) => { - const topicObj = { thumb: '/uploads/1.png', user: { username: 'baris' } }; + const topicObj = { thumb: '/uploads/1.png', user: { username: 'baris', displayname: 'Baris Soner Usakli' } }; const html = helpers.renderTopicImage(topicObj); - assert.equal(html, ``); + assert.equal(html, ``); done(); }); From 29804a1093e9ca913fd434948d7efc7f6e6dba95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 3 Jan 2025 10:07:42 -0500 Subject: [PATCH 06/27] test: fix user picture test --- test/template-helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/template-helpers.js b/test/template-helpers.js index 8f73d71712..76346dbf13 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -176,9 +176,9 @@ describe('helpers', () => { }); it('should render user picture as topic image', (done) => { - const topicObj = { thumb: '', user: { uid: 1, username: 'baris', picture: '/uploads/2.png' } }; + const topicObj = { thumb: '', user: { uid: 1, username: 'baris', displayname: 'Baris Soner Usakli', picture: '/uploads/2.png' } }; const html = helpers.renderTopicImage(topicObj); - assert.equal(html, ``); + assert.equal(html, ``); done(); }); From fcf50a3c01243e3f52e7d365572d4573666239fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 4 Jan 2025 09:37:10 -0500 Subject: [PATCH 07/27] fix: closes #13022, don't do anything if notif setting isnt set to email --- src/controllers/accounts/settings.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index 6248bc5ddd..8530486d7c 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -113,8 +113,14 @@ const doUnsubscribe = async (payload) => { user.updateDigestSetting(payload.uid, 'off'), ]); } else if (payload.template === 'notification') { + const currentToNewSetting = { + notificationemail: 'notification', + email: 'none', + }; const current = await db.getObjectField(`user:${payload.uid}:settings`, `notificationType_${payload.type}`); - await user.setSetting(payload.uid, `notificationType_${payload.type}`, (current === 'notificationemail' ? 'notification' : 'none')); + if (currentToNewSetting.hasOwnProperty(current)) { + await user.setSetting(payload.uid, `notificationType_${payload.type}`, currentToNewSetting[current]); + } } return true; }; From d155da3ca9b995fbaf7f96dfcae444e7a3b02e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 5 Jan 2025 09:49:24 -0500 Subject: [PATCH 08/27] feat: add missing default notification settings --- install/data/defaults.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install/data/defaults.json b/install/data/defaults.json index 8b6f7a2b03..e49c8282a9 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -107,11 +107,14 @@ "flags:actionOnReject": "rescind", "notificationType_upvote": "notification", "notificationType_new-topic": "notification", + "notificationType_new-topic-with-tag": "notification", + "notificationType_new-topic-in-category": "notification", "notificationType_new-reply": "notification", "notificationType_post-edit": "notification", "notificationType_follow": "notification", "notificationType_new-chat": "notification", "notificationType_new-group-chat": "notification", + "notificationType_new-public-chat": "none", "notificationType_group-invite": "notification", "notificationType_group-leave": "notification", "notificationType_group-request-membership": "notification", From 238a3ed5b22c47b426a277d3bff5198d02629325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Jan 2025 10:22:31 -0500 Subject: [PATCH 09/27] fix: closes #13024, on register dont auto login with user doesn't have local:login privilege on login page show login form if at least one user group has local:login privilege, for example local:login might be removed from registered-users but verified-users can have login privilege so login form should be still visible --- src/controllers/authentication.js | 5 ++++- src/controllers/index.js | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 6591459cf2..299bfa571b 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -49,7 +49,10 @@ async function registerAndLoginUser(req, res, userData) { const uid = await user.create(userData); if (res.locals.processLogin) { - await authenticationController.doLogin(req, uid); + const hasLoginPrivilege = await privileges.global.can('local:login', uid); + if (hasLoginPrivilege) { + await authenticationController.doLogin(req, uid); + } } // Distinguish registrations through invites from direct ones diff --git a/src/controllers/index.js b/src/controllers/index.js index 0e5dde32b6..50c7fe4444 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -7,6 +7,7 @@ const meta = require('../meta'); const user = require('../user'); const plugins = require('../plugins'); const privileges = require('../privileges'); +const privilegesHelpers = require('../privileges/helpers'); const helpers = require('./helpers'); const Controllers = module.exports; @@ -124,7 +125,8 @@ Controllers.login = async function (req, res) { data.title = '[[pages:login]]'; data.allowPasswordReset = !meta.config['password:disableEdit']; - const hasLoginPrivilege = await privileges.global.canGroup('local:login', 'registered-users'); + const loginPrivileges = await privilegesHelpers.getGroupPrivileges(0, ['groups:local:login']); + const hasLoginPrivilege = !!loginPrivileges.find(privilege => privilege.privileges['groups:local:login']); data.allowLocalLogin = hasLoginPrivilege || parseInt(req.query.local, 10) === 1; if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { From 4f682a310ed9239c1bc5cdbc1c6f7769a805a6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Jan 2025 10:54:04 -0500 Subject: [PATCH 10/27] feat: add -y flag to upgrade, closes #13023 --- src/cli/index.js | 7 +++++-- src/cli/upgrade-plugins.js | 22 ++++++++++++---------- src/cli/upgrade.js | 10 +++++----- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/cli/index.js b/src/cli/index.js index e6f0485585..7bd1c37b87 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -87,7 +87,8 @@ program .option('--log-level ', 'Default logging level to use', 'info') .option('--config ', 'Specify a config file', 'config.json') .option('-d, --dev', 'Development mode, including verbose logging', false) - .option('-l, --log', 'Log subprocess output to console', false); + .option('-l, --log', 'Log subprocess output to console', false) + .option('-y, --unattended', 'Answer yes to any prompts, like plugin upgrades', false); // provide a yargs object ourselves // otherwise yargs will consume `--help` or `help` @@ -294,6 +295,7 @@ program ].join('\n')}`); }) .action((scripts, options) => { + options.unattended = program.opts().unattended; if (program.opts().dev) { process.env.NODE_ENV = 'development'; global.env = 'development'; @@ -308,7 +310,8 @@ program .alias('upgradePlugins') .description('Upgrade plugins') .action(() => { - require('./upgrade-plugins').upgradePlugins((err) => { + const { unattended } = program.opts(); + require('./upgrade-plugins').upgradePlugins(unattended, (err) => { if (err) { throw err; } diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index b68db55eac..ec25fdaa69 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -120,7 +120,7 @@ async function checkPlugins() { return upgradable; } -async function upgradePlugins() { +async function upgradePlugins(unattended = false) { try { const found = await checkPlugins(); if (found && found.length) { @@ -132,16 +132,18 @@ async function upgradePlugins() { console.log(chalk.green('\nAll packages up-to-date!')); return; } + let result = { upgrade: 'y' }; + if (!unattended) { + prompt.message = ''; + prompt.delimiter = ''; - prompt.message = ''; - prompt.delimiter = ''; - - prompt.start(); - const result = await prompt.get({ - name: 'upgrade', - description: '\nProceed with upgrade (y|n)?', - type: 'string', - }); + prompt.start(); + result= await prompt.get({ + name: 'upgrade', + description: '\nProceed with upgrade (y|n)?', + type: 'string', + }); + } if (['y', 'Y', 'yes', 'YES'].includes(result.upgrade)) { console.log('\nUpgrading packages...'); diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index ff3487388c..e879239b7a 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -24,9 +24,9 @@ const steps = { }, plugins: { message: 'Checking installed plugins for updates...', - handler: async function () { + handler: async function (options) { await require('../database').init(); - await upgradePlugins(); + await upgradePlugins(options.unattended); }, }, schema: { @@ -45,14 +45,14 @@ const steps = { }, }; -async function runSteps(tasks) { +async function runSteps(tasks, options) { try { for (let i = 0; i < tasks.length; i++) { const step = steps[tasks[i]]; if (step && step.message && step.handler) { process.stdout.write(`\n${chalk.bold(`${i + 1}. `)}${chalk.yellow(step.message)}`); /* eslint-disable-next-line */ - await step.handler(); + await step.handler(options); } } const message = 'NodeBB Upgrade Complete!'; @@ -95,7 +95,7 @@ async function runUpgrade(upgrades, options) { options.plugins || options.schema || options.build) { tasks = tasks.filter(key => options[key]); } - await runSteps(tasks); + await runSteps(tasks, options); return; } From 7bfca7ba1bf20f8ccad5b42ba2eaf6b8b8615c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Jan 2025 10:55:10 -0500 Subject: [PATCH 11/27] lint: index.js --- src/controllers/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/index.js b/src/controllers/index.js index 50c7fe4444..3812d0bc55 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -6,7 +6,6 @@ const validator = require('validator'); const meta = require('../meta'); const user = require('../user'); const plugins = require('../plugins'); -const privileges = require('../privileges'); const privilegesHelpers = require('../privileges/helpers'); const helpers = require('./helpers'); @@ -125,7 +124,7 @@ Controllers.login = async function (req, res) { data.title = '[[pages:login]]'; data.allowPasswordReset = !meta.config['password:disableEdit']; - const loginPrivileges = await privilegesHelpers.getGroupPrivileges(0, ['groups:local:login']); + const loginPrivileges = await privilegesHelpers.getGroupPrivileges(0, ['groups:local:login']); const hasLoginPrivilege = !!loginPrivileges.find(privilege => privilege.privileges['groups:local:login']); data.allowLocalLogin = hasLoginPrivilege || parseInt(req.query.local, 10) === 1; From 27e945fde8128a21dfd513004ed134e2ba67ddb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Jan 2025 11:36:53 -0500 Subject: [PATCH 12/27] lint: whitespace --- src/cli/upgrade-plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index ec25fdaa69..92cc980fa2 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -138,7 +138,7 @@ async function upgradePlugins(unattended = false) { prompt.delimiter = ''; prompt.start(); - result= await prompt.get({ + result = await prompt.get({ name: 'upgrade', description: '\nProceed with upgrade (y|n)?', type: 'string', From d75b169cd8ed3d11774127ece9e19c14f3ebd43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Jan 2025 20:53:03 -0500 Subject: [PATCH 13/27] refactor: silence deprecations in prod --- src/meta/minifier.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/meta/minifier.js b/src/meta/minifier.js index 6aa975b334..6a617cc45d 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -164,7 +164,10 @@ actions.buildCSS = async function buildCSS(data) { loadPaths: data.paths, }; if (data.minify) { - opts.silenceDeprecations = ['mixed-decls', 'color-functions']; + opts.silenceDeprecations = [ + 'legacy-js-api', 'mixed-decls', 'color-functions', + 'global-builtin', 'import', + ]; } const scssOutput = await sass.compileStringAsync(data.source, opts); css = scssOutput.css.toString(); From 8864e65805330246a43aa245e628d4309901a1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 11:35:20 -0500 Subject: [PATCH 14/27] test: add back disabled test since node 18 is minimum --- test/utils.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/utils.js b/test/utils.js index 2d3edb7ff0..7dbf0fa0bd 100644 --- a/test/utils.js +++ b/test/utils.js @@ -199,10 +199,9 @@ describe('Utility Methods', () => { utils.assertPasswordValidity('Yzsh31j!a', zxcvbn); }); - // it('should generate UUID', () => { - // TODO: add back when nodejs 18 is minimum - // assert(validator.isUUID(utils.generateUUID())); - // }); + it('should generate UUID', () => { + assert(validator.isUUID(utils.generateUUID())); + }); it('should shallow merge two objects', (done) => { const a = { foo: 1, cat1: 'ginger' }; From d5ae91a35dafd4b7994212f9794e4b9fc52bbc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 11:58:54 -0500 Subject: [PATCH 15/27] test: debounce/throttle --- test/utils.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/utils.js b/test/utils.js index 7dbf0fa0bd..5a5ac44130 100644 --- a/test/utils.js +++ b/test/utils.js @@ -501,4 +501,58 @@ describe('Utility Methods', () => { assert.strictEqual(result.user1.uid, uid1); assert.strictEqual(result.user2.uid, uid2); }); + + describe('debounce/throttle', () => { + it('should call function after x milliseconds once', (done) => { + let count = 0; + const now = Date.now(); + const fn = utils.debounce(() => { + count += 1; + assert.strictEqual(count, 1); + assert(Date.now() - now > 50); + }, 100); + fn(); + fn(); + setTimeout(() => done(), 200); + }); + + it('should call function first if immediate=true', (done) => { + let count = 0; + const now = Date.now(); + const fn = utils.debounce(() => { + count += 1; + assert.strictEqual(count, 1); + assert(Date.now() - now < 50); + }, 100, true); + fn(); + fn(); + setTimeout(() => done(), 200); + }); + + it('should call function after x milliseconds once', (done) => { + let count = 0; + const now = Date.now(); + const fn = utils.throttle(() => { + count += 1; + assert.strictEqual(count, 1); + assert(Date.now() - now > 50); + }, 100); + fn(); + fn(); + setTimeout(() => done(), 200); + }); + + it('should call function twice if immediate=true', (done) => { + let count = 0; + const fn = utils.throttle(() => { + count += 1; + }, 100, true); + fn(); + fn(); + setTimeout(() => { + assert.strictEqual(count, 2); + done(); + }, 200); + }); + }); }); From 95b42f830a33bc07614785bbabb908dc259c0283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 13:09:16 -0500 Subject: [PATCH 16/27] test: add helper tests --- public/src/modules/helpers.common.js | 2 +- test/template-helpers.js | 91 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 32cda3b7ac..ae7493aedc 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -383,7 +383,7 @@ module.exports = function (utils, Benchpress, relative_path) { `; }); - return html; + return html.join(''); } function register() { diff --git a/test/template-helpers.js b/test/template-helpers.js index 76346dbf13..8e95e66c8c 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -52,6 +52,17 @@ describe('helpers', () => { done(); }); + it('should return true if route is visible', (done) => { + const flag = helpers.displayMenuItem({ + navigation: [{ route: '/recent' }], + user: { + privileges: {}, + }, + }, 0); + assert(flag); + done(); + }); + it('should stringify object', (done) => { const str = helpers.stringify({ a: 'herp < derp > and & quote "' }); assert.equal(str, '{"a":"herp < derp > and & quote \\""}'); @@ -64,7 +75,57 @@ describe('helpers', () => { done(); }); + it('should build category icon', (done) => { + assert.strictEqual( + helpers.buildCategoryIcon({ + bgColor: '#ff0000', + color: '#00ff00', + backgroundImage: '/assets/uploads/image.png', + imageClass: 'auto', + }, 16, 'rounded-circle'), + '' + ); + assert.strictEqual( + helpers.buildCategoryIcon({ + bgColor: '#ff0000', + color: '#00ff00', + backgroundImage: '/assets/uploads/image.png', + imageClass: 'auto', + icon: 'fa-book', + }, 16, 'rounded-circle'), + '' + ); + done(); + }); + + it('should build category label', (done) => { + assert.strictEqual( + helpers.buildCategoryLabel({ + bgColor: '#ff0000', + color: '#00ff00', + backgroundImage: '/assets/uploads/image.png', + imageClass: 'auto', + name: 'Category 1', + }, 'a', ''), + `\n\t\t\t\n\t\t\tCategory 1\n\t\t` + ); + assert.strictEqual( + helpers.buildCategoryLabel({ + bgColor: '#ff0000', + color: '#00ff00', + backgroundImage: '/assets/uploads/image.png', + imageClass: 'auto', + name: 'Category 1', + icon: 'fa-book', + }, 'span', 'rounded-1'), + `\n\t\t\t\n\t\t\tCategory 1\n\t\t`, + ); + done(); + }); + it('should return empty string if category is falsy', (done) => { + assert.equal(helpers.buildCategoryIcon(null), ''); + assert.equal(helpers.buildCategoryLabel(null), ''); assert.equal(helpers.generateCategoryBackground(null), ''); done(); }); @@ -251,4 +312,34 @@ describe('helpers', () => { assert.equal(html, ''); done(); }); + + it('should generate replied to or wrote based on toPid', (done) => { + const now = Date.now(); + const iso = new Date().toISOString(); + let post = { pid: 2, toPid: 1, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; + let str = helpers.generateWroteReplied(post, 1); + assert.strictEqual(str, `[[topic:replied-to-user-ago, 1, /post/1, baris, /post/2, ${iso}]]`); + + post = { pid: 2, toPid: 1, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; + str = helpers.generateWroteReplied(post, -1); + assert.strictEqual(str, `[[topic:replied-to-user-on, 1, /post/1, baris, /post/2, ${iso}]]`); + + post = { pid: 2, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; + str = helpers.generateWroteReplied(post, 1); + assert.strictEqual(str, `[[topic:wrote-ago, /post/2, ${iso}]]`); + + str = helpers.generateWroteReplied(post, -1); + assert.strictEqual(str, `[[topic:wrote-on, /post/2, ${iso}]]`); + + done(); + }); + + it('should generate placeholder wave', (done) => { + const items = [2, 'divider', 3]; + const str = helpers.generatePlaceholderWave(items); + assert(str.includes('dropdown-divider')); + assert(str.includes('col-2')); + assert(str.includes('col-3')); + done(); + }); }); From 407b0cc6acc286a09b2a90cb2e8b47af4e79e60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 13:14:41 -0500 Subject: [PATCH 17/27] test: subfolder in category link --- test/template-helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/template-helpers.js b/test/template-helpers.js index 8e95e66c8c..fa72cbdced 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -107,7 +107,7 @@ describe('helpers', () => { imageClass: 'auto', name: 'Category 1', }, 'a', ''), - `\n\t\t\t\n\t\t\tCategory 1\n\t\t` + `\n\t\t\t\n\t\t\tCategory 1\n\t\t` ); assert.strictEqual( helpers.buildCategoryLabel({ From 225c91edb1b8380d6dfd8c04c14e006aebe5d733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 13:29:32 -0500 Subject: [PATCH 18/27] test: add translateInPlace test --- test/utils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/utils.js b/test/utils.js index 5a5ac44130..c3207bacbb 100644 --- a/test/utils.js +++ b/test/utils.js @@ -555,4 +555,17 @@ describe('Utility Methods', () => { }, 200); }); }); + + describe('Translator', () => { + const shim = require('../src/translator'); + + const { Translator } = shim; + it('should translate in place', async () => { + const translator = Translator.create('en-GB'); + const el = $(`
[[global:home]]
`); + await translator.translateInPlace(el.get(0)); + assert.strictEqual(el.find('#text').text(), 'Home'); + assert.strictEqual(el.find('#search').attr('title'), 'Search'); + }); + }); }); From 127e0e22cf4b9e56b8f56a963e775afb6219bb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 13:31:48 -0500 Subject: [PATCH 19/27] test: fix relative path in tests --- test/template-helpers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/template-helpers.js b/test/template-helpers.js index fa72cbdced..0a523e29b5 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -318,18 +318,18 @@ describe('helpers', () => { const iso = new Date().toISOString(); let post = { pid: 2, toPid: 1, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; let str = helpers.generateWroteReplied(post, 1); - assert.strictEqual(str, `[[topic:replied-to-user-ago, 1, /post/1, baris, /post/2, ${iso}]]`); + assert.strictEqual(str, `[[topic:replied-to-user-ago, 1, ${nconf.get('relative_path')}/post/1, baris, ${nconf.get('relative_path')}/post/2, ${iso}]]`); post = { pid: 2, toPid: 1, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; str = helpers.generateWroteReplied(post, -1); - assert.strictEqual(str, `[[topic:replied-to-user-on, 1, /post/1, baris, /post/2, ${iso}]]`); + assert.strictEqual(str, `[[topic:replied-to-user-on, 1, ${nconf.get('relative_path')}/post/1, baris, ${nconf.get('relative_path')}/post/2, ${iso}]]`); post = { pid: 2, timestamp: now, timestampISO: iso, parent: { displayname: 'baris' } }; str = helpers.generateWroteReplied(post, 1); - assert.strictEqual(str, `[[topic:wrote-ago, /post/2, ${iso}]]`); + assert.strictEqual(str, `[[topic:wrote-ago, ${nconf.get('relative_path')}/post/2, ${iso}]]`); str = helpers.generateWroteReplied(post, -1); - assert.strictEqual(str, `[[topic:wrote-on, /post/2, ${iso}]]`); + assert.strictEqual(str, `[[topic:wrote-on, ${nconf.get('relative_path')}/post/2, ${iso}]]`); done(); }); From 0d3a8757bd1a8806ddd8155acb8f2cad75fa466d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 13:55:40 -0500 Subject: [PATCH 20/27] test: missing translator tests --- test/translator.js | 30 ++++++++++++++++++++++++++++++ test/utils.js | 6 ++++++ 2 files changed, 36 insertions(+) diff --git a/test/translator.js b/test/translator.js index 61c3d5af8e..017efd3ce2 100644 --- a/test/translator.js +++ b/test/translator.js @@ -41,6 +41,36 @@ describe('Translator shim', () => { assert.strictEqual(t, 'secret'); }); }); + + describe('translateKeys', () => { + it('should translate each key in array', async () => { + const translated = await shim.translateKeys(['[[global:home]]', '[[global:search]]'], 'en-GB'); + assert.deepStrictEqual(translated, ['Home', 'Search']); + }); + + it('should translate each key in array using a callback', (done) => { + shim.translateKeys(['[[global:save]]', '[[global:close]]'], 'en-GB', (translated) => { + assert.deepStrictEqual(translated, ['Save', 'Close']); + done(); + }); + }); + }); + + it('should load translations for language', (done) => { + shim.load('en-GB', 'global', (translations) => { + assert(translations); + assert(translations['header.profile']); + done(); + }); + }); + + it('should get translations for language', (done) => { + shim.getTranslations('en-GB', 'global', (translations) => { + assert(translations); + assert(translations['header.profile']); + done(); + }); + }); }); describe('new Translator(language)', () => { diff --git a/test/utils.js b/test/utils.js index c3207bacbb..2cd422bdb6 100644 --- a/test/utils.js +++ b/test/utils.js @@ -567,5 +567,11 @@ describe('Utility Methods', () => { assert.strictEqual(el.find('#text').text(), 'Home'); assert.strictEqual(el.find('#search').attr('title'), 'Search'); }); + + it('should not error', (done) => { + shim.flush(); + shim.flushNamespace(); + done(); + }); }); }); From b129c6c0e7c83e9c20f567e027aa797d07133bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 14:13:59 -0500 Subject: [PATCH 21/27] test: fix controller tests --- test/controllers.js | 85 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/test/controllers.js b/test/controllers.js index e94720b471..53e2747b8d 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -31,7 +31,6 @@ describe('Controllers', () => { let fooUid; let adminUid; let category; - let testRoutes = []; before(async () => { category = await categories.create({ @@ -56,48 +55,6 @@ describe('Controllers', () => { tid = result.topicData.tid; pid = result.postData.pid; - - testRoutes = [ - { it: 'should load /reset without code', url: '/reset' }, - { it: 'should load /reset with invalid code', url: '/reset/123123' }, - { it: 'should load /login', url: '/login' }, - { it: 'should load /register', url: '/register' }, - { it: 'should load /robots.txt', url: '/robots.txt' }, - { it: 'should load /manifest.webmanifest', url: '/manifest.webmanifest' }, - { it: 'should load /outgoing?url=', url: '/outgoing?url=http://youtube.com' }, - { it: 'should 404 on /outgoing with no url', url: '/outgoing', status: 404 }, - { it: 'should 404 on /outgoing with javascript: protocol', url: '/outgoing?url=javascript:alert(1);', status: 404 }, - { it: 'should 404 on /outgoing with invalid url', url: '/outgoing?url=derp', status: 404 }, - { it: 'should load /sping', url: '/sping', body: 'healthy' }, - { it: 'should load /ping', url: '/ping', body: '200' }, - { it: 'should handle 404', url: '/arouteinthevoid', status: 404 }, - { it: 'should load topic rss feed', url: `/topic/${tid}.rss` }, - { it: 'should load category rss feed', url: `/category/${cid}.rss` }, - { it: 'should load topics rss feed', url: `/topics.rss` }, - { it: 'should load recent rss feed', url: `/recent.rss` }, - { it: 'should load top rss feed', url: `/top.rss` }, - { it: 'should load popular rss feed', url: `/popular.rss` }, - { it: 'should load popular rss feed with term', url: `/popular/day.rss` }, - { it: 'should load recent posts rss feed', url: `/recentposts.rss` }, - { it: 'should load category recent posts rss feed', url: `/category/${cid}/recentposts.rss` }, - { it: 'should load user topics rss feed', url: `/user/foo/topics.rss` }, - { it: 'should load tag rss feed', url: `/tags/nodebb.rss` }, - { it: 'should load client.css', url: `/assets/client.css` }, - { it: 'should load admin.css', url: `/assets/admin.css` }, - { it: 'should load sitemap.xml', url: `/sitemap.xml` }, - { it: 'should load sitemap/pages.xml', url: `/sitemap/pages.xml` }, - { it: 'should load sitemap/categories.xml', url: `/sitemap/categories.xml` }, - { it: 'should load sitemap/topics.1.xml', url: `/sitemap/topics.1.xml` }, - { it: 'should load theme screenshot', url: `/css/previews/nodebb-theme-harmony` }, - { it: 'should load users page', url: `/users` }, - { it: 'should load users page section', url: `/users?section=online` }, - { it: 'should load groups page', url: `/groups` }, - { it: 'should get recent posts', url: `/api/recent/posts/month` }, - { it: 'should get post data', url: `/api/v3/posts/${pid}` }, - { it: 'should get topic data', url: `/api/v3/topics/${tid}` }, - { it: 'should get category data', url: `/api/v3/categories/${cid}` }, - { it: 'should return osd data', url: `/osd.xml` }, - ]; }); it('should load /config with csrf_token', async () => { @@ -222,7 +179,49 @@ describe('Controllers', () => { describe('routes that should 200/404 etc.', () => { const baseUrl = nconf.get('url'); + const testRoutes = [ + { it: 'should load /reset without code', url: '/reset' }, + { it: 'should load /reset with invalid code', url: '/reset/123123' }, + { it: 'should load /login', url: '/login' }, + { it: 'should load /register', url: '/register' }, + { it: 'should load /robots.txt', url: '/robots.txt' }, + { it: 'should load /manifest.webmanifest', url: '/manifest.webmanifest' }, + { it: 'should load /outgoing?url=', url: '/outgoing?url=http://youtube.com' }, + { it: 'should 404 on /outgoing with no url', url: '/outgoing', status: 404 }, + { it: 'should 404 on /outgoing with javascript: protocol', url: '/outgoing?url=javascript:alert(1);', status: 404 }, + { it: 'should 404 on /outgoing with invalid url', url: '/outgoing?url=derp', status: 404 }, + { it: 'should load /sping', url: '/sping', body: 'healthy' }, + { it: 'should load /ping', url: '/ping', body: '200' }, + { it: 'should handle 404', url: '/arouteinthevoid', status: 404 }, + { it: 'should load topic rss feed', url: `/topic/1.rss` }, + { it: 'should load category rss feed', url: `/category/1.rss` }, + { it: 'should load topics rss feed', url: `/topics.rss` }, + { it: 'should load recent rss feed', url: `/recent.rss` }, + { it: 'should load top rss feed', url: `/top.rss` }, + { it: 'should load popular rss feed', url: `/popular.rss` }, + { it: 'should load popular rss feed with term', url: `/popular/day.rss` }, + { it: 'should load recent posts rss feed', url: `/recentposts.rss` }, + { it: 'should load category recent posts rss feed', url: `/category/1/recentposts.rss` }, + { it: 'should load user topics rss feed', url: `/user/foo/topics.rss` }, + { it: 'should load tag rss feed', url: `/tags/nodebb.rss` }, + { it: 'should load client.css', url: `/assets/client.css` }, + { it: 'should load admin.css', url: `/assets/admin.css` }, + { it: 'should load sitemap.xml', url: `/sitemap.xml` }, + { it: 'should load sitemap/pages.xml', url: `/sitemap/pages.xml` }, + { it: 'should load sitemap/categories.xml', url: `/sitemap/categories.xml` }, + { it: 'should load sitemap/topics.1.xml', url: `/sitemap/topics.1.xml` }, + { it: 'should load theme screenshot', url: `/css/previews/nodebb-theme-harmony` }, + { it: 'should load users page', url: `/users` }, + { it: 'should load users page section', url: `/users?section=online` }, + { it: 'should load groups page', url: `/groups` }, + { it: 'should get recent posts', url: `/api/recent/posts/month` }, + { it: 'should get post data', url: `/api/v3/posts/1` }, + { it: 'should get topic data', url: `/api/v3/topics/1` }, + { it: 'should get category data', url: `/api/v3/categories/1` }, + { it: 'should return osd data', url: `/osd.xml` }, + ]; testRoutes.forEach((route) => { + console.log('route', route); it(route.it, async () => { const { response, body } = await request.get(`${baseUrl}/${route.url}`); assert.equal(response.statusCode, route.status || 200); From 2f5effda26604a9ee7cc0674cbf1eb5d54b47d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 14:25:15 -0500 Subject: [PATCH 22/27] chore: remove log --- test/controllers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/controllers.js b/test/controllers.js index 53e2747b8d..d0eec251c7 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -221,7 +221,6 @@ describe('Controllers', () => { { it: 'should return osd data', url: `/osd.xml` }, ]; testRoutes.forEach((route) => { - console.log('route', route); it(route.it, async () => { const { response, body } = await request.get(`${baseUrl}/${route.url}`); assert.equal(response.statusCode, route.status || 200); From ae2bd5ab4160c37290dc4f17d66df16128ec94b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 17:35:42 -0500 Subject: [PATCH 23/27] test: custom fields tests add missing setReputation test --- src/controllers/accounts/helpers.js | 2 +- src/user/profile.js | 2 +- test/socket.io.js | 20 +++++ test/user/custom-fields.js | 117 ++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 test/user/custom-fields.js diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 5b30cd0d7a..f2e0fbc5a7 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -160,7 +160,7 @@ helpers.getCustomUserFields = async function (callerUID, userData) { if (f.type === 'input-link' && userValue) { f.linkValue = validator.escape(String(userValue.replace('http://', '').replace('https://', ''))); } - f['select-options'] = f['select-options'].split('\n').filter(Boolean).map( + f['select-options'] = (f['select-options'] || '').split('\n').filter(Boolean).map( opt => ({ value: opt, selected: Array.isArray(userValue) ? diff --git a/src/user/profile.js b/src/user/profile.js index 7869f14410..14942f98aa 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -16,7 +16,7 @@ const tx = require('../translator'); module.exports = function (User) { User.updateProfile = async function (uid, data, extraFields) { let fields = [ - 'username', 'email', 'fullname', 'website', 'location', + 'username', 'email', 'fullname', 'groupTitle', 'birthday', 'signature', 'aboutme', ...await db.getSortedSetRange('user-custom-fields', 0, -1), ]; diff --git a/test/socket.io.js b/test/socket.io.js index 9f2aeebbb6..8fd2895809 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -199,6 +199,26 @@ describe('socket.io', () => { assert(Array.isArray(users[0].groups)); }); + it('should error with invalid data set user reputation', async () => { + await assert.rejects( + socketAdmin.user.setReputation({ uid: adminUid }, null), + { message: '[[error:invalid-data]]' } + ); + await assert.rejects( + socketAdmin.user.setReputation({ uid: adminUid }, {}), + { message: '[[error:invalid-data]]' } + ); + await assert.rejects( + socketAdmin.user.setReputation({ uid: adminUid }, { uids: [], value: null }), + { message: '[[error:invalid-data]]' } + ); + }); + + it('should set user reputation', async () => { + await socketAdmin.user.setReputation({ uid: adminUid }, { uids: [adminUid], value: 10 }); + assert.strictEqual(10, await db.sortedSetScore('users:reputation', adminUid)); + }); + it('should reset lockouts', (done) => { socketAdmin.user.resetLockouts({ uid: adminUid }, [regularUid], (err) => { assert.ifError(err); diff --git a/test/user/custom-fields.js b/test/user/custom-fields.js new file mode 100644 index 0000000000..c032fd47a8 --- /dev/null +++ b/test/user/custom-fields.js @@ -0,0 +1,117 @@ +'use strict'; + +const nconf = require('nconf'); +const assert = require('assert'); +const async = require('async'); + +const db = require('../mocks/databasemock'); + +const user = require('../../src/user'); +const groups = require('../../src/groups'); + +const request = require('../../src/request'); +const socketUser = require('../../src/socket.io/user'); +const adminUser = require('../../src/socket.io/admin/user'); + + +describe('custom user fields', () => { + let adminUid; + let lowRepUid; + let highRepUid; + before(async () => { + adminUid = await user.create({ username: 'admin' }); + await groups.join('administrators', adminUid); + lowRepUid = await user.create({ username: 'lowRepUser' }); + highRepUid = await user.create({ username: 'highRepUser' }); + await db.setObjectField(`user:${highRepUid}`, 'reputation', 10); + await db.sortedSetAdd(`users:reputation`, 10, highRepUid); + }); + + it('should create custom user fields', async () => { + const fields = [ + { key: 'website', icon: 'fa-solid fa-globe', name: 'Website', type: 'input-link', visibility: 'all', 'min:rep': 0 }, + { key: 'location', icon: 'fa-solid fa-pin', name: 'Location', type: 'input-text', visibility: 'all', 'min:rep': 0 }, + { key: 'favouriteDate', icon: '', name: 'Anniversary', type: 'input-date', visibility: 'all', 'min:rep': 0 }, + { key: 'favouriteLanguages', icon: 'fa-solid fa-code', name: 'Favourite Languages', type: 'select-multi', visibility: 'all', 'min:rep': 0, 'select-options': 'C++\nC\nJavascript\nPython\nAssembly' }, + { key: 'luckyNumber', icon: 'fa-solid fa-dice', name: 'Lucky Number', type: 'input-number', visibility: 'privileged', 'min:rep': 7 }, + { key: 'soccerTeam', icon: 'fa-regular fa-futbol', name: 'Soccer Team', type: 'select', visibility: 'all', 'min:rep': 0, 'select-options': 'Barcelona\nLiverpool\nArsenal\nGalatasaray\n' }, + ]; + await adminUser.saveCustomFields({ uid: adminUid }, fields); + }); + + it('should fail to update a field if user does not have enough reputation', async () => { + await assert.rejects( + user.updateProfile(lowRepUid, { + uid: lowRepUid, + luckyNumber: 13, + }), + { message: '[[error:not-enough-reputation-custom-field, 7, Lucky Number]]' }, + ); + }); + + it('should fail with invalid field data', async () => { + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + location: new Array(300).fill('a').join(''), + }), + { message: '[[error:custom-user-field-value-too-long, Location]]' }, + ); + + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + luckyNumber: 'not-a-number', + }), + { message: '[[error:custom-user-field-invalid-number, Lucky Number]]' }, + ); + + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + favouriteDate: 'not-a-date', + }), + { message: '[[error:custom-user-field-invalid-date, Anniversary]]' }, + ); + + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + website: 'not-a-url', + }), + { message: '[[error:custom-user-field-invalid-link, Website]]' }, + ); + + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + soccerTeam: 'not-in-options', + }), + { message: '[[error:custom-user-field-select-value-invalid, Soccer Team]]' }, + ); + + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + favouriteLanguages: '["not-in-options"]', + }), + { message: '[[error:custom-user-field-select-value-invalid, Favourite Languages]]' }, + ); + }); + + it('should update a users custom fields if they have enough reputation', async () => { + await user.updateProfile(highRepUid, { + uid: highRepUid, + website: 'https://nodebb.org', + location: 'Toronto', + favouriteDate: '2014-05-01', + favouriteLanguages: '["Javascript", "Python"]', + luckyNumber: 13, + soccerTeam: 'Galatasaray', + }); + + const { response, body } = await request.get(`${nconf.get('url')}/api/user/highrepuser`); + // console.log(body); + assert.strictEqual(body.website, 'https://nodebb.org'); + }); +}); From 54233dcdf0d66805bb60e2fb713a62aa3802e9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 17:52:26 -0500 Subject: [PATCH 24/27] test: add last one --- test/user/custom-fields.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/user/custom-fields.js b/test/user/custom-fields.js index c032fd47a8..8e4bb0e5c3 100644 --- a/test/user/custom-fields.js +++ b/test/user/custom-fields.js @@ -66,6 +66,14 @@ describe('custom user fields', () => { { message: '[[error:custom-user-field-invalid-number, Lucky Number]]' }, ); + await assert.rejects( + user.updateProfile(highRepUid, { + uid: highRepUid, + location: 'https://spam.com', + }), + { message: '[[error:custom-user-field-invalid-text, Location]]' }, + ); + await assert.rejects( user.updateProfile(highRepUid, { uid: highRepUid, @@ -110,8 +118,7 @@ describe('custom user fields', () => { soccerTeam: 'Galatasaray', }); - const { response, body } = await request.get(`${nconf.get('url')}/api/user/highrepuser`); - // console.log(body); + const { body } = await request.get(`${nconf.get('url')}/api/user/highrepuser`); assert.strictEqual(body.website, 'https://nodebb.org'); }); }); From e845afc120fd1a8803fefaff42ec2755c66cbe63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 17:59:41 -0500 Subject: [PATCH 25/27] test: missing service worker test --- test/controllers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/controllers.js b/test/controllers.js index d0eec251c7..bd5cd60fd0 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -219,6 +219,7 @@ describe('Controllers', () => { { it: 'should get topic data', url: `/api/v3/topics/1` }, { it: 'should get category data', url: `/api/v3/categories/1` }, { it: 'should return osd data', url: `/osd.xml` }, + { it: 'should load service worker', url: '/service-worker.js' }, ]; testRoutes.forEach((route) => { it(route.it, async () => { From 82538ca33314a24b6b02b974632b2818c080f700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Jan 2025 18:29:42 -0500 Subject: [PATCH 26/27] test: editor tests --- test/posts.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/posts.js b/test/posts.js index 20403e24cf..2ec2faf24c 100644 --- a/test/posts.js +++ b/test/posts.js @@ -86,7 +86,7 @@ describe('Post\'s', () => { assert.deepStrictEqual(await db.sortedSetScores(`tid:${postResult.topicData.tid}:posters`, [oldUid, newUid]), [2, null]); - await posts.changeOwner([pid1, pid2], newUid); + await socketPosts.changeOwner({ uid: globalModUid }, { pids: [pid1, pid2], toUid: newUid }); assert.deepStrictEqual(await db.sortedSetScores(`tid:${postResult.topicData.tid}:posters`, [oldUid, newUid]), [null, 2]); @@ -1072,6 +1072,65 @@ describe('Post\'s', () => { }); }); + describe('post editors', () => { + it('should fail with invalid data', async () => { + await assert.rejects( + socketPosts.saveEditors({ uid: 0 }, { + pid: 1, + uids: [1], + }), + { message: '[[error:no-privileges]]' }, + ); + await assert.rejects( + socketPosts.saveEditors({ uid: 0 }, null), + { message: '[[error:invalid-data]]' }, + ); + await assert.rejects( + socketPosts.saveEditors({ uid: 0 }, { + pid: null, + uids: [1], + }), + { message: '[[error:invalid-data]]' }, + ); + await assert.rejects( + socketPosts.saveEditors({ uid: 0 }, { + pid: 1, + uids: null, + }), + { message: '[[error:invalid-data]]' }, + ); + + await assert.rejects( + socketPosts.getEditors({ uid: 0 }, null), + { message: '[[error:invalid-data]]' }, + ); + + await assert.rejects( + socketPosts.saveEditors({ uid: 0 }, { pid: null }), + { message: '[[error:invalid-data]]' }, + ); + }); + + it('should add another user to post editors', async () => { + const ownerUid = await user.create({ username: 'owner user' }); + const editorUid = await user.create({ username: 'editor user' }); + const topic = await topics.post({ + uid: ownerUid, + cid, + title: 'just a topic for multi editor testing', + content: `Some text here for the OP`, + }); + const { pid } = topic.postData; + await socketPosts.saveEditors({ uid: ownerUid }, { + pid: pid, + uids: [editorUid], + }); + + const userData = await socketPosts.getEditors({ uid: ownerUid }, { pid: pid }); + assert.strictEqual(userData[0].username, 'editor user'); + }); + }); + describe('Topic Backlinks', () => { let tid1; before(async () => { From d05c203bb2bfc2a18b6e97fd03619b62beede65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 8 Jan 2025 09:43:08 -0500 Subject: [PATCH 27/27] fix: closes #13036, if image isn't loaded yet width is 0 --- public/openapi/read/config.yaml | 2 -- public/src/client/topic/images.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml index 3b38168c2e..5e6f05d91c 100644 --- a/public/openapi/read/config.yaml +++ b/public/openapi/read/config.yaml @@ -122,8 +122,6 @@ get: type: array items: type: string - resizeImageWidth: - type: number cookies: type: object properties: diff --git a/public/src/client/topic/images.js b/public/src/client/topic/images.js index 5fc2b34f66..c4c30319d8 100644 --- a/public/src/client/topic/images.js +++ b/public/src/client/topic/images.js @@ -19,7 +19,7 @@ define('forum/topic/images', [], function () { } if (!imageEl.parent().is('a')) { - if (utils.isRelativeUrl(src) && suffixRegex.test(src) && imageEl.get(0).naturalWidth >= config.resizeImageWidth) { + if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { src = src.replace(suffixRegex, '$1'); } const alt = imageEl.attr('alt') || '';