How to add custom validator function in Joi? - javascript

I have Joi schema and want to add a custom validator for validating data which isn't possible with default Joi validators.
Currently, I'm using the version 16.1.7 of Joi
const method = (value, helpers) => {
// for example if the username value is (something) then it will throw an error with flowing message but it throws an error inside (value) object without error message. It should throw error inside the (error) object with a proper error message
if (value === "something") {
return new Error("something is not allowed as username");
}
// Return the value unchanged
return value;
};
const createProfileSchema = Joi.object().keys({
username: Joi.string()
.required()
.trim()
.empty()
.min(5)
.max(20)
.lowercase()
.custom(method, "custom validation")
});
const { error,value } = createProfileSchema.validate({ username: "something" });
console.log(value); // returns {username: Error}
console.log(error); // returns undefined
But I couldn't implement it the right way. I read Joi documents but it seems a little bit confusing to me. Can anyone help me to figure it out?

const Joi = require('#hapi/joi');
Joi.object({
password: Joi
.string()
.custom((value, helper) => {
if (value.length < 8) {
return helper.message("Password must be at least 8 characters long")
} else {
return true
}
})
}).validate({
password: '1234'
});

Your custom method must be like this:
const method = (value, helpers) => {
// for example if the username value is (something) then it will throw an error with flowing message but it throws an error inside (value) object without error message. It should throw error inside the (error) object with a proper error message
if (value === "something") {
return helpers.error("any.invalid");
}
// Return the value unchanged
return value;
};
Docs:
https://github.com/hapijs/joi/blob/master/API.md#anycustommethod-description
Output for value :
{ username: 'something' }
Output for error:
[Error [ValidationError]: "username" contains an invalid value] {
_original: { username: 'something' },
details: [
{
message: '"username" contains an invalid value',
path: [Array],
type: 'any.invalid',
context: [Object]
}
]
}

This is how I validated my code, have a look at it and try to format yours
const busInput = (req) => {
const schema = Joi.object().keys({
routId: Joi.number().integer().required().min(1)
.max(150),
bus_plate: Joi.string().required().min(5),
currentLocation: Joi.string().required().custom((value, helper) => {
const coordinates = req.body.currentLocation.split(',');
const lat = coordinates[0].trim();
const long = coordinates[1].trim();
const valRegex = /-?\d/;
if (!valRegex.test(lat)) {
return helper.message('Laltitude must be numbers');
}
if (!valRegex.test(long)) {
return helper.message('Longitude must be numbers');
}
}),
bus_status: Joi.string().required().valid('active', 'inactive'),
});
return schema.validate(req.body);
};

Related

Joi requiring fields when it shoudnt be

So I have some posts validation using #hapi/joi 17.1.1 and in there I have two fields: textfield and picture. Im not requring any of the fields yet still it is saying that picture is required.
posts validation
module.exports.postsValidation = (data) => {
const schema = Joi.object({
textfield: Joi.string().max(280),
picture: Joi.string(),
});
return schema.validate(data);
};
posts.js (where im using validation)
router.post("/create", authenticateToken, async (req, res) => {
try {
if ((req.body.textfield == "") & (req.body.picture == "")) {
return res.status(400).json("Fill one of the fields");
}
const { error } = postsValidation(req.body);
if (error) return res.status(400).json(error.details[0].message);
// Getting info for new post
const newPost = new Post({
textfield: req.body.textfield,
picture: req.body.picture,
ownerId: req.user._id,
});
// Saving new post
await newPost.save();
res.json(newPost);
} catch (error) {
res.sendStatus(500);
}
});
when I log out the error it says this
[Error [ValidationError]: "picture" is not allowed to be empty] {
_original: { textfield: 'sssss', picture: '' },
details: [
{
message: '"picture" is not allowed to be empty',
path: [Array],
type: 'string.empty',
context: [Object]
}
]
}
Can anyone please tell whats going on?
This is because you are sending the prop picture from the FE side and it's an empty string ''. You should add an .allow('') your validation picture: Joi.string().allow('') if you want to save an empty string inside the DB or change the FE side to not send the picture prop at all if the string is empty.
Just in case the value is null too
module.exports.postsValidation = (data) => {
const schema = Joi.object({
textfield: Joi.string().max(280),
picture: Joi.string().allow("", null),
});
return schema.validate(data);
};

