init
This commit is contained in:
168
tests/unit/middlewares/error.test.js
Normal file
168
tests/unit/middlewares/error.test.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const mongoose = require('mongoose');
|
||||
const httpStatus = require('http-status');
|
||||
const httpMocks = require('node-mocks-http');
|
||||
const { errorConverter, errorHandler } = require('../../../src/middlewares/error');
|
||||
const ApiError = require('../../../src/utils/ApiError');
|
||||
const config = require('../../../src/config/config');
|
||||
const logger = require('../../../src/config/logger');
|
||||
|
||||
describe('Error middlewares', () => {
|
||||
describe('Error converter', () => {
|
||||
test('should return the same ApiError object it was called with', () => {
|
||||
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
test('should convert an Error to ApiError and preserve its status and message', () => {
|
||||
const error = new Error('Any error');
|
||||
error.statusCode = httpStatus.BAD_REQUEST;
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
statusCode: error.statusCode,
|
||||
message: error.message,
|
||||
isOperational: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert an Error without status to ApiError with status 500', () => {
|
||||
const error = new Error('Any error');
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
statusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: error.message,
|
||||
isOperational: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert an Error without message to ApiError with default message of that http status', () => {
|
||||
const error = new Error();
|
||||
error.statusCode = httpStatus.BAD_REQUEST;
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
statusCode: error.statusCode,
|
||||
message: httpStatus[error.statusCode],
|
||||
isOperational: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert a Mongoose error to ApiError with status 400 and preserve its message', () => {
|
||||
const error = new mongoose.Error('Any mongoose error');
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
statusCode: httpStatus.BAD_REQUEST,
|
||||
message: error.message,
|
||||
isOperational: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should convert any other object to ApiError with status 500 and its message', () => {
|
||||
const error = {};
|
||||
const next = jest.fn();
|
||||
|
||||
errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(ApiError));
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
statusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
||||
isOperational: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handler', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test('should send proper error response and put the error message in res.locals', () => {
|
||||
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||
const res = httpMocks.createResponse();
|
||||
const sendSpy = jest.spyOn(res, 'send');
|
||||
|
||||
errorHandler(error, httpMocks.createRequest(), res);
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ code: error.statusCode, message: error.message }));
|
||||
expect(res.locals.errorMessage).toBe(error.message);
|
||||
});
|
||||
|
||||
test('should put the error stack in the response if in development mode', () => {
|
||||
config.env = 'development';
|
||||
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||
const res = httpMocks.createResponse();
|
||||
const sendSpy = jest.spyOn(res, 'send');
|
||||
|
||||
errorHandler(error, httpMocks.createRequest(), res);
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ code: error.statusCode, message: error.message, stack: error.stack })
|
||||
);
|
||||
config.env = process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
test('should send internal server error status and message if in production mode and error is not operational', () => {
|
||||
config.env = 'production';
|
||||
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error', false);
|
||||
const res = httpMocks.createResponse();
|
||||
const sendSpy = jest.spyOn(res, 'send');
|
||||
|
||||
errorHandler(error, httpMocks.createRequest(), res);
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
||||
})
|
||||
);
|
||||
expect(res.locals.errorMessage).toBe(error.message);
|
||||
config.env = process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
test('should preserve original error status and message if in production mode and error is operational', () => {
|
||||
config.env = 'production';
|
||||
const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');
|
||||
const res = httpMocks.createResponse();
|
||||
const sendSpy = jest.spyOn(res, 'send');
|
||||
|
||||
errorHandler(error, httpMocks.createRequest(), res);
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: error.statusCode,
|
||||
message: error.message,
|
||||
})
|
||||
);
|
||||
config.env = process.env.NODE_ENV;
|
||||
});
|
||||
});
|
||||
});
|
||||
61
tests/unit/models/plugins/paginate.plugin.test.js
Normal file
61
tests/unit/models/plugins/paginate.plugin.test.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const mongoose = require('mongoose');
|
||||
const setupTestDB = require('../../../utils/setupTestDB');
|
||||
const paginate = require('../../../../src/models/plugins/paginate.plugin');
|
||||
|
||||
const projectSchema = mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
projectSchema.virtual('tasks', {
|
||||
ref: 'Task',
|
||||
localField: '_id',
|
||||
foreignField: 'project',
|
||||
});
|
||||
|
||||
projectSchema.plugin(paginate);
|
||||
const Project = mongoose.model('Project', projectSchema);
|
||||
|
||||
const taskSchema = mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
project: {
|
||||
type: mongoose.SchemaTypes.ObjectId,
|
||||
ref: 'Project',
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
taskSchema.plugin(paginate);
|
||||
const Task = mongoose.model('Task', taskSchema);
|
||||
|
||||
setupTestDB();
|
||||
|
||||
describe('paginate plugin', () => {
|
||||
describe('populate option', () => {
|
||||
test('should populate the specified data fields', async () => {
|
||||
const project = await Project.create({ name: 'Project One' });
|
||||
const task = await Task.create({ name: 'Task One', project: project._id });
|
||||
|
||||
const taskPages = await Task.paginate({ _id: task._id }, { populate: 'project' });
|
||||
|
||||
expect(taskPages.results[0].project).toHaveProperty('_id', project._id);
|
||||
});
|
||||
|
||||
test('should populate nested fields', async () => {
|
||||
const project = await Project.create({ name: 'Project One' });
|
||||
const task = await Task.create({ name: 'Task One', project: project._id });
|
||||
|
||||
const projectPages = await Project.paginate({ _id: project._id }, { populate: 'tasks.project' });
|
||||
const { tasks } = projectPages.results[0];
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0]).toHaveProperty('_id', task._id);
|
||||
expect(tasks[0].project).toHaveProperty('_id', project._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
89
tests/unit/models/plugins/toJSON.plugin.test.js
Normal file
89
tests/unit/models/plugins/toJSON.plugin.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { toJSON } = require('../../../../src/models/plugins');
|
||||
|
||||
describe('toJSON plugin', () => {
|
||||
let connection;
|
||||
|
||||
beforeEach(() => {
|
||||
connection = mongoose.createConnection();
|
||||
});
|
||||
|
||||
it('should replace _id with id', () => {
|
||||
const schema = mongoose.Schema();
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model();
|
||||
expect(doc.toJSON()).not.toHaveProperty('_id');
|
||||
expect(doc.toJSON()).toHaveProperty('id', doc._id.toString());
|
||||
});
|
||||
|
||||
it('should remove __v', () => {
|
||||
const schema = mongoose.Schema();
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model();
|
||||
expect(doc.toJSON()).not.toHaveProperty('__v');
|
||||
});
|
||||
|
||||
it('should remove createdAt and updatedAt', () => {
|
||||
const schema = mongoose.Schema({}, { timestamps: true });
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model();
|
||||
expect(doc.toJSON()).not.toHaveProperty('createdAt');
|
||||
expect(doc.toJSON()).not.toHaveProperty('updatedAt');
|
||||
});
|
||||
|
||||
it('should remove any path set as private', () => {
|
||||
const schema = mongoose.Schema({
|
||||
public: { type: String },
|
||||
private: { type: String, private: true },
|
||||
});
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model({ public: 'some public value', private: 'some private value' });
|
||||
expect(doc.toJSON()).not.toHaveProperty('private');
|
||||
expect(doc.toJSON()).toHaveProperty('public');
|
||||
});
|
||||
|
||||
it('should remove any nested paths set as private', () => {
|
||||
const schema = mongoose.Schema({
|
||||
public: { type: String },
|
||||
nested: {
|
||||
private: { type: String, private: true },
|
||||
},
|
||||
});
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model({
|
||||
public: 'some public value',
|
||||
nested: {
|
||||
private: 'some nested private value',
|
||||
},
|
||||
});
|
||||
expect(doc.toJSON()).not.toHaveProperty('nested.private');
|
||||
expect(doc.toJSON()).toHaveProperty('public');
|
||||
});
|
||||
|
||||
it('should also call the schema toJSON transform function', () => {
|
||||
const schema = mongoose.Schema(
|
||||
{
|
||||
public: { type: String },
|
||||
private: { type: String },
|
||||
},
|
||||
{
|
||||
toJSON: {
|
||||
transform: (doc, ret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete ret.private;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
schema.plugin(toJSON);
|
||||
const Model = connection.model('Model', schema);
|
||||
const doc = new Model({ public: 'some public value', private: 'some private value' });
|
||||
expect(doc.toJSON()).not.toHaveProperty('private');
|
||||
expect(doc.toJSON()).toHaveProperty('public');
|
||||
});
|
||||
});
|
||||
57
tests/unit/models/user.model.test.js
Normal file
57
tests/unit/models/user.model.test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const faker = require('faker');
|
||||
const { User } = require('../../../src/models');
|
||||
|
||||
describe('User model', () => {
|
||||
describe('User validation', () => {
|
||||
let newUser;
|
||||
beforeEach(() => {
|
||||
newUser = {
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email().toLowerCase(),
|
||||
password: 'password1',
|
||||
role: 'user',
|
||||
};
|
||||
});
|
||||
|
||||
test('should correctly validate a valid user', async () => {
|
||||
await expect(new User(newUser).validate()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test('should throw a validation error if email is invalid', async () => {
|
||||
newUser.email = 'invalidEmail';
|
||||
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw a validation error if password length is less than 8 characters', async () => {
|
||||
newUser.password = 'passwo1';
|
||||
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw a validation error if password does not contain numbers', async () => {
|
||||
newUser.password = 'password';
|
||||
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw a validation error if password does not contain letters', async () => {
|
||||
newUser.password = '11111111';
|
||||
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw a validation error if role is unknown', async () => {
|
||||
newUser.role = 'invalid';
|
||||
await expect(new User(newUser).validate()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User toJSON()', () => {
|
||||
test('should not return user password when toJSON is called', () => {
|
||||
const newUser = {
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email().toLowerCase(),
|
||||
password: 'password1',
|
||||
role: 'user',
|
||||
};
|
||||
expect(new User(newUser).toJSON()).not.toHaveProperty('password');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user