mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-11-03 20:45:58 +01:00 
			
		
		
		
	Merge branch 'master' into develop
This commit is contained in:
		@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -122,8 +122,6 @@ get:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  type: string
 | 
			
		||||
              resizeImageWidth:
 | 
			
		||||
                type: number
 | 
			
		||||
              cookies:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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') || '';
 | 
			
		||||
 
 | 
			
		||||
@@ -220,9 +220,9 @@ module.exports = function (utils, Benchpress, relative_path) {
 | 
			
		||||
 | 
			
		||||
	function renderTopicImage(topicObj) {
 | 
			
		||||
		if (topicObj.thumb) {
 | 
			
		||||
			return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />';
 | 
			
		||||
			return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.displayname + '" />';
 | 
			
		||||
		}
 | 
			
		||||
		return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.username + '" />';
 | 
			
		||||
		return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.displayname + '" />';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function renderDigestAvatar(block) {
 | 
			
		||||
@@ -319,7 +319,7 @@ module.exports = function (utils, Benchpress, relative_path) {
 | 
			
		||||
		let output = '';
 | 
			
		||||
 | 
			
		||||
		if (userObj.picture) {
 | 
			
		||||
			output += `<img${attr2String(attributes)} alt="${userObj.username}" loading="lazy" component="${component || 'avatar/picture'}" src="${userObj.picture}" style="${styles.join(' ')}" onError="this.remove()" itemprop="image" />`;
 | 
			
		||||
			output += `<img${attr2String(attributes)} alt="${userObj.displayname}" loading="lazy" component="${component || 'avatar/picture'}" src="${userObj.picture}" style="${styles.join(' ')}" onError="this.remove()" itemprop="image" />`;
 | 
			
		||||
		}
 | 
			
		||||
		output += `<span${attr2String(attributes)} component="${component || 'avatar/icon'}" style="${styles.join(' ')} background-color: ${userObj['icon:bgColor']}">${userObj['icon:text']}</span>`;
 | 
			
		||||
		return output;
 | 
			
		||||
@@ -393,7 +393,7 @@ module.exports = function (utils, Benchpress, relative_path) {
 | 
			
		||||
			</li>`;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return html;
 | 
			
		||||
		return html.join('');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function register() {
 | 
			
		||||
 
 | 
			
		||||
@@ -402,10 +402,10 @@ async function getTooltipData(uids) {
 | 
			
		||||
		uids = uids.slice(0, cutoff - 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const usernames = await user.getUsernamesByUids(uids);
 | 
			
		||||
	const users = await user.getUsersFields(uids, ['username']);
 | 
			
		||||
	return {
 | 
			
		||||
		otherCount,
 | 
			
		||||
		usernames,
 | 
			
		||||
		usernames: users.map(user => user.displayname),
 | 
			
		||||
		cutoff,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,8 @@ program
 | 
			
		||||
	.option('--log-level <level>', 'Default logging level to use', 'info')
 | 
			
		||||
	.option('--config <value>', '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;
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -194,7 +194,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) ?
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ 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');
 | 
			
		||||
 | 
			
		||||
const Controllers = module.exports;
 | 
			
		||||
@@ -126,7 +126,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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,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),
 | 
			
		||||
		];
 | 
			
		||||
 
 | 
			
		||||
@@ -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>', 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,6 +179,48 @@ 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>', 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` },
 | 
			
		||||
			{ it: 'should load service worker', url: '/service-worker.js' },
 | 
			
		||||
		];
 | 
			
		||||
		testRoutes.forEach((route) => {
 | 
			
		||||
			it(route.it, async () => {
 | 
			
		||||
				const { response, body } = await request.get(`${baseUrl}/${route.url}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -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 () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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'),
 | 
			
		||||
			'<span class="icon d-inline-flex justify-content-center align-items-center align-middle rounded-circle" style="background-color: #ff0000; border-color: #ff0000!important; color: #00ff00; background-image: url(/assets/uploads/image.png); background-size: auto; width:16; height: 16; font-size: 8px;"></span>'
 | 
			
		||||
		);
 | 
			
		||||
		assert.strictEqual(
 | 
			
		||||
			helpers.buildCategoryIcon({
 | 
			
		||||
				bgColor: '#ff0000',
 | 
			
		||||
				color: '#00ff00',
 | 
			
		||||
				backgroundImage: '/assets/uploads/image.png',
 | 
			
		||||
				imageClass: 'auto',
 | 
			
		||||
				icon: 'fa-book',
 | 
			
		||||
			}, 16, 'rounded-circle'),
 | 
			
		||||
			'<span class="icon d-inline-flex justify-content-center align-items-center align-middle rounded-circle" style="background-color: #ff0000; border-color: #ff0000!important; color: #00ff00; background-image: url(/assets/uploads/image.png); background-size: auto; width:16; height: 16; font-size: 8px;"><i class="fa fa-fw fa-book"></i></span>'
 | 
			
		||||
		);
 | 
			
		||||
		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', ''),
 | 
			
		||||
			`<a href="${nconf.get('relative_path')}/category/undefined" class="badge px-1 text-truncate text-decoration-none " style="color: #00ff00;background-color: #ff0000;border-color: #ff0000!important; max-width: 70vw;">\n\t\t\t\n\t\t\tCategory 1\n\t\t</a>`
 | 
			
		||||
		);
 | 
			
		||||
		assert.strictEqual(
 | 
			
		||||
			helpers.buildCategoryLabel({
 | 
			
		||||
				bgColor: '#ff0000',
 | 
			
		||||
				color: '#00ff00',
 | 
			
		||||
				backgroundImage: '/assets/uploads/image.png',
 | 
			
		||||
				imageClass: 'auto',
 | 
			
		||||
				name: 'Category 1',
 | 
			
		||||
				icon: 'fa-book',
 | 
			
		||||
			}, 'span', 'rounded-1'),
 | 
			
		||||
			`<span  class="badge px-1 text-truncate text-decoration-none rounded-1" style="color: #00ff00;background-color: #ff0000;border-color: #ff0000!important; max-width: 70vw;">\n\t\t\t<i class="fa fa-fw fa-book"></i>\n\t\t\tCategory 1\n\t\t</span>`,
 | 
			
		||||
		);
 | 
			
		||||
		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();
 | 
			
		||||
	});
 | 
			
		||||
@@ -169,16 +230,16 @@ 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, `<img src="${topicObj.thumb}" class="img-circle user-img" title="${topicObj.user.username}" />`);
 | 
			
		||||
		assert.equal(html, `<img src="${topicObj.thumb}" class="img-circle user-img" title="${topicObj.user.displayname}" />`);
 | 
			
		||||
		done();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	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, `<img component="user/picture" data-uid="${topicObj.user.uid}" src="${topicObj.user.picture}" class="user-img" title="${topicObj.user.username}" />`);
 | 
			
		||||
		assert.equal(html, `<img component="user/picture" data-uid="${topicObj.user.uid}" src="${topicObj.user.picture}" class="user-img" title="${topicObj.user.displayname}" />`);
 | 
			
		||||
		done();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -251,4 +312,34 @@ describe('helpers', () => {
 | 
			
		||||
		assert.equal(html, '<i class="fa fa-fw fa-question-circle"></i><i class="fa fa-fw fa-question-circle"></i>');
 | 
			
		||||
		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, ${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, ${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, ${nconf.get('relative_path')}/post/2, ${iso}]]`);
 | 
			
		||||
 | 
			
		||||
		str = helpers.generateWroteReplied(post, -1);
 | 
			
		||||
		assert.strictEqual(str, `[[topic:wrote-on, ${nconf.get('relative_path')}/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();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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)', () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										124
									
								
								test/user/custom-fields.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								test/user/custom-fields.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
'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,
 | 
			
		||||
				location: 'https://spam.com',
 | 
			
		||||
			}),
 | 
			
		||||
			{ message: '[[error:custom-user-field-invalid-text, Location]]' },
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		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 { body } = await request.get(`${nconf.get('url')}/api/user/highrepuser`);
 | 
			
		||||
		assert.strictEqual(body.website, 'https://nodebb.org');
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@@ -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' };
 | 
			
		||||
@@ -502,4 +501,77 @@ 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);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('Translator', () => {
 | 
			
		||||
		const shim = require('../src/translator');
 | 
			
		||||
 | 
			
		||||
		const { Translator } = shim;
 | 
			
		||||
		it('should translate in place', async () => {
 | 
			
		||||
			const translator = Translator.create('en-GB');
 | 
			
		||||
			const el = $(`<div><span id="search" title="[[global:search]]"></span><span id="text">[[global:home]]</span></div>`);
 | 
			
		||||
			await translator.translateInPlace(el.get(0));
 | 
			
		||||
			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();
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user