extracts out tests into separate files to establish a better testing pattern

This commit is contained in:
James
2020-04-16 15:03:16 -04:00
parent 5699bb6ecd
commit f4faefbd7e
24 changed files with 213 additions and 447 deletions

View File

@@ -12,14 +12,12 @@ const payload = new Payload({
exports.payload = payload;
exports.start = (cb) => {
expressApp.listen(config.port, () => {
const server = expressApp.listen(config.port, () => {
console.log(`listening on ${config.port}...`);
if (cb) cb();
});
};
exports.close = (cb) => {
if (expressApp) expressApp.close(cb);
return server;
};
// when app.js is launched directly

6
jest.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
verbose: true,
testEnvironment: 'node',
globalSetup: '<rootDir>/src/tests/globalSetup.js',
globalTeardown: '<rootDir>/src/tests/globalTeardown.js',
};

View File

@@ -3,15 +3,9 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"nodemonConfig": {
"ignore": [
"src/*",
"demo/client/*"
]
},
"scripts": {
"test:unit": "cross-env NODE_ENV=test jest --config src/tests/jest.config.js",
"test:int": "cross-env NODE_ENV=test jest src/tests/integration/api.spec.js --forceExit --detectOpenHandles",
"test:int": "cross-env NODE_ENV=test jest --forceExit",
"cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage",
"dev": "nodemon demo/server.js",
"server": "node demo/server.js",

View File

@@ -4,42 +4,19 @@
const faker = require('faker');
const server = require('../../../demo/server');
const config = require('../../../demo/payload.config');
const { email, password } = require('../../tests/credentials');
describe('API', () => {
const url = 'http://localhost:3000';
let token = null;
const email = 'test@test.com';
const password = 'test123';
const url = config.serverURL;
let localizedPostID;
const englishPostDesc = faker.lorem.lines(20);
const spanishPostDesc = faker.lorem.lines(20);
let token = null;
beforeAll((done) => {
server.start(done);
});
let localizedPostID;
const englishPostDesc = faker.lorem.lines(20);
const spanishPostDesc = faker.lorem.lines(20);
it('should register a first user', async () => {
const response = await fetch(`${url}/api/first-register`, {
body: JSON.stringify({
email,
password,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toHaveProperty('email');
expect(data).toHaveProperty('role');
expect(data).toHaveProperty('createdAt');
});
it('should login a user successfully', async () => {
describe('Collection CRUD', () => {
beforeAll(async () => {
const response = await fetch(`${url}/api/login`, {
body: JSON.stringify({
email,
@@ -53,33 +30,9 @@ describe('API', () => {
const data = await response.json();
expect(response.status).toBe(200);
expect(data.token).not.toBeNull();
({ token } = data);
});
it('should allow a user to be created', async () => {
const response = await fetch(`${url}/api/users/register`, {
body: JSON.stringify({
email: `${faker.name.firstName()}@test.com`,
password,
}),
headers: {
Authorization: `JWT ${token}`,
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toHaveProperty('email');
expect(data).toHaveProperty('role');
expect(data).toHaveProperty('createdAt');
});
it('should allow a post to be created in English', async () => {
const response = await fetch(`${url}/api/posts`, {
body: JSON.stringify({

View File

@@ -23,6 +23,7 @@ const updateHandler = async (req, res) => {
doc,
});
} catch (err) {
console.log(err);
return res.status(httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(err, 'mongoose'));
}
};

2
src/tests/credentials.js Normal file
View File

@@ -0,0 +1,2 @@
exports.email = 'test@test.com';
exports.password = 'test123';

26
src/tests/globalSetup.js Normal file
View File

@@ -0,0 +1,26 @@
const server = require('../../demo/server');
const config = require('../../demo/payload.config');
const { email, password } = require('./credentials');
const url = config.serverURL;
const usernameField = config.user.auth.useAsUsername;
const globalSetup = async () => {
global.PAYLOAD_SERVER = await server.start();
const response = await fetch(`${url}/api/first-register`, {
body: JSON.stringify({
[usernameField]: email,
password,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
global.AUTH_TOKEN = data.token;
};
module.exports = globalSetup;

View File

@@ -0,0 +1,6 @@
const globalTeardown = async () => {
const serverClosePromise = new Promise(resolve => global.PAYLOAD_SERVER.close(resolve));
await serverClosePromise;
};
module.exports = globalTeardown;

View File

@@ -1,8 +0,0 @@
const defaults = require('./jest.config');
module.exports = {
...defaults,
roots: [
'./integration'
],
};

View File

@@ -1,11 +0,0 @@
module.exports = {
verbose: true,
testURL: 'http://localhost/',
roots: [
'./unit'
],
transform: {
'^.+\\.(j|t)s$': 'babel-jest'
},
testEnvironment: 'node',
};

View File

@@ -1,99 +0,0 @@
const mockExpress = require('jest-mock-express');
const localizationMiddleware = require('../../localization/middleware');
let res = null;
let next = null;
describe('Payload Middleware', () => {
beforeEach(() => {
res = mockExpress.response();
next = jest.fn();
});
describe('Payload Locale Middleware', () => {
let req, localization;
beforeEach(() => {
req = {
query: {},
headers: {},
body: {}
};
localization = {
locales: ['en', 'es'],
defaultLocale: 'en'
};
});
it('Supports query params', () => {
req.query.locale = 'es';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual(req.query.locale);
});
it('Supports query param fallback to default', () => {
req.query.locale = 'pt';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual(localization.defaultLocale);
});
it('Supports accept-language header', () => {
req.headers['accept-language'] = 'es,fr;';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual('es');
});
it('Supports accept-language header fallback', () => {
req.query.locale = 'pt';
req.headers['accept-language'] = 'fr;';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual(localization.defaultLocale);
});
it('Query param takes precedence over header', () => {
req.query.locale = 'es';
req.headers['accept-language'] = 'en;';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual('es');
});
it('Supports default locale', () => {
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual(localization.defaultLocale);
});
it('Supports locale all', () => {
req.query.locale = '*';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toBeUndefined();
});
it('Supports locale in body on post', () => {
req.body = {locale: 'es'};
req.method = 'post';
localizationMiddleware(localization)(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(req.locale).toEqual('es');
});
});
});

View File

@@ -1,21 +0,0 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import mongooseApiQuery from '../../mongoose/buildQuery.plugin';
const TestUserSchema = new Schema({
name: {type: String},
description: {type: String},
married: {type: Boolean},
age: {type: Number}
}
);
TestUserSchema.plugin(mongooseApiQuery);
const TestUser = mongoose.model('TestUser', TestUserSchema);
describe('mongooseApiQuery', () => {
it('Should not blow up', () => {
expect(mongooseApiQuery).not.toBeNull();
});
});

View File

@@ -1,26 +0,0 @@
import SchemaLoader from '../../mongoose/schema/schemaLoader';
import config from '../../../demo/payload.config';
let schemaLoader;
describe('schemaLoader', () => {
beforeAll(async () => {
schemaLoader = new SchemaLoader(config);
console.log('before done');
});
it('load collections', async () => {
expect(schemaLoader.collections).not.toBeNull();
});
it('load globals', async () => {
expect(schemaLoader.globalModel).not.toBeNull();
expect(schemaLoader.globals).not.toBeNull();
});
it('load blocks', async () => {
expect(schemaLoader.blockSchema).not.toBeNull();
expect(schemaLoader.contentBlocks).not.toBeNull();
});
});

View File

@@ -1,126 +0,0 @@
/* eslint-disable camelcase */
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import { intlModel } from './testModels/IntlModel';
import { paramParser } from '../../mongoose/buildQuery.plugin';
const AuthorSchema = new Schema({
name: String,
publish_count: Number
});
const PageSchema = new Schema({
title: { type: String, unique: true },
author: AuthorSchema,
content: { type: String },
metaTitle: String,
likes: { type: Number }
});
const Page = mongoose.model('Page', PageSchema);
describe('Param Parser', () => {
describe('Parameter Parsing', () => {
it('No params', () => {
let parsed = paramParser(Page, {});
expect(parsed.searchParams).toEqual({});
});
it('Property Equals - Object property', () => {
let parsed = paramParser(Page, { title: 'This is my title' });
expect(parsed.searchParams).toEqual({ title: 'This is my title' });
});
it('Property Equals - String property', () => {
let parsed = paramParser(Page, { metaTitle: 'This is my title' });
expect(parsed.searchParams).toEqual({ metaTitle: 'This is my title' });
});
it('Multiple params', () => {
let parsed = paramParser(Page, { title: 'This is my title', metaTitle: 'this-is-my-title' });
expect(parsed.searchParams).toEqual({ title: 'This is my title', metaTitle: 'this-is-my-title' });
});
it('Greater than or equal', () => {
let parsed = paramParser(Page, { likes: '{gte}50' });
expect(parsed.searchParams).toEqual({ likes: { '$gte': '50' } });
});
it('Greater than, less than', () => {
let parsed = paramParser(Page, { likes: '{gte}50{lt}100' });
expect(parsed.searchParams).toEqual({ likes: { '$gte': '50', '$lt': '100' } });
});
it('Like', () => {
let parsed = paramParser(Page, { title: '{like}This' });
expect(parsed.searchParams).toEqual({ title: { '$regex': 'This', '$options': '-i' } });
});
describe('SubSchemas', () => {
it('Parse subschema for String', () => {
let parsed = paramParser(Page, { 'author.name': 'Jane' });
expect(parsed.searchParams).toEqual({ 'author.name': 'Jane' })
});
it('Parse subschema for Number', () => {
let parsed = paramParser(Page, { 'author.publish_count': '7' });
expect(parsed.searchParams).toEqual({ 'author.publish_count': '7' })
})
});
describe('Locale handling', () => {
it('should handle intl string property', () => {
let parsed = paramParser(intlModel, { title: 'This is my title' }, 'en');
expect(parsed.searchParams).toEqual({ 'title.en': 'This is my title'});
});
it('should handle intl string property', () => {
let parsed = paramParser(intlModel, { title: 'This is my title' }, 'en');
expect(parsed.searchParams).toEqual({ 'title.en': 'This is my title'});
});
});
});
describe('Include', () => {
it('Include Single', () => {
let parsed = paramParser(Page, { include: 'SomeId' });
expect(parsed.searchParams).toEqual({ _id: 'SomeId' });
});
it('Include Multiple', () => {
let parsed = paramParser(Page, { include: 'SomeId,SomeSecondId' });
expect(parsed.searchParams)
.toEqual({ '$or': [{ _id: 'SomeId' }, { _id: 'SomeSecondId' }] });
});
});
describe('Exclude', () => {
it('Exclude Single', () => {
let parsed = paramParser(Page, { exclude: 'SomeId' });
expect(parsed.searchParams).toEqual({ _id: { '$ne': 'SomeId' } });
});
it('Exclude Multiple', () => {
let parsed = paramParser(Page, { exclude: 'SomeId,SomeSecondId' });
expect(parsed.searchParams)
.toEqual({ '$and': [{ _id: { '$ne': 'SomeId' } }, { _id: { '$ne': 'SomeSecondId' } }] });
});
});
describe('Ordering/Sorting', () => {
it('Order by ascending (default)', () => {
let parsed = paramParser(Page, { sort_by: 'title' });
expect(parsed).toEqual({ searchParams: {}, sort: { title: 1 } });
});
it('Order by ascending', () => {
let parsed = paramParser(Page, { sort_by: 'title,asc' });
expect(parsed).toEqual({ searchParams: {}, sort: { title: 1 } });
});
it('Order by descending', () => {
let parsed = paramParser(Page, { sort_by: 'title,desc' });
expect(parsed).toEqual({ searchParams: {}, sort: { title: 'desc' } });
})
});
});

View File

@@ -1,29 +0,0 @@
import mongoose from 'mongoose';
import { schemaBaseFields } from '../../../mongoose/schema/schemaBaseFields';
import paginate from '../../../mongoose/paginate.plugin';
import buildQueryPlugin from '../../../mongoose/buildQuery.plugin';
import localizationPlugin from '../../../localization/localization.plugin';
const IntlSchema = new mongoose.Schema({
...schemaBaseFields,
title: { type: String, localized: true, unique: true },
content: { type: String, localized: true },
metaTitle: String,
metaDesc: String
},
{ timestamps: true }
);
IntlSchema.plugin(paginate);
IntlSchema.plugin(buildQueryPlugin);
IntlSchema.plugin(localizationPlugin, {
locales: [
'en',
'es'
],
defaultLocale: 'en',
fallback: true
});
const intlModel = mongoose.model('IntlModel', IntlSchema);
export { intlModel };

View File

@@ -1,7 +0,0 @@
import express from 'express';
export function getConfiguredExpress() {
let expressApp = express();
expressApp.set('view engine', 'pug');
return expressApp;
}

View File

@@ -1,7 +0,0 @@
describe('troubleshooting', () => {
describe('plugins', () => {
it('should return all records', () => {
});
});
});

View File

@@ -1,6 +1,5 @@
const checkIfInitialized = require('./checkIfInitialized');
const login = require('./login');
const me = require('./me');
const refresh = require('./refresh');
const register = require('./register');
const init = require('./init');
@@ -10,7 +9,6 @@ const resetPassword = require('./resetPassword');
module.exports = {
checkIfInitialized,
login,
me,
refresh,
init,
register,

View File

@@ -0,0 +1,55 @@
const jwt = require('jsonwebtoken');
const refresh = async (args) => {
try {
// Await validation here
let options = {
config: args.config,
api: args.api,
authorization: args.authorization,
};
// /////////////////////////////////////
// 1. Execute before refresh hook
// /////////////////////////////////////
const beforeRefreshHook = args.config.user && args.config.user.hooks && args.config.user.hooks.beforeRefresh;
if (typeof beforeRefreshHook === 'function') {
options = await beforeRefreshHook(options);
}
// /////////////////////////////////////
// 2. Perform refresh
// /////////////////////////////////////
const secret = options.config.user.auth.secretKey;
const opts = {};
opts.expiresIn = options.config.user.auth.tokenExpiration;
const token = options.authorization.replace('JWT ', '');
jwt.verify(token, secret, {});
const refreshedToken = jwt.sign(token, secret);
// /////////////////////////////////////
// 3. Execute after login hook
// /////////////////////////////////////
const afterRefreshHook = args.config.user && args.config.user.hooks && args.config.user.hooks.afterRefresh;
if (typeof afterRefreshHook === 'function') {
await afterRefreshHook(options, refreshedToken);
}
// /////////////////////////////////////
// 4. Return refreshed token
// /////////////////////////////////////
return refreshedToken;
} catch (error) {
throw error;
}
};
module.exports = refresh;

View File

@@ -1,11 +1,5 @@
/**
* Returns User if user session is still open
* @param req
* @param res
* @returns {*}
*/
const me = (req, res) => {
return res.status(200).send(req.user);
const meHandler = async (req, res) => {
return res.status(200).json(req.user);
};
module.exports = me;
module.exports = meHandler;

View File

@@ -1,35 +1,22 @@
const jwt = require('jsonwebtoken');
const httpStatus = require('http-status');
const { Forbidden, APIError } = require('../../errors');
const formatErrorResponse = require('../../express/responses/formatError');
const { refresh } = require('../operations');
/**
* Refresh an expired or soon to be expired auth token
* @param req
* @param res
* @param next
*/
const refresh = config => (req, res, next) => {
const secret = config.user.auth.secretKey;
const opts = {};
opts.expiresIn = config.user.auth.tokenExpiration;
const refreshHandler = config => async (req, res) => {
try {
const token = req.headers.authorization.replace('JWT ', '');
jwt.verify(token, secret, {});
const refreshedToken = jwt.sign(token, secret);
res.status(200)
.json({
message: 'Token Refresh Successful',
refreshedToken,
});
} catch (e) {
if (e.status && e.status === 401) {
return res.status(httpStatus.FORBIDDEN).send(formatErrorResponse(new Forbidden()));
}
const refreshedToken = await refresh({
config,
api: 'REST',
authorization: req.headers.authorization,
});
return res.status(httpStatus.INTERNAL_SERVER_ERROR).send(formatErrorResponse(new APIError()));
res.status(200).json({
message: 'Token refresh successful',
refreshedToken,
});
} catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error));
}
};
module.exports = refresh;
module.exports = refreshHandler;

88
src/users/users.spec.js Normal file
View File

@@ -0,0 +1,88 @@
require('isomorphic-fetch');
const faker = require('faker');
const { email, password } = require('../tests/credentials');
/**
* @jest-environment node
*/
const config = require('../../demo/payload.config');
describe('Users REST API', () => {
const url = config.serverURL;
const usernameField = config.user.auth.useAsUsername;
let token = null;
it('should prevent registering a first user', async () => {
const response = await fetch(`${url}/api/first-register`, {
body: JSON.stringify({
[usernameField]: 'thisuser@shouldbeprevented.com',
password: 'get-out',
}),
headers: {
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
expect(response.status).toBe(403);
});
it('should login a user successfully', async () => {
const response = await fetch(`${url}/api/login`, {
body: JSON.stringify({
[usernameField]: email,
password,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
expect(response.status).toBe(200);
expect(data.token).not.toBeNull();
({ token } = data);
});
it('should return a logged in user from /me', async () => {
const response = await fetch(`${url}/api/me`, {
method: 'post',
headers: {
Authorization: `JWT ${token}`,
},
});
const data = await response.json();
expect(response.status).toBe(200);
expect(data[usernameField]).not.toBeNull();
});
it('should allow a user to be created', async () => {
const response = await fetch(`${url}/api/users/register`, {
body: JSON.stringify({
[usernameField]: `${faker.name.firstName()}@test.com`,
password,
}),
headers: {
Authorization: `JWT ${token}`,
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toHaveProperty(usernameField);
expect(data).toHaveProperty('role');
expect(data).toHaveProperty('createdAt');
});
});

View File

@@ -1,8 +0,0 @@
@ECHO OFF
sc query "MongoDB" | findstr "RUNNING"
if errorlevel 1 (
net start "MongoDB Server"
) else (
net stop "MongoDB Server"
)