mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
Jest for server
- Introduced the Jest testing framework into our server side code so plugins can now have tests that run
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const { pluginsPath } = require('../plugins');
|
||||
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
const specPattern = 'client/**/__tests__/**/*.spec.js?(x)';
|
||||
|
||||
module.exports = {
|
||||
rootDir: '../',
|
||||
testMatch: [
|
||||
`<rootDir>/${specPattern}`,
|
||||
`<rootDir>/plugins/**/${specPattern}`,
|
||||
],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map(target => `<rootDir>/client/${target}/src`),
|
||||
...buildEmbeds.map(embed => `<rootDir>/client/coral-embed-${embed}/src`),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js',
|
||||
},
|
||||
testResultsProcessor: process.env.JEST_REPORTER,
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js',
|
||||
},
|
||||
};
|
||||
+5
-37
@@ -1,40 +1,8 @@
|
||||
const path = require('path');
|
||||
const { pluginsPath } = require('./plugins');
|
||||
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
testMatch: ['**/client/**/__tests__/**/*.js?(x)'],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map(target =>
|
||||
path.join('<rootDir>', 'client', target, 'src')
|
||||
),
|
||||
...buildEmbeds.map(embed =>
|
||||
path.join('<rootDir>', 'client', `coral-embed-${embed}`, 'src')
|
||||
),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js',
|
||||
},
|
||||
|
||||
projects: ['<rootDir>', '<rootDir>/client'],
|
||||
testPathIgnorePatterns: ['client'],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/setupJest.js',
|
||||
testResultsProcessor: process.env.JEST_REPORTER,
|
||||
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js',
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
modulePaths: ['<rootDir>'],
|
||||
};
|
||||
|
||||
+6
-5
@@ -18,10 +18,11 @@
|
||||
"lint:js": "eslint bin/cli* .",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"plugins:reconcile": "./bin/cli plugins reconcile",
|
||||
"test": "npm-run-all test:client test:server",
|
||||
"test:server": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test:client": "TEST_MODE=unit NODE_ENV=test jest",
|
||||
"test:client:watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"test": "npm-run-all test:jest test:mocha",
|
||||
"test:jest": "TEST_MODE=unit NODE_ENV=test jest --runInBand",
|
||||
"test:client": "TEST_MODE=unit NODE_ENV=test jest --projects client",
|
||||
"test:mocha": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test:server:jest": "TEST_MODE=unit NODE_ENV=test jest --runInBand --projects .",
|
||||
"e2e": "./scripts/e2e.js",
|
||||
"e2e:ci": "./scripts/e2e-ci.sh",
|
||||
"heroku-postbuild": "npm-run-all plugins:reconcile build",
|
||||
@@ -127,6 +128,7 @@
|
||||
"inquirer-autocomplete-prompt": "^0.12.1",
|
||||
"ioredis": "3.1.4",
|
||||
"ip": "^1.1.5",
|
||||
"jest": "^22.4.3",
|
||||
"joi": "^13.0.0",
|
||||
"jsonwebtoken": "^8.0.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
@@ -226,7 +228,6 @@
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"husky": "^0.14.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^21.2.1",
|
||||
"jest-junit": "^3.6.0",
|
||||
"lint-staged": "^7.0.0",
|
||||
"mocha": "^3.1.2",
|
||||
|
||||
@@ -239,26 +239,14 @@ function getReactionConfig(reaction) {
|
||||
hooks: {
|
||||
Action: {
|
||||
__resolveType: {
|
||||
post({ action_type }) {
|
||||
switch (action_type) {
|
||||
case REACTION:
|
||||
return `${Reaction}Action`;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
post: ({ action_type }) =>
|
||||
action_type === REACTION ? `${Reaction}Action` : undefined,
|
||||
},
|
||||
},
|
||||
ActionSummary: {
|
||||
__resolveType: {
|
||||
post({ action_type }) {
|
||||
switch (action_type) {
|
||||
case REACTION:
|
||||
return `${Reaction}ActionSummary`;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
post: ({ action_type = '' } = {}) =>
|
||||
action_type === REACTION ? `${Reaction}ActionSummary` : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const getReactionConfig = require('./getReactionConfig');
|
||||
|
||||
describe('plugins-api', () => {
|
||||
describe('getReactionConfig', () => {
|
||||
let config;
|
||||
beforeEach(() => {
|
||||
config = getReactionConfig('heart');
|
||||
});
|
||||
|
||||
describe('context', () => {
|
||||
it('provides a sort function', () => {
|
||||
expect(config.context.Sort).toBeInstanceOf(Function);
|
||||
const sort = config.context.Sort();
|
||||
expect(sort.Comments).toHaveProperty('hearts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hooks', () => {
|
||||
it('handles the __resolveType properly', () => {
|
||||
expect(config.hooks.ActionSummary.__resolveType).toHaveProperty('post');
|
||||
expect(config.hooks.ActionSummary.__resolveType.post).toBeInstanceOf(
|
||||
Function
|
||||
);
|
||||
expect(
|
||||
config.hooks.ActionSummary.__resolveType.post({})
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
config.hooks.ActionSummary.__resolveType.post({ action_type: 'LOVE' })
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
config.hooks.ActionSummary.__resolveType.post({
|
||||
action_type: 'HEART',
|
||||
})
|
||||
).toEqual('HeartActionSummary');
|
||||
});
|
||||
it('handles the __resolveType properly', () => {
|
||||
expect(config.hooks.Action.__resolveType).toHaveProperty('post');
|
||||
expect(config.hooks.Action.__resolveType.post).toBeInstanceOf(Function);
|
||||
expect(config.hooks.Action.__resolveType.post({})).toBeUndefined();
|
||||
expect(
|
||||
config.hooks.Action.__resolveType.post({ action_type: 'LOVE' })
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
config.hooks.Action.__resolveType.post({
|
||||
action_type: 'HEART',
|
||||
})
|
||||
).toEqual('HeartAction');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,126 +1,5 @@
|
||||
const debug = require('debug')('talk:plugin:akismet');
|
||||
const { ErrSpam } = require('./errors');
|
||||
const akismet = require('akismet-api');
|
||||
const { get, merge } = require('lodash');
|
||||
const { KEY, SITE } = require('./config');
|
||||
const client = akismet.client({
|
||||
key: KEY,
|
||||
blog: SITE,
|
||||
});
|
||||
const typeDefs = require('./server/typeDefs');
|
||||
const hooks = require('./server/hooks');
|
||||
const resolvers = require('./server/resolvers');
|
||||
|
||||
let enabled = true;
|
||||
|
||||
// TODO: when using a developer key, this is possible, the plus plan does not
|
||||
// allow us to check the key.
|
||||
// let enabled = false;
|
||||
// client.verifyKey((err, valid) => {
|
||||
// if (err) {
|
||||
// throw err;
|
||||
// }
|
||||
|
||||
// if (valid) {
|
||||
// enabled = true;
|
||||
// } else {
|
||||
// throw new Error('Akismet key is invalid');
|
||||
// }
|
||||
// });
|
||||
|
||||
module.exports = {
|
||||
typeDefs: `
|
||||
input CreateCommentInput {
|
||||
|
||||
# If true, the mutation will fail when the
|
||||
# body contains detected spam.
|
||||
checkSpam: Boolean
|
||||
}
|
||||
|
||||
type Comment {
|
||||
spam: Boolean
|
||||
}
|
||||
`,
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
async pre(_, { input }, { loaders, parent: req }) {
|
||||
// If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
}
|
||||
|
||||
let spam = false;
|
||||
try {
|
||||
const user_ip = get(req, 'ip', false);
|
||||
if (!user_ip) {
|
||||
debug('no ip on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some headers from the request.
|
||||
const user_agent = req.get('User-Agent');
|
||||
if (!user_agent || user_agent.length === 0) {
|
||||
debug('no user agent on request');
|
||||
return;
|
||||
}
|
||||
|
||||
const referrer = req.get('Referrer');
|
||||
if (!referrer || referrer.length === 0) {
|
||||
debug('no referrer on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Asset that the comment is being made against.
|
||||
const asset = await loaders.Assets.getByID.load(input.asset_id);
|
||||
if (!asset) {
|
||||
debug('asset not found for new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send off the comment to Akismet to check to see what they say.
|
||||
spam = await client.checkSpam({
|
||||
user_ip,
|
||||
user_agent,
|
||||
referrer,
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: input.body,
|
||||
is_test: true,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = merge({}, input.metadata || {}, {
|
||||
akismet: spam,
|
||||
});
|
||||
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw new ErrSpam();
|
||||
}
|
||||
|
||||
// Attach reason information for the flag being added.
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SPAM_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolvers: {
|
||||
Comment: {
|
||||
spam: comment => get(comment, 'metadata.akismet', null),
|
||||
},
|
||||
},
|
||||
};
|
||||
module.exports = { typeDefs, hooks, resolvers };
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
const debug = require('debug')('talk:plugin:akismet');
|
||||
const { ErrSpam } = require('./errors');
|
||||
const akismet = require('akismet-api');
|
||||
const { get, merge } = require('lodash');
|
||||
const { KEY, SITE } = require('./config');
|
||||
const client = akismet.client({
|
||||
key: KEY,
|
||||
blog: SITE,
|
||||
});
|
||||
|
||||
let enabled = true;
|
||||
|
||||
// TODO: when using a developer key, this is possible, the plus plan does not
|
||||
// allow us to check the key.
|
||||
// let enabled = false;
|
||||
// client.verifyKey((err, valid) => {
|
||||
// if (err) {
|
||||
// throw err;
|
||||
// }
|
||||
|
||||
// if (valid) {
|
||||
// enabled = true;
|
||||
// } else {
|
||||
// throw new Error('Akismet key is invalid');
|
||||
// }
|
||||
// });
|
||||
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
async pre(_, { input }, { loaders, parent: req }) {
|
||||
// If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
}
|
||||
|
||||
let spam = false;
|
||||
try {
|
||||
const user_ip = get(req, 'ip', false);
|
||||
if (!user_ip) {
|
||||
debug('no ip on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some headers from the request.
|
||||
const user_agent = req.get('User-Agent');
|
||||
if (!user_agent || user_agent.length === 0) {
|
||||
debug('no user agent on request');
|
||||
return;
|
||||
}
|
||||
|
||||
const referrer = req.get('Referrer');
|
||||
if (!referrer || referrer.length === 0) {
|
||||
debug('no referrer on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Asset that the comment is being made against.
|
||||
const asset = await loaders.Assets.getByID.load(input.asset_id);
|
||||
if (!asset) {
|
||||
debug('asset not found for new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send off the comment to Akismet to check to see what they say.
|
||||
spam = await client.checkSpam({
|
||||
user_ip,
|
||||
user_agent,
|
||||
referrer,
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: input.body,
|
||||
is_test: true,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = merge({}, input.metadata || {}, {
|
||||
akismet: spam,
|
||||
});
|
||||
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw new ErrSpam();
|
||||
}
|
||||
|
||||
// Attach reason information for the flag being added.
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SPAM_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
const { get } = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
Comment: {
|
||||
spam: comment => get(comment, 'metadata.akismet', null),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
const resolvers = require('./resolvers');
|
||||
|
||||
describe('talk-plugin-akismet', () => {
|
||||
describe('resolvers', () => {
|
||||
it('resolves when there is a akismet value', () => {
|
||||
const spam = resolvers.Comment.spam({ metadata: { akismet: true } });
|
||||
expect(spam).toEqual(true);
|
||||
});
|
||||
it('resolves when there not is a akismet value', () => {
|
||||
const spam = resolvers.Comment.spam({});
|
||||
expect(spam).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
input CreateCommentInput {
|
||||
|
||||
# If true, the mutation will fail when the
|
||||
# body contains detected spam.
|
||||
checkSpam: Boolean
|
||||
}
|
||||
|
||||
type Comment {
|
||||
spam: Boolean
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = fs.readFileSync(
|
||||
path.join(__dirname, 'typeDefs.graphql'),
|
||||
'utf8'
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
let values = {};
|
||||
|
||||
const getScores = () => values.getScores;
|
||||
|
||||
const isToxic = () => values.isToxic;
|
||||
|
||||
const setValues = newValues => {
|
||||
values = newValues;
|
||||
};
|
||||
|
||||
module.exports = { getScores, isToxic, setValues };
|
||||
@@ -1,11 +1,6 @@
|
||||
const { getScores, isToxic } = require('./perspective');
|
||||
const { ErrToxic } = require('./errors');
|
||||
|
||||
// We don't add the hooks during _test_ as the perspective API is not available.
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
@@ -16,7 +11,7 @@ module.exports = {
|
||||
scores = await getScores(input.body);
|
||||
} catch (err) {
|
||||
// Warn and let mutation pass.
|
||||
console.trace(err);
|
||||
console.trace(err); // TODO: log/handle this differently?
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const hooks = require('./hooks');
|
||||
const { ErrToxic } = require('./errors');
|
||||
|
||||
// Mock out the perspective api call.
|
||||
jest.mock('./perspective');
|
||||
|
||||
describe('talk-plugin-toxic-comments', () => {
|
||||
describe('hooks', () => {
|
||||
beforeEach(() => {
|
||||
require('./perspective').setValues({ isToxic: true });
|
||||
});
|
||||
|
||||
it('sets the correct values for a toxic comment', async () => {
|
||||
let input = { body: 'This is a body.', checkToxicity: false };
|
||||
await hooks.RootMutation.createComment.pre(null, { input }, null, null);
|
||||
expect(input).toHaveProperty('status', 'SYSTEM_WITHHELD');
|
||||
});
|
||||
|
||||
it('throws an error when a toxic comment is sent', async () => {
|
||||
expect.assertions(1);
|
||||
await expect(
|
||||
hooks.RootMutation.createComment.pre(
|
||||
null,
|
||||
{ input: { checkToxicity: true } },
|
||||
null,
|
||||
null
|
||||
)
|
||||
).rejects.toBeInstanceOf(ErrToxic);
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-4
@@ -7,11 +7,11 @@ const { LOGGING_LEVEL, REVISION_HASH } = require('../config');
|
||||
// but will send JSON logs in production that's parsable by a system like ELK.
|
||||
const streams = (() => {
|
||||
// In development, use the debug stream printer.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const debug = require('bunyan-debug-stream');
|
||||
return [
|
||||
{
|
||||
level: 'debug',
|
||||
level: LOGGING_LEVEL,
|
||||
type: 'raw',
|
||||
stream: debug({
|
||||
basepath: path.resolve(__dirname, '..'),
|
||||
@@ -22,7 +22,7 @@ const streams = (() => {
|
||||
}
|
||||
|
||||
// In production, emit JSON.
|
||||
return [{ stream: process.stdout, level: 'info' }];
|
||||
return [{ stream: process.stdout, level: LOGGING_LEVEL }];
|
||||
})();
|
||||
|
||||
// logger is the base logger used by all logging systems in Talk.
|
||||
@@ -31,7 +31,6 @@ const logger = createBunyanLogger({
|
||||
name: 'talk',
|
||||
version,
|
||||
revision: REVISION_HASH,
|
||||
level: LOGGING_LEVEL,
|
||||
streams,
|
||||
serializers: stdSerializers,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,15 @@ if (WEBPACK) {
|
||||
// here just ensures that the application can quit correctly.
|
||||
mongoose.disconnect();
|
||||
} else {
|
||||
mongoose.connection.on('connected', () => logger.debug('mongodb connected'));
|
||||
mongoose.connection.on('disconnected', () =>
|
||||
logger.debug('mongodb disconnected')
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
mongoose.disconnect();
|
||||
}, 5000);
|
||||
|
||||
// Connect to the Mongo instance.
|
||||
mongoose
|
||||
.connect(MONGO_URL, {
|
||||
@@ -45,9 +54,6 @@ if (WEBPACK) {
|
||||
autoIndex: CREATE_MONGO_INDEXES,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('mongodb connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const mongoose = require('../services/mongoose');
|
||||
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
Object.keys(mongoose.connection.collections).map(collection => {
|
||||
return new Promise((resolve, reject) => {
|
||||
mongoose.connection.collections[collection].remove(function(err) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async function() {
|
||||
await mongoose.disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user