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
198 lines
7.2 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|