Writing test for Joi validation - node.js

I have the following model:
const mongoose = require("mongoose");
const Joi = require("#hapi/joi");
const activitySchema = new mongoose.Schema({
title: {
type: String,
maxlength: 255,
minlength: 3,
required: true
}
});
const Activity = mongoose.model("Activity", activitySchema);
function validateActivity(activity) {
const schema = Joi.object({
title: Joi.string().min(3).max(255).required()
});
return schema.validate(activity)
}
module.exports.Activity = Activity;
module.exports.validate = validateActivity;
And I'm writing a unit test for the validateActivity function. I am not sure about what's the best practice for writing these tests.
So far I came up with:
const {validateActivity} = require("../../models/activity");
describe("activity model", () => {
let mockActivity;
beforeEach(() => {
mockActivity = {
title: "123"
}
});
it("should return an error if no title is provided", () => {
delete mockActivity.title;
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/any.required/);
});
it("should return an error if title is not a string", () => {
mockActivity.title = { test: "test" };
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.base/);
});
it("should return an error if title is less than 3 chars", () => {
mockActivity.title = "12";
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.min/);
});
it("should return an error if title is more than 255 chars", () => {
mockActivity.title = Array(258).join("a");
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.max/);
});
});
So once I add several fields to my model the testing will be quite repetitive. Is it necessary to write a test for each scenario?

I guess it all depends on how strict and important you want your validation to be. If you have a data centric application where your bread and butter is data. if you don't test your data thoroughly you might end up in a bad situation.
Now imagine in your case you have a schema amd you don't have tests. If you for example remove max(255) condition, your all tests will pass (since you don't have any) and your customers will be able to insert data which is longer than 255 which obviously will be wrong.
I don't see any thing wrong in what you are doing. You can consider grouping stuff when you have multiple fields. In nutshell, you should test your schema thoroughly if data integrity is super important for you.

After viewing this comment https://stackoverflow.com/a/33588497/5733078 it seems that it would not make sense to check every scenario since we can trust that the validate function has already been tested within Joi.

Related

Mongoose unique NanoID

const user = new mongoose.Schema(
{
nano_id: {
type: String,
required: true,
default: () => nanoid(7),
index: { unique: true },
},
...
}
How to run again nanoid(7) if is not unique? (run automatically and not get any error in console)
There are two ways to do this:
Prevent the error from happening in the first place by searching for a document with a similar Nano ID, if a document exists, regenerate a new Nano ID using a recursive function.
const { customAlphabet } = require('nanoid');
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
const nanoid = customAlphabet(alphabet, 8);
// userRegistration controller or route...
async function uniqueNanoId(query): Promise<string> {
const nanoId = nanoid();
const sameNanoId = await User.findOne({ nano_id:nanoId });
if (sameNanoId) {
return uniqueNanoId(query);
}
return nanoId;
}
const nanoId = await uniqueNanoId();
const user = User.create({...userBody,nanoId});
//...
Catch the error - as #cachius hinted - and regenerate the unique Nano ID accordingly (not tested). Catching a duplicate key has been discussed here
Bonus: Ask yourself the question, do I really need both Default Mongoose IDs and Nano IDs? If not, then this is a simple solution.
// ...
_id: {
type: String,
default: () => nanoid(),
},
// ...
The database will throw an error you have to catch and react to by generating the same record again with a newly generated nanoid.

mongoose filter by multiple conditions and execute to update data

I am wondering what would be the best approach to make schema functions using mongoose. I have never used this so the way I think is somewhat limited, same goes for looking for docs, without knowing what's available, is not very efficient.
Through docs I found that either using findOneAndUpdate might solve the problem; but there are some constraints.
Here is the code I am planning to run:
models/Bookmark.js
const mongoose = require('mongoose')
const bookmarkItemSchema = new mongoose.Schema({
restaurantId: String,
cachedAttr: {
name: String,
latitude: Number,
longitude: Number,
},
})
const bookmarkListSchema = new mongoose.Schema({
listName: String,
items: [bookmarkItemSchema],
})
const bookmarkSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
lists: [bookmarkListSchema],
})
// const add = (lists, userId) => {
// let bookmark = Bookmark.findOne({userId})
// bookmark.lists.listName === lists.listName //current, new
// ? bookmark.lists.items.push(lists.items)
// : bookmark.lists.push(lists)
// return bookmark
// }
mongoose.model('Bookmark', bookmarkSchema)
Routes/bookmark.js
router.post('/bookmarks', async (req, res) => {
const {lists} = req.body
console.log(lists)
if (!lists) {
return res.status(422).send({error: 'You must provide lists'})
}
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
try {
// const bookmark = Bookmark.add(lists, req.user._id, obj)
// await bookmark.save()
// res.send(bookmark)
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){ // THIS IS UNDEFINED. How to get this object?
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
} catch (e) {
res.status(422).send({error: e.message})
}
})
The req.body looks like this:
{
"lists": {
"listName": "My Saved List",
"items": {
"restaurantId": "abcdefg",
"cachedAttr": {
"name": "abcdefg",
"latitude": 200,
"longitude": 200
}
}
}
}
Basically what I commented out in the models/Bookmark.js file is what I would really like to do.
If the userId's list name already exists, then I would like to just add an item to the list.
Otherwise, I would like to add a new list to the object.
What is the best approach for doing this? Is there a straight forward mongoose api that I could use for this problem? or do I need to make two separated function that would handle each case and make that as schema methods and handle it in the routes file?

