Skip to content

Commit

Permalink
test(valitor): Added tests for validator process to ensure correct fu…
Browse files Browse the repository at this point in the history
…nctionality under various conditions such as schema validation passing with no errors when all rules are met handling errors when fields fail validation skipping validation for schemas already validated by conditions throwing errors when conflicting schema options are used like pick and omit applied validation on fields with schema options respecting pick option skipping validation for omitted fields handling nested schema validation correctly validating fields with conditional dependencies between schemas testing handling of empty or undefined data and validating complex patterns in rules these tests cover different scenarios of validation like required rules field validation with pick and omit options skipping validated schemas validating nested schemas and ensuring error messages are correctly returned for failing rules
  • Loading branch information
m-mdy-m committed Jan 21, 2025
1 parent db49113 commit 30373db
Show file tree
Hide file tree
Showing 3 changed files with 428 additions and 0 deletions.
201 changes: 201 additions & 0 deletions test/unit/validator/FieldValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { describe, it, beforeEach, afterEach, after } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
import { RuleAction } from '../../../dist/validator/RuleAction';
import { FieldValidator } from '../../../dist/validator/FieldValidator';
import { DefaultMessages } from '../../../dist/validator/config';
import { ValidationField } from '../../../dist/common/interfaces';

describe('FieldValidator.validateField', () => {
let ruleActionStub: sinon.SinonStub;

beforeEach(() => {
ruleActionStub = sinon.stub(RuleAction, 'applyRule');
});

afterEach(() => {
ruleActionStub.restore();
ruleActionStub.reset();
});
after(() => {
sinon.restore();
});

// General use case: Validates a simple required field
it('should return no errors when all rules pass', async () => {
ruleActionStub.returns(true);

const result = await FieldValidator.validateField('validValue', { rules: ['required'], options: {} }, 'testField', false, {});

expect(result).to.be.empty;
});

// General use case: Fails on a single rule
it('should return an error when a rule fails', async () => {
ruleActionStub.withArgs({ ruleName: 'required' }, undefined).returns(false);

const result = await FieldValidator.validateField(undefined, { rules: ['required'], options: {} }, 'testField', false, {});

expect(result).to.deep.equal(['The testField field is required and cannot be left blank.']);
});

// Edge case: Inherits default rules
it('should apply inherited default rules', async () => {
ruleActionStub.withArgs({ ruleName: 'min', param: '5' }, '123').returns(false);

const result = await FieldValidator.validateField(
'123',
{ rules: ['inherit'], options: {} },
'testField',
false,
{},
['min:5'], // Default rules
);

expect(result).to.deep.equal([DefaultMessages.min?.replace('{value}', '5').replace('{field}', 'testField')]);
});

// Unique case: Validates custom function
it('should return an error when custom validation fails', async () => {
const customStub = sinon.stub().resolves(false);

const result = await FieldValidator.validateField('value', { rules: [], options: { custom: customStub }, messages: { custom: 'Custom error message' } }, 'testField', false, {});

expect(result).to.deep.equal(['Custom error message']);
sinon.assert.calledOnce(customStub);
});

// General use case: Validates multiple rules
it('should validate multiple rules and return all errors when `returnFirstError` is false', async () => {
ruleActionStub.withArgs({ ruleName: 'required' }, '').returns(false);
ruleActionStub.withArgs({ ruleName: 'min', param: '5' }, '').returns(false);

const result = await FieldValidator.validateField('', { rules: ['required', 'min:5'], options: {} }, 'testField', false, {});

expect(result).to.deep.equal([DefaultMessages.required?.replace('{field}', 'testField'), DefaultMessages.min?.replace('{value}', '5')?.replace('{field}', 'testField')]);
});

// Edge case: Stops validation on the first error
it('should stop on the first error when `returnFirstError` is true', async () => {
ruleActionStub.withArgs({ ruleName: 'required' }, '').returns(false);

const result = await FieldValidator.validateField('', { rules: ['required', 'min:5'], options: {} }, 'testField', true, {});

expect(result).to.deep.equal([DefaultMessages.required?.replace('{field}', 'testField')]);
});

describe('FieldValidator', () => {
describe('General Use Cases', () => {
it('should return an error for missing required value', async () => {
const value = '';
const fieldRules: ValidationField = { rules: ['required'] };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.have.lengthOf(1);
expect(result[0]).to.equal(DefaultMessages.required?.replace('{field}', 'testField'));
});

it('should return an error for values outside min or max range', async () => {
const value = 3;
const fieldRules: ValidationField = { rules: ['min:5'] };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.have.lengthOf(1);
expect(result[0]).to.equal(DefaultMessages.min?.replace('{field}', 'testField').replace('{value}', '5'));
});
});

it('should handle empty rules gracefully', async () => {
const value = 'test';
const fieldRules: ValidationField = { rules: [] };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.be.empty;
});

it('should validate custom rules asynchronously', async () => {
const value = 'test';
const fieldRules: ValidationField = {
rules: [],
options: {
custom: async (val) => val === 'test',
},
};
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.be.empty;
});

it('should return an error for failed custom validation', async () => {
const value = 'invalid';
const fieldRules: ValidationField = {
rules: [],
options: {
custom: async (val) => val === 'test',
},
messages: {
custom: 'Custom validation failed for {field}.',
},
};
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.have.lengthOf(1);
expect(result[0]).to.equal('Custom validation failed for testField.');
});

it('should handle dependent field validation', async () => {
const value = 'child';
const fieldRules: ValidationField = {
rules: [],
options: {
dependsOn: {
field: 'parentField',
operator: 'equal',
value: 'parent',
},
},
messages: {
dependsOn: '{field} requires {dependentField} to have a specific value.',
},
};
const allData = { parentField: 'parent' };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, allData);
expect(result).to.be.empty;
});

it('should return an error for failed dependent field validation', async () => {
const value = 'child';
const fieldRules: ValidationField = {
rules: [],
options: {
dependsOn: {
field: 'parentField',
operator: 'equal',
value: 'parent',
},
},
messages: {
dependsOn: '{field} requires {dependentField} to have a specific value.',
},
};
const allData = { parentField: 'differentValue' };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, allData);
expect(result).to.have.lengthOf(1);
expect(result[0]).to.equal('testField requires parentField to have a specific value.');
});

describe('Return First Error Mode', () => {
it('should return only the first error when returnFirstError is true', async () => {
const value = '';
const fieldRules: ValidationField = { rules: ['required', 'string'] };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', true, {});
expect(result).to.have.lengthOf(1);
expect(result[0]).to.equal(DefaultMessages.required?.replace('{field}', 'testField'));
});

it('should return all errors when returnFirstError is false', async () => {
const value = '';
const fieldRules: ValidationField = { rules: ['required', 'string'] };
const result = await FieldValidator.validateField(value, fieldRules, 'testField', false, {});
expect(result).to.have.lengthOf(2);
expect(result[0]).to.equal(DefaultMessages.required?.replace('{field}', 'testField'));
expect(result[1]).to.equal(DefaultMessages.string?.replace('{field}', 'testField'));
});
});
});
});
140 changes: 140 additions & 0 deletions test/unit/validator/RuleAction.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { expect } from 'chai';
import sinon from 'sinon';
import 'mocha';
import { RuleAction } from '../../../dist/validator/RuleAction';
import { describe, it } from 'mocha';
import Reflector from '../../../dist/metadata/';
import { createMockReflector } from '../../mocks/reflector.mock';

