first batch of unit tests for the unique validator

This commit is contained in:
Dieter Stinglhamber 2021-11-26 13:18:16 +01:00
parent 2b625210d7
commit d3364e0ae8
2 changed files with 766 additions and 12 deletions

View File

@ -0,0 +1,744 @@
'use strict';
const strapiUtils = require('@strapi/utils');
const { YupValidationError } = require('../../../../../utils/lib/errors');
const entityValidator = require('../validators');
describe('Entity validator', () => {
const fakeFindOne = jest.fn();
global.strapi = {
db: {
query: jest.fn(() => ({
findOne: fakeFindOne,
})),
},
};
afterEach(() => {
jest.clearAllMocks();
fakeFindOne.mockReset();
});
describe('String unique validator', () => {
const fakeModel = {
kind: 'contentType',
modelName: 'test-model',
uid: 'test-uid',
privateAttributes: [],
options: {},
attributes: {
attrStringUnique: { type: 'string', unique: true },
},
};
test('it does not validates the unique constraint if the attribute is not set as unique', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string' },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: null,
data: 'non-unique-test-data',
}
)
);
await validator('non-unique-test-data');
expect(fakeFindOne).not.toBeCalled();
});
test('it validates the unique constraint if there is no other record in the database', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: null,
data: 'non-unique-test-data',
}
)
);
expect(await validator('non-unique-test-data')).toBe('non-unique-test-data');
});
test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => {
expect.assertions(1);
fakeFindOne.mockResolvedValueOnce({ attrStringUnique: 'unique-test-data' });
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: null,
data: 'unique-test-data',
}
)
);
try {
await validator('unique-test-data');
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
}
});
test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => {
fakeFindOne.mockResolvedValueOnce({ attrStringUnique: 'non-updated-unique-test-data' });
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: { id: 1, attrStringUnique: 'non-updated-unique-test-data' },
data: 'non-updated-unique-test-data',
}
)
);
expect(await validator('non-updated-unique-test-data')).toBe('non-updated-unique-test-data');
});
test('it checks the database for records with the same value for the checked attribute', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: null,
data: 'test-data',
}
)
);
await validator('test-data');
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrStringUnique: 'test-data' },
});
});
test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.string(
{ type: 'string', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrStringUnique',
entity: { id: 1 },
data: 'test-data',
}
)
);
await validator('test-data');
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { $and: [{ attrStringUnique: 'test-data' }, { $not: { id: 1 } }] },
});
});
});
describe('Integer unique validator', () => {
const fakeModel = {
kind: 'contentType',
uid: 'test-uid',
modelName: 'test-model',
privateAttributes: [],
options: {},
attributes: {
attrIntegerUnique: { type: 'integer', unique: true },
},
};
test('it does not validates the unique constraint if the attribute is not set as unique', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer' },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: null,
data: 1,
}
)
);
await validator(1);
expect(fakeFindOne).not.toBeCalled();
});
test('it validates the unique constraint if there is no other record in the database', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: null,
data: 1,
}
)
);
expect(await validator(1)).toBe(1);
});
test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => {
expect.assertions(1);
fakeFindOne.mockResolvedValueOnce({ attrIntegerUnique: 2 });
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: null,
data: 2,
}
)
);
try {
await validator(2);
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
}
});
test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => {
fakeFindOne.mockResolvedValueOnce({ attrIntegerUnique: 3 });
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: { attrIntegerUnique: 3 },
data: 3,
}
)
);
expect(await validator(3)).toBe(3);
});
test('it checks the database for records with the same value for the checked attribute', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: null,
data: 4,
}
)
);
await validator(4);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrIntegerUnique: 4 },
});
});
test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.integer(
{ type: 'integer', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrIntegerUnique',
entity: { id: 1 },
data: 5,
}
)
);
await validator(5);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { $and: [{ attrIntegerUnique: 5 }, { $not: { id: 1 } }] },
});
});
});
describe('BigInteger unique validator', () => {
const fakeModel = {
kind: 'contentType',
modelName: 'test-model',
uid: 'test-uid',
privateAttributes: [],
options: {},
attributes: {
attrBigIntegerUnique: { type: 'biginteger', unique: true },
},
};
test('it does not validates the unique constraint if the attribute is not set as unique', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger' },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: null,
data: 1,
}
)
);
await validator(1);
expect(fakeFindOne).not.toBeCalled();
});
test('it validates the unique constraint if there is no other record in the database', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: null,
data: 1,
}
)
);
expect(await validator(1)).toBe(1);
});
test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => {
expect.assertions(1);
fakeFindOne.mockResolvedValueOnce({ attrBigIntegerUnique: 2 });
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: null,
data: 2,
}
)
);
try {
await validator(2);
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
}
});
test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => {
fakeFindOne.mockResolvedValueOnce({ attrBigIntegerUnique: 3 });
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: { attrBigIntegerUnique: 3 },
data: 3,
}
)
);
expect(await validator(3)).toBe(3);
});
test('it checks the database for records with the same value for the checked attribute', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: null,
data: 4,
}
)
);
await validator(4);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrBigIntegerUnique: 4 },
});
});
test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.biginteger(
{ type: 'biginteger', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrBigIntegerUnique',
entity: { id: 1 },
data: 5,
}
)
);
await validator(5);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { $and: [{ attrBigIntegerUnique: 5 }, { $not: { id: 1 } }] },
});
});
});
describe('Float unique validator', () => {
const fakeModel = {
kind: 'contentType',
modelName: 'test-model',
uid: 'test-uid',
privateAttributes: [],
options: {},
attributes: {
attrFloatUnique: { type: 'float', unique: true },
},
};
test('it does not validates the unique constraint if the attribute is not set as unique', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float' },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: null,
data: 1,
}
)
);
await validator(1);
expect(fakeFindOne).not.toBeCalled();
});
test('it validates the unique constraint if there is no other record in the database', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: null,
data: 1,
}
)
);
expect(await validator(1)).toBe(1);
});
test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => {
expect.assertions(1);
fakeFindOne.mockResolvedValueOnce({ attrFloatUnique: 2 });
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: null,
data: 2,
}
)
);
try {
await validator(2);
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
}
});
test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => {
fakeFindOne.mockResolvedValueOnce({ attrFloatUnique: 3 });
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: { attrFloatUnique: 3 },
data: 3,
}
)
);
expect(await validator(3)).toBe(3);
});
test('it checks the database for records with the same value for the checked attribute', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: null,
data: 4,
}
)
);
await validator(4);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrFloatUnique: 4 },
});
});
test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.float(
{ type: 'float', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrFloatUnique',
entity: { id: 1 },
data: 5,
}
)
);
await validator(5);
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { $and: [{ attrFloatUnique: 5 }, { $not: { id: 1 } }] },
});
});
});
describe('UID unique validator', () => {
const fakeModel = {
kind: 'contentType',
modelName: 'test-model',
uid: 'test-uid',
privateAttributes: [],
options: {},
attributes: {
attrUidUnique: { type: 'uid' },
},
};
test('it validates the unique constraint if there is no other record in the database', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: null,
data: 'non-unique-uid',
}
)
);
expect(await validator('non-unique-uid')).toBe('non-unique-uid');
});
test('it always validates the unique constraint even if the attribute is not set as unique', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid' },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: null,
data: 'non-unique-uid',
}
)
);
expect(await validator('non-unique-uid')).toBe('non-unique-uid');
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrUidUnique: 'non-unique-uid' },
});
});
test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => {
expect.assertions(1);
fakeFindOne.mockResolvedValueOnce({ attrUidUnique: 'unique-uid' });
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: null,
data: 'unique-uid',
}
)
);
try {
await validator(2);
} catch (err) {
expect(err).toBeInstanceOf(YupValidationError);
}
});
test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => {
fakeFindOne.mockResolvedValueOnce({ attrUidUnique: 'unchanged-unique-uid' });
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: { attrUidUnique: 'unchanged-unique-uid' },
data: 'unchanged-unique-uid',
}
)
);
expect(await validator('unchanged-unique-uid')).toBe('unchanged-unique-uid');
});
test('it checks the database for records with the same value for the checked attribute', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: null,
data: 'unique-uid',
}
)
);
await validator('unique-uid');
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { attrUidUnique: 'unique-uid' },
});
});
test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => {
fakeFindOne.mockResolvedValueOnce(null);
const validator = strapiUtils.validateYupSchema(
entityValidator.uid(
{ type: 'uid', unique: true },
{
isDraft: false,
model: fakeModel,
attributeName: 'attrUidUnique',
entity: { id: 1 },
data: 'unique-uid',
}
)
);
await validator('unique-uid');
expect(fakeFindOne).toHaveBeenCalledWith({
select: ['id'],
where: { $and: [{ attrUidUnique: 'unique-uid' }, { $not: { id: 1 } }] },
});
});
});
});