How to add two records in a row?

When I want to add two records in sequence, only one record is added, on the second it throws an error due to the fact that it cannot create a field with such data:
"NOTES_ID is required","key: NOTES_ID, value: undefined, is not a
number"
How to create an entry for two related tables sequentially from the beginning for the main table, and then for the one that has the foreign key installed.
module.exports.create = async function (req, res) {
const stateMatrix = await StateMatrix.select().exec()
const noteObj = {
DATE: req.body.DATE,
TITLE: req.body.TITLE,
CONTENT: req.body.CONTENT
};
const noteStateObj = {
STATE_DATE: new Date().toLocaleDateString("en-US"),
STATES_ID: stateMatrix[0]._props.STATES_ID_CURR,
NOTES_ID: req.body.NOTE_ID,
USERS_ID: req.decoded.user_id
};
try {
await Notes.create(noteObj);
await NoteStates.create(noteStateObj);
res.status(201).json(noteObj, noteStateObj);
} catch (e) {
errorHandler(res, e);
}
};
Probably NoteStates is related to Notes through note_id field which can not be empty (I guess it's foreign key). It means that you should set it before saving noteStateObj:
// Something like this
const newNote = await Notes.create(noteObj);
noteStateObj.NOTES_ID = newNote.ID;
await NoteStates.create(noteStateObj);

Mocking mongoose validators with jest

I'm testing a mongoose model's validation, while trying to mock a validator, the model still has the reference to the original function, so the validation keeps calling the original function.
I want to test that the validator function get's called, however since the validator goes to the db I need to mock it.
This is my model:
const { hasRequiredCustoms } = require('../utils/validators')
const ProductSchema = new Schema({
customs: {
type: [String],
validate: hasRequiredCustoms // <- This is the validator
}
})
const Product = mongoose.model('Product', ProductSchema)
module.exports = Product
The original validators:
module.exports = {
hasRequiredCustoms(val) {
console.log('Original')
// validation logic
return result
},
//etc...
}
This is the mock for validators:
const validators = jest.genMockFromModule('../validators')
function hasRequiredCustoms (val) {
console.log('Mock')
return true
}
validators.hasRequiredCustoms = hasRequiredCustoms
module.exports = validators
And the test:
test('Should be invalid if required customs missing: price', done => {
jest.mock('../../utils/validators')
function callback(err) {
if (!err) done()
}
const m = new Product( validProduct )
m.validate(callback)
})
Every time I run the tests the console logs the Original. Why is the reference still going back to the original module? seems like I'm missing some super essential concept of how require works or the way mongoose stores the validators references.
Thanks for the help.

Mongoose versioning broken?

I'm sort of at a loss here. I was expecting that anytime an updated an object in mongoose the version would increment on __v. This does not appear to be the case. Am I missing something or is this a bug?
var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mongooseTest');
var Cat = mongoose.model('Cat', {
name: String,
manualVersion: Number,
arr: []
});
var kitty = new Cat({
name: 'Zildjian',
manualVersion: 0,
arr: []
});
kitty.save()
.then(x => {
x.manualVersion = x.manualVersion + 1;
//x.arr.push(x.manualVersion); <-- pushing here makes '__v' be correct
return x.save();
})
.then(() => Cat.findOne({}))
.then(x => {
x.manualVersion = x.manualVersion + 1;
//x.arr.push(x.manualVersion); <-- pushing here makes '__v' be correct
return x.save();
})
.then(() => Cat.findOne({}))
.then(x => {
console.log(x.toObject());
// RESULT (from console.log and what is also in the DB):
// {
// _id: 565386b058b2632c0886b160,
// name: 'Zildjian',
// manualVersion: 2,
// __v: 0,
// arr: []
// }
});
x.increment().save();
in the above example fixes my problem. 'save()' appears to be idempotent as well so its cool to just call on every save as long as you know you are making changes. According to one of the authors of mongoose versioning is only meant to be used with arrays and only increments for those. Ugh.
http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning
At the very least this should be clearly spelled out in the docs IMO as it is currently very ambigious...
http://mongoosejs.com/docs/guide.html#versionKey

Resources