describe('RuleAction', () => {
describe('applyRule', () => {
it('should validate "required" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, 'value')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, null)).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, undefined)).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, '')).to.be.false;
});

it('should validate "string" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'string', param: '' }, 'test')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'string', param: '' }, 123)).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'string', param: '' }, null)).to.be.false;
});

it('should validate "integer" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'integer', param: '' }, 42)).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'integer', param: '' }, 42.5)).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'integer', param: '' }, '42')).to.be.false;
});

it('should validate "email" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'email', param: '' }, 'test@example.com')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'email', param: '' }, 'test.com')).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'email', param: '' }, '')).to.be.false;
});

it('should validate "min" and "max" rules correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'min', param: '5' }, 'hello')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'min', param: '5' }, 'hi')).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'max', param: '5' }, 'hello')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'max', param: '5' }, 'hello world')).to.be.false;
});

it('should validate "regex" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'regex', param: '^test$' }, 'test')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'regex', param: '^test$' }, 'fail')).to.be.false;
});

it('should validate "url" rule correctly', () => {
expect(RuleAction.applyRule({ ruleName: 'url', param: '' }, 'http://example.com')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'url', param: '' }, 'invalid-url')).to.be.false;
});
});

describe('applyDependencyRule', () => {
it('should validate "equal" dependency correctly', () => {
expect(RuleAction.applyDependencyRule({ operator: 'equal', dependencyValue: 'test' }, 'test')).to.be.true;
expect(RuleAction.applyDependencyRule({ operator: 'equal', dependencyValue: 'test' }, 'fail')).to.be.false;
});

it('should validate "greaterThan" dependency correctly', () => {
expect(RuleAction.applyDependencyRule({ operator: 'greaterThan', dependencyValue: 10 }, 15)).to.be.true;
expect(RuleAction.applyDependencyRule({ operator: 'greaterThan', dependencyValue: 10 }, 5)).to.be.false;
});

it('should validate "exists" dependency correctly', () => {
expect(RuleAction.applyDependencyRule({ operator: 'exists' }, 'value')).to.be.true;
expect(RuleAction.applyDependencyRule({ operator: 'exists' }, undefined)).to.be.false;
});
});