View File

@ -9,7 +9,7 @@ const { yup } = require('@strapi/utils');
*/
const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, entity, data }) => {
return fns.reduce((validator, fn) => {
return fn(attr, validator, { isDraft, model, attributeName, entity, data });
return fn(attr, { isDraft, model, attributeName, entity, data }, validator);
}, yup.mixed());
};
@ -20,7 +20,7 @@ const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, en
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinLengthValidator = ({ minLength }, validator, { isDraft }) =>
const addMinLengthValidator = ({ minLength }, { isDraft }, validator) =>
_.isInteger(minLength) && !isDraft ? validator.min(minLength) : validator;
/**
@ -28,7 +28,7 @@ const addMinLengthValidator = ({ minLength }, validator, { isDraft }) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxLengthValidator = ({ maxLength }, validator) =>
const addMaxLengthValidator = ({ maxLength }, __, validator) =>
_.isInteger(maxLength) ? validator.max(maxLength) : validator;
/**
@ -36,7 +36,7 @@ const addMaxLengthValidator = ({ maxLength }, validator) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinIntegerValidator = ({ min }, validator) =>
const addMinIntegerValidator = ({ min }, __, validator) =>
_.isNumber(min) ? validator.min(_.toInteger(min)) : validator;
/**
@ -44,7 +44,7 @@ const addMinIntegerValidator = ({ min }, validator) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxIntegerValidator = ({ max }, validator) =>
const addMaxIntegerValidator = ({ max }, __, validator) =>
_.isNumber(max) ? validator.max(_.toInteger(max)) : validator;
/**
@ -52,7 +52,7 @@ const addMaxIntegerValidator = ({ max }, validator) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinFloatValidator = ({ min }, validator) =>
const addMinFloatValidator = ({ min }, __, validator) =>
_.isNumber(min) ? validator.min(min) : validator;
/**
@ -60,7 +60,7 @@ const addMinFloatValidator = ({ min }, validator) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxFloatValidator = ({ max }, validator) =>
const addMaxFloatValidator = ({ max }, __, validator) =>
_.isNumber(max) ? validator.max(max) : validator;
/**
@ -68,10 +68,18 @@ const addMaxFloatValidator = ({ max }, validator) =>
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addStringRegexValidator = ({ regex }, validator) =>
const addStringRegexValidator = ({ regex }, __, validator) =>
_.isUndefined(regex) ? validator : validator.matches(new RegExp(regex));
const addUniqueValidator = (attr, validator, { model, attributeName, entity, data }) => {
const addUniqueValidator = (attr, { model, attributeName, entity, data }, validator) => {
/**
* If the attribute value is `null` we want to skip the unique validation.
* Otherwise it'll only accept a single `null` entry in the database.
*/
if (data === null) {
return validator;
}
/**
* If the attribute is unchanged we skip the unique verification. This will
* prevent the validator to be triggered in case the user activated the
@ -93,7 +101,7 @@ const addUniqueValidator = (attr, validator, { model, attributeName, entity, dat
where: whereParams,
});
return !!record;
return !record;
});
}
@ -110,9 +118,11 @@ const stringValidator = composeValidators(
addUniqueValidator
);
const emailValidator = composeValidators(stringValidator, (attr, validator) => validator.email());
const emailValidator = composeValidators(stringValidator, (attr, __, validator) =>
validator.email()
);
const uidValidator = composeValidators(stringValidator, (attr, validator) =>
const uidValidator = composeValidators(stringValidator, (attr, __, validator) =>
validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$'))
);