Files
upstream-rocketchat/apps/meteor/tests/unit/server/lib/settingValidationRules.spec.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

198 lines
7.2 KiB
TypeScript

import { expect } from 'chai';
import p from 'proxyquire';
import sinon from 'sinon';
import { registerSettingSchema } from '../../../../server/settings/functions/settingSchemas';
import {
positiveOrDisabled,
notGreaterThanSetting,
notLowerThanSetting,
} from '../../../../server/settings/functions/validationRuleBuilders';
const settingsGetMock = sinon.stub();
const settingsGetSettingMock = sinon.stub();
const { validateSettingRules, SettingValidationError } = p.noCallThru().load('../../../../server/lib/settingValidationRules.ts', {
'@rocket.chat/logger': {
Logger: class {
error = (): undefined => undefined;
},
},
'../settings': { settings: { get: settingsGetMock, getSetting: settingsGetSettingMock } },
});
const validationBySettingId: Record<string, unknown> = {
Accounts_Password_Policy_MinLength: [positiveOrDisabled(), notGreaterThanSetting('Accounts_Password_Policy_MaxLength')],
Accounts_Password_Policy_MaxLength: [positiveOrDisabled(), notLowerThanSetting('Accounts_Password_Policy_MinLength')],
};
describe('validateSettingRules', () => {
beforeEach(() => {
settingsGetMock.reset();
settingsGetSettingMock.reset();
settingsGetSettingMock.callsFake((_id: string) => ({
_id,
type: 'int',
...(validationBySettingId[_id] ? { validation: JSON.stringify(validationBySettingId[_id]) } : {}),
}));
});
it('rejects an incoherent batch with the failed rule i18n key as the error message', () => {
const error = (() => {
try {
validateSettingRules([
{ _id: 'Accounts_Password_Policy_MinLength', value: 6 },
{ _id: 'Accounts_Password_Policy_MaxLength', value: 4 },
]);
} catch (thrown) {
return thrown as Error;
}
})();
expect(error).to.be.instanceOf(SettingValidationError);
expect(error?.message).to.equal('Accounts_Password_Policy_MinLength_Invalid');
});
it('resolves values batch-first, not from the stale cached value', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(14);
expect(() =>
validateSettingRules([
{ _id: 'Accounts_Password_Policy_MinLength', value: 6 },
{ _id: 'Accounts_Password_Policy_MaxLength', value: 10 },
]),
).to.not.throw();
});
it('rejects raising the minimum above the stored maximum, with the minimum-side message', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MaxLength').returns(10);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MinLength', value: 12 }])).to.throw(
'Accounts_Password_Policy_MinLength_Invalid',
);
});
it('rejects lowering the maximum below the stored minimum, with the maximum-side message', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(6);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MaxLength', value: 4 }])).to.throw(
'Accounts_Password_Policy_MaxLength_Invalid',
);
});
it('treats exactly -1 as a disabled bound, on either side', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MaxLength').returns(-1);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MinLength', value: 20 }])).to.not.throw();
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(-1);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MaxLength', value: 5 }])).to.not.throw();
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(6);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MaxLength', value: -1 }])).to.not.throw();
});
it('rejects bound values below 1 that are not exactly -1', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(6);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MaxLength', value: 0 }])).to.throw(
'Accounts_Password_Policy_MaxLength_Invalid',
);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MinLength', value: -5 }])).to.throw(
'Accounts_Password_Policy_MinLength_Invalid',
);
});
it('validates the pair even when the same batch disables the policy', () => {
expect(() =>
validateSettingRules([
{ _id: 'Accounts_Password_Policy_Enabled', value: false },
{ _id: 'Accounts_Password_Policy_MinLength', value: 6 },
{ _id: 'Accounts_Password_Policy_MaxLength', value: 4 },
]),
).to.throw('Accounts_Password_Policy_MinLength_Invalid');
});
it('does not evaluate rules of settings that are not in the batch', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MinLength').returns(6);
settingsGetMock.withArgs('Accounts_Password_Policy_MaxLength').returns(4);
expect(() => validateSettingRules([{ _id: 'Some_Unrelated_Setting', value: 10 }])).to.not.throw();
});
it('passes a rule referencing a setting that does not exist', () => {
settingsGetSettingMock.withArgs('Ref_Setting').returns({
_id: 'Ref_Setting',
type: 'int',
validation: '[{"query":{"value":{"$lte":{"$setting":"Missing_Setting"}}}}]',
});
expect(() => validateSettingRules([{ _id: 'Ref_Setting', value: 999 }])).to.not.throw();
});
it('skips malformed persisted rules instead of crashing the save', () => {
const malformed = [
'[{"query":null}]',
'[{"query":{"value":{"$gte":1}},"appliesWhen":"junk"}]',
'[{"query":{"value":{"$gte":1}},"appliesWhen":{"_id":"Gate"}}]',
];
for (const validation of malformed) {
settingsGetSettingMock.withArgs('Malformed_Setting').returns({ _id: 'Malformed_Setting', type: 'int', validation });
expect(() => validateSettingRules([{ _id: 'Malformed_Setting', value: 0 }]), validation).to.not.throw();
}
});
it('does not enforce the value type, leaving those checks to each save path', () => {
settingsGetMock.withArgs('Accounts_Password_Policy_MaxLength').returns(10);
expect(() => validateSettingRules([{ _id: 'Accounts_Password_Policy_MinLength', value: 5.5 }])).to.not.throw();
});
describe('JSON settings with a schema', () => {
const schema = {
type: 'object',
required: ['version'],
additionalProperties: false,
properties: { version: { const: 1 } },
};
beforeEach(() => {
registerSettingSchema('Json_Config', schema);
settingsGetSettingMock.withArgs('Json_Config').returns({
_id: 'Json_Config',
type: 'code',
code: 'application/json',
});
});
it('accepts a JSON document matching the schema', () => {
expect(() => validateSettingRules([{ _id: 'Json_Config', value: '{"version":1}' }])).to.not.throw();
});
it('rejects a JSON document violating the schema', () => {
expect(() => validateSettingRules([{ _id: 'Json_Config', value: '{"version":2}' }])).to.throw('Json_Config_Invalid');
});
it('rejects a value that is not parseable JSON', () => {
expect(() => validateSettingRules([{ _id: 'Json_Config', value: 'not json {' }])).to.throw('Json_Config_Invalid');
});
it('accepts an empty value, meaning the setting is unconfigured', () => {
expect(() => validateSettingRules([{ _id: 'Json_Config', value: '' }])).to.not.throw();
});
it('ignores a schema registered for a code setting that is not application/json', () => {
registerSettingSchema('Js_Code', schema);
settingsGetSettingMock.withArgs('Js_Code').returns({
_id: 'Js_Code',
type: 'code',
code: 'text/javascript',
});
expect(() => validateSettingRules([{ _id: 'Js_Code', value: 'not json {' }])).to.not.throw();
});
});
});