Mongoose Throw Error When Non Existing Key is Present

I have a code where I am updating my schema object with request body. I have applied validation rules on the schema. The problem is, I want the schema to throw an error when there's a non existing field in the request body. Non existing key doesn't save to the database as I want but I want to throw some error instead of saving the object. Schema:
const peopleSchema = new mongoose.Schema(
{
fullname: {
type: String,
required: [true, "fullname is required"],
validate: [(value) => isAlpha(value, "en-US", {ignore: " "}), "name should be alphabetic only"],
},
phone: {
type: String,
validate: [isPhone, "please enter a valid phone number"],
},
address: String,
},
{ timestamps: true }
);
Code to update person:
router.put("/:id", checkUser, async (req, res, next) => {
try {
const { id } = req.params;
const user = req.currentUser;
const person = user.people.id(id);
person.set(req.body);
const response = await user.save();
res.json({ response });
} catch (err) {
next(new BadRequestError(err));
}
});
for validation there are two way based on callback and async approache ,
because your code is based on async/await you must to use validateSync() like the following code:
let errors = user.validateSync()//check validation
if(errors){
console.log(errors)
throw errors;//handle your error
}
const response = await user.save()
in callback method :
user.save(function(err,response){
if (err){
console.log(err);
//handle error
}
else{
console.log(response)
res.json({ response });
}
})

How can I make sure that this function handles errors properly?

I have a function that checks user input in an express application. I don't want to use any library to validate those inputs so I declared an array where errors are pushed into.
I have embedded the middleware function as a static method in a class...
static postAdchecker(req, res, next) {
let { state, price, manufacturer, model, bodytype } = req.body;
console.log('req', req.body);
const errors = [];
// If state is empty or undefined throw this error
if (!state) {
console.log('state', state);
const error = {
message: 'Please specify the state of the car'
};
errors.push(error);
}
// If state is supplied, convert it to lowercase, trim and check if value is new/used
if (state.toLowerCase().trim() !== 'new' && state.toLowerCase().trim() !== 'used') {
const error = {
message: 'State can only be new or used'
};
errors.push(error);
}
// Same goes for the others.
if (!price) {
const error = {
message: 'You will need to specify a sale price'
};
errors.push(error);
}
if (!manufacturer) {
const error = {
message: 'Specify a manufacturer'
};
errors.push(error);
}
if (!model) {
const error = {
message: 'Specify a model'
};
errors.push(error);
}
if (!bodytype) {
const error = {
message: 'You will need to specify a bodytype'
};
errors.push(error);
}
return res.status(400).json({
status: 400,
errors: {
body: errors.map(err => err.message)
}
});
console.log('errors', errors);
req.body.state = state.toLowerCase().trim();
req.body.price = price.toLowerCase().trim();
req.body.manufacturer = manufacturer.toLowerCase().trim();
req.body.model = model.toLowerCase().trim();
req.body.bodytype = bodytype.toLowerCase().trim();
// req.authData;
return next();
}
How can I achieve the following?
Convert the values in the input field to lowercase and trim when supplied.
When there are errors, return all the errors.
When there are no errors, transfer operation to the next function instead of returning an empty array.
You are just missing one condition:
if(errors.length) { // <<<
return res.status(400).json({
status: 400,
errors: {
body: errors.map(err => err.message)
}
});
}

Jest : Testing for type or null