describe('filter', () => {
let mockReflector: ReturnType<typeof createMockReflector>['mockReflector'];
let restoreReflector: ReturnType<typeof createMockReflector>['restore'];

beforeEach(() => {
const mock = createMockReflector();
mockReflector = mock.mockReflector;
restoreReflector = mock.restore;
});
afterEach(() => {
sinon.restore();
restoreReflector();
});
it('should filter rules by pick option', () => {
const mockRules = {
username: { rules: 'string|min:5|max:20' },
email: { rules: 'string|email' },
age: { rules: 'integer|min:18' },
};

(Reflector.get as any).withArgs('validation:rules', sinon.match.any).returns(mockRules);

class MockSchema {}

const result = RuleAction.filter<any>(MockSchema, ['username', 'email']);

expect(result).to.have.keys(['username', 'email']);
expect(result).to.not.have.key('age');
});

it('should filter rules by omit option', () => {
const mockRules = {
username: { rules: 'string|min:5|max:20' },
email: { rules: 'string|email' },
age: { rules: 'integer|min:18' },
};
(Reflector.get as any).withArgs('validation:rules', sinon.match.any).returns(mockRules);
const result = RuleAction.filter<any>(class MockSchema {}, undefined, ['age']);
expect(result).to.have.keys(['username', 'email']);
});

it('should return all rules if no filter options are provided', () => {
const mockRules = {
username: { rules: 'string|min:5|max:20' },
email: { rules: 'string|email' },
age: { rules: 'integer|min:18' },
};
(Reflector.get as any).withArgs('validation:rules', sinon.match.any).returns(mockRules);
const result = RuleAction.filter(class MockSchema {}, undefined, undefined);
expect(result).to.have.keys(['username', 'email', 'age']);
});
});

describe('Edge Cases', () => {
it('should handle empty string with "min" rule', () => {
expect(RuleAction.applyRule({ ruleName: 'min', param: '0' }, '')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'min', param: '1' }, '')).to.be.false;
});

it('should handle null or undefined for rules', () => {
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, null)).to.be.false;
expect(RuleAction.applyRule({ ruleName: 'required', param: '' }, undefined)).to.be.false;
});

it('should validate "regex" rule with complex patterns', () => {
expect(RuleAction.applyRule({ ruleName: 'regex', param: '^[a-z]{3,5}$' }, 'test')).to.be.true;
expect(RuleAction.applyRule({ ruleName: 'regex', param: '^[a-z]{3,5}$' }, 'toolong')).to.be.false;
});
});
});
Loading

0 comments on commit 30373db

Please sign in to comment.