I have a test where i want to test if my received object value types match a schema. Prob is that for some keys i may receive something or null
I tried this so far
const attendeeSchema = {
birthDate: expect.extend(toBeTypeOrNull("Date")),
contact: expect.extend(toBeTypeOrNull(String)),
createdAt: expect.any(Date),
firstName: expect.any(String),
id: expect.any(Number),
idDevice: expect.extend(toBeTypeOrNull(Number)),
information: expect.extend(toBeTypeOrNull(String)),
lastName: expect.any(String),
macAddress: expect.extend(toBeTypeOrNull(String)),
updatedAt: expect.any(Date),
// state: toBeTypeOrNull()
};
const toBeTypeOrNull = (received, argument) => {
const pass = expect(received).toEqual(expect.any(argument));
if (pass || received === null) {
return {
message: () => `Ok`,
pass: true
};
} else {
return {
message: () => `expected ${received} to be ${argument} type or null`,
pass: false
};
}
};
and on my test
expect(res.result.data).toBe(attendeeSchema);
i also tried tobeEqual and other stuff....
my test doenst pass with
TypeError: any() expects to be passed a constructor function. Please pass one or use anything() to match any object.
I have no idea what to do here..
If someone has an idea
Thanks
All the previously given responses improperly use expect() in their implementation, so they don't really work.
What you want is a jest matcher that works like any() but accepts null values and doesn't use expect() functions in its implementation. You can implement this as an extension by basically copying the original any() implementation (from Jasmine), but with a null test added to the start:
expect.extend({
nullOrAny(received, expected) {
if (received === null) {
return {
pass: true,
message: () => `expected null or instance of ${this.utils.printExpected(expected) }, but received ${ this.utils.printReceived(received) }`
};
}
if (expected == String) {
return {
pass: typeof received == 'string' || received instanceof String,
message: () => `expected null or instance of ${this.utils.printExpected(expected) }, but received ${ this.utils.printReceived(received) }`
};
}
if (expected == Number) {
return {
pass: typeof received == 'number' || received instanceof Number,
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
if (expected == Function) {
return {
pass: typeof received == 'function' || received instanceof Function,
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
if (expected == Object) {
return {
pass: received !== null && typeof received == 'object',
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
if (expected == Boolean) {
return {
pass: typeof received == 'boolean',
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
/* jshint -W122 */
/* global Symbol */
if (typeof Symbol != 'undefined' && this.expectedObject == Symbol) {
return {
pass: typeof received == 'symbol',
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
/* jshint +W122 */
return {
pass: received instanceof expected,
message: () => `expected null or instance of ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
};
}
});
throw the above in a .js file, then point to that file with the jest setupFilesAfterEnv configuration variable. Now you can run your tests like:
const schema = {
person: expect.nullOrAny(Person),
age: expect.nullOrAny(Number)
};
expect(object).toEqual(schema);
Adding on the previous two answers a more common approach would be to wrap the core matcher in a try/catch block
expect.extend({
toBeTypeOrNull(received, classTypeOrNull) {
try {
expect(received).toEqual(expect.any(classTypeOrNull));
return {
message: () => `Ok`,
pass: true
};
} catch (error) {
return received === null
? {
message: () => `Ok`,
pass: true
}
: {
message: () => `expected ${received} to be ${classTypeOrNull} type or null`,
pass: false
};
}
}
});
I actually don't know Jest at all, but I took a look because code testing interests me at the moment.
From what i see in the expect.extend documentation it seems that you are using it the wrong way. You are currently giving it the result of the call to toBeTypeOrNull, for example in birthDate: expect.extend(toBeTypeOrNull("Date")), not the function itself. This probably causes the call to have an undefined argument because it's declared with 2 arguments (received, argument). argument is then undefined and you can't do expect.any(argument) inside your custom function.
From what i see in the docs, you are supposed to call extend at the beginning with an object which contains all your custom functions so you can use them later. Try this code and don't hesitate to comment if something goes wrong:
Update: for the differences between objectContaining and toMatchObject, see this answer
expect.extend({
toBeTypeOrNull(received, argument) {
const pass = expect(received).toEqual(expect.any(argument));
if (pass || received === null) {
return {
message: () => `Ok`,
pass: true
};
} else {
return {
message: () => `expected ${received} to be ${argument} type or null`,
pass: false
};
}
}
});
//your code that starts the test and gets the data
expect(res.result.data).toMatchObject({
birthDate: expect.toBeTypeOrNull(Date),
contact: expect.toBeTypeOrNull(String),
createdAt: expect.any(Date),
firstName: expect.any(String),
id: expect.any(Number),
idDevice: expect.toBeTypeOrNull(Number),
information: expect.toBeTypeOrNull(String),
lastName: expect.any(String),
macAddress: expect.toBeTypeOrNull(String),
updatedAt: expect.any(Date),
// state: toBeTypeOrNull()
});
You can use toBeOneOf from jest-extended.
After you have installed jest-extended to make expect.toBeOneOf available:
const attendeeSchema = {
birthDate: expect.toBeOneOf([expect.any(Date), null]),
contact: expect.toBeOneOf([expect.any(String), null]),
createdAt: expect.any(Date),
firstName: expect.any(String),
id: expect.any(Number),
idDevice: expect.toBeOneOf([expect.any(Number), null]),
information: expect.toBeOneOf([expect.any(String), null]),
lastName: expect.any(String),
macAddress: expect.toBeOneOf([expect.any(String), null]),
updatedAt: expect.any(Date),
};
expect(res.result.data).toEqual(attendeeSchema);
I was looking for something to validate any type or null and came across this answer, which was 95% right. The problem was in the line as the expectation was failing trying to compare null with argument.
const pass = expect(received).toEqual(expect.any(argument));
And as a bonus, I have a created toBeObjectContainingOrNull. Here is my code:
const expect = require("expect");
const okObject = {
message: () => "Ok",
pass: true
};
expect.extend({
toBeTypeOrNull(received, argument) {
if (received === null)
return okObject;
if (expect(received).toEqual(expect.any(argument))) {
return okObject;
} else {
return {
message: () => `expected ${received} to be ${argument} type or null`,
pass: false
};
}
},
toBeObjectContainingOrNull(received, argument) {
if (received === null)
return okObject;
const pass = expect(received).toEqual(expect.objectContaining(argument));
if (pass) {
return okObject;
} else {
return {
message: () => `expected ${received} to be ${argument} type or null`,
pass: false
};
}
}
});
module.exports = { expect };
Then you can use toBeObjectContainingOrNull as follows:
const userImageSchema = {
displayName: expect.any(String),
image: expect.toBeObjectContainingOrNull({
type: "Buffer",
data: expect.any(Array)
}),
orgs: expect.any(Array)
};
I hope it helps.
Instead of making specific handler I decided to make a general "any of":
expect.extend({
toEqualAnyOf(received: any, argument: any[]) {
const found = argument.some((eqItem) => {
// undefined
if (typeof eqItem === 'undefined' && typeof received === 'undefined') {
return true;
}
// null
if (eqItem === null && received === null) {
return true;
}
// any expect.<any> or direct value
try {
expect(received).toEqual(eqItem);
return true;
} catch (e) {
return false;
}
});
return found
? {
message: () => 'Ok',
pass: true,
}
: {
message: () => `expected ${received} to be any of ${argument}`,
pass: false,
};
},
});
module.exports = { expect };
And it can be used like this:
const userImageSchema = {
displayName: expect.any(String),
// null or undefined or part of structure or instance of a class
image: expect.toEqualAnyOf([
null,
undefined,
expect.objectContaining({
type: 'Buffer',
data: expect.any(Array),
}),
expect.any(ImageClass),
]),
orgs: expect.any(Array),
};
Already implemented in expect-more-jest
This will work:
require('expect-more-jest');
test("test 1 2 3", async () => {
expect(null).toBeNullableOf(String)
expect('String').toBeNullableOf(expect.any(String))
expect('String').not.toBeNullableOf(expect.any(Number))
expect('String').toBeNullableOf('String')
expect(1).toBeNullableOf(expect.any(Number))
expect(1).not.toBeNullableOf(expect.any(String))
expect(1).not.toBeNullableOf(String)
}

asserting against thrown error objects in jest

I have a function which throws an object, how can I assert that the correct object is thrown in jest?
it('should throw', () => {
const errorObj = {
myError: {
name: 'myError',
desc: 'myDescription'
}
};
const fn = () => {
throw errorObj;
}
expect(() => fn()).toThrowError(errorObj);
});
https://repl.it/repls/FrayedViolentBoa
If you are looking to test the contents of a custom error (which I think is what you are trying to do). You could catch the error then perform an assertion afterwards.
it('should throw', () => {
let thrownError;
try {
fn();
}
catch(error) {
thrownError = error;
}
expect(thrownError).toEqual(expectedErrorObj);
});
As Dez has suggested the toThrowError function will not work if you do not throw an instance of a javascript Error object. However, you could create your custom error by decorating an instance of an error object.
e.g.
let myError = new Error('some message');
myError.data = { name: 'myError',
desc: 'myDescription' };
throw myError;
Then once you had caught the error in your test you could test the custom contents of the error.
expect(thrownError.data).toEqual({ name: 'myError',
desc: 'myDescription' });
You need to throw a Javascript Error object, so the Jest toThrowError method identifies that an error has been thrown. Also the toThrowError looks to match the message of the error thrown or if an error has been thrown if you just check by .toThrowError().
it('should throw', () => {
const errorObj = {
myError: {
name: 'myError',
desc: 'myDescription'
}
};
const fn = () => {
throw new Error(errorObj.myError.desc);
}
expect(() => fn()).toThrowError("myDescription");
});
If you want to check the whole object is being passed as it is, you need to check it like this:
it('should throw', () => {
const errorObj = {
myError: {
name: 'myError',
desc: 'myDescription'
}
};
const fn = () => {
throw errorObj;
}
expect(() => fn()).toThrowError(new Error(errorObj));
});
It's known issue in jest, see https://github.com/facebook/jest/issues/8140
Meanwhile, here is my workaround - https://github.com/DanielHreben/jest-matcher-specific-error
If the objective is to check partial content of error, we can use Jest expect.objectContaining to help us keep code simple and check object payload returned as error :
const invalidJob = () => {
throw {
type: '/errors/invalid-job',
message: 'This job is invalid',
};
};
expect(() => invalidJob()).toThrowError(
expect.objectContaining({
type: '/errors/invalid-job',
})
);
Also possible with nested objects :
const invalidJob = () => {
throw {
response: {
type: '/errors/invalid-job',
message: 'This job is invalid',
},
status: 400
};
};
expect(() => invalidJob()).toThrowError(
expect.objectContaining({
status: 400,
response: expect.objectContaining({
type: '/errors/invalid-job'
})
})
);
You could add a custom matcher.
1. Custom Matcher
import { CsrfError } from '../src/shield';
declare global {
namespace jest {
interface Matchers<R> {
toThrowCsrfError(expected: {
statusCode: number;
message: string;
}): CustomMatcherResult;
}
}
}
const mismatchResult = (message: string) => ({
pass: false,
message: () => message,
});
expect.extend({
toThrowCsrfError(received, expected): jest.CustomMatcherResult {
try {
received();
} catch (error) {
const isCsrfError = error instanceof CsrfError;
if (!isCsrfError) {
return mismatchResult('Not an CsrfError Error');
}
if (error.message !== expected.message) {
return mismatchResult(
`Recieved Message "${error.message}" different from expected "${expected.message}"`
);
}
if (error.statusCode !== expected.statusCode) {
return mismatchResult(
`Recieved statusCode "${error.statusCode}" different from expected "${expected.statusCode}"`
);
}
return {
pass: true,
message: () => ``,
};
}
return {
pass: false,
message: () => `Expected to throw, but didn't`,
};
},
});
2. Add to setupFilesAfterEnv
In your jest.config add the file above to your setupFilesAfterEnv list, for example:
const config = {
setupFilesAfterEnv: ['./test/matchers.ts'],
};
module.exports = config;
3. Call
expect(() => {
shield({ pathname: 'https://example.com/' });
}).toThrowCsrfError({ statusCode: 401, message: 'No CSRF cookie.' });
When I need to test a custom error (subclass of Error), I use the following approach:
export class CustomError extends Error {
constructor(public code: string, public data: any) {
super(`Custom Error`);
}
}
export async function method(): Promise<void> {
throw new CustomError('ABC001', { field: 'X' });
}
// test:
it('should throw a CustomError for field X', () => {
expect.assertions(1); // Expects for an error
return method().catch(e => {
expect(Object.assign({}, e)).toStrictEqual({
code: 'ABC001',
data: { field: 'X' }
});
});
});

Categories

Resources