From 9ff1798a0721242b550937decb7cf3a6ca76334b Mon Sep 17 00:00:00 2001 From: Justin Headley Date: Sat, 27 Aug 2022 20:55:18 -0700 Subject: [PATCH] Bugfix/wraper objectids (#308) * feature: add tests for better crud coverage * bugfix: support delete payload with rest call * bugfix: properly handle objectIds in payload * feature: better test coverage between rest and wrapper calls * 3.0.1 --- package-lock.json | 4 +- package.json | 2 +- tests/e2e/basic-crud.tests.js | 536 ++++++- ...bed.tests.js => basic-embed-rest.tests.js} | 2 +- tests/e2e/basic-embed-wrapper.tests.js | 1387 +++++++++++++++++ tests/e2e/end-to-end.tests.js | 8 +- utilities/handler-helper.js | 7 +- 7 files changed, 1934 insertions(+), 12 deletions(-) rename tests/e2e/{basic-embed.tests.js => basic-embed-rest.tests.js} (99%) create mode 100644 tests/e2e/basic-embed-wrapper.tests.js diff --git a/package-lock.json b/package-lock.json index 1d370549..a585e96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rest-hapi", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rest-hapi", - "version": "3.0.0", + "version": "3.0.1", "license": "MIT", "dependencies": { "@hapi/boom": "^9.1.0", diff --git a/package.json b/package.json index 994ddc1f..51205ef7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rest-hapi", - "version": "3.0.0", + "version": "3.0.1", "description": "A RESTful API generator for hapi", "main": "rest-hapi.js", "bin": { diff --git a/tests/e2e/basic-crud.tests.js b/tests/e2e/basic-crud.tests.js index 26ef4908..7784eb22 100644 --- a/tests/e2e/basic-crud.tests.js +++ b/tests/e2e/basic-crud.tests.js @@ -281,7 +281,7 @@ module.exports = (t, Mongoose, internals, Log) => // }) }) - // basic "Soft Delete" works + // basic "Delete" works .then(function() { return t.test('basic "Delete" works', function(t) { // @@ -291,6 +291,7 @@ module.exports = (t, Mongoose, internals, Log) => const config = { loglevel: 'ERROR', absoluteModelPath: true, + enableSoftDelete: false, modelPath: path.join( __dirname, @@ -309,12 +310,17 @@ module.exports = (t, Mongoose, internals, Log) => config: config } }) - .then(function() { + .then(function(response) { server.start() + return Mongoose.model('role').find() + }) + .then(function(response) { + internals.previous = response + return RestHapi.deleteOne({ model: Mongoose.model('role'), - _id: internals.previous._id, + _id: internals.previous[0]._id, restCall: true }) }) @@ -325,6 +331,11 @@ module.exports = (t, Mongoose, internals, Log) => return Mongoose.model('role') .find() .then(function(response) { + t.deepEquals( + internals.previous[0].name, + 'test_updated', + 'role previously existed' + ) t.deepEquals(response, [], 'role deleted') }) }) @@ -349,5 +360,524 @@ module.exports = (t, Mongoose, internals, Log) => // }) }) + // basic "Soft Delete" works + .then(function() { + return t.test('basic "Soft Delete" works', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + enableSoftDelete: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: { name: 'test' }, + restCall: true + }) + }) + .then(function(response) { + return RestHapi.deleteOne({ + model: Mongoose.model('role'), + _id: response._id, + restCall: true + }) + }) + // + + // + .then(function(response) { + return Mongoose.model('role') + .find() + .then(function(response) { + t.deepEquals( + response[0].isDeleted, + true, + 'role soft deleted' + ) + }) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) + // basic "Hard Delete" works + .then(function() { + return t.test('basic "Hard Delete" works', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + enableSoftDelete: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: { name: 'test_hard' }, + restCall: true + }) + }) + .then(function(response) { + internals.previous = response + return RestHapi.deleteOne({ + model: Mongoose.model('role'), + _id: response._id, + hardDelete: true, + restCall: true + }) + }) + // + + // + .then(function() { + return Mongoose.model('role') + .find() + .then(function(response) { + t.deepEquals( + internals.previous.name, + 'test_hard', + 'role previously existed' + ) + t.deepEquals(response, [], 'role hard deleted') + }) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) + // create many works + .then(function() { + return t.test('create many works', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: [ + { name: 'test1' }, + { name: 'test2' }, + { name: 'test3' } + ], + restCall: true + }) + }) + // + + // + .then(function(response) { + internals.previous = response + t.equals( + response[0].name, + 'test1', + 'role with name "test1" created' + ) + t.equals( + response[1].name, + 'test2', + 'role with name "test2" created' + ) + t.equals( + response[2].name, + 'test3', + 'role with name "test3" created' + ) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) + // basic delete many works + .then(function() { + return t.test('basic delete many works', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + enableSoftDelete: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: [ + { name: 'test1' }, + { name: 'test2' }, + { name: 'test3' } + ], + restCall: true + }) + }) + .then(function(response) { + return RestHapi.deleteMany({ + model: Mongoose.model('role'), + payload: response + .filter(obj => obj.name !== 'test1') + .map(obj => ({ + _id: obj._id, + hardDelete: true + })), + restCall: true + }) + }) + // + + // + .then(function(response) { + return Mongoose.model('role') + .find() + .then(function(response) { + t.deepEquals( + response.length, + 1, + 'correct number of roles deleted' + ) + t.deepEquals( + response[0].name, + 'test1', + 'correct roles deleted' + ) + }) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) + // cant soft delete when disabled + .then(function() { + return t.test('cant soft delete when disabled', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + enableSoftDelete: false, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: [ + { name: 'test1' }, + { name: 'test2' }, + { name: 'test3' } + ], + restCall: true + }) + }) + .then(function(response) { + return RestHapi.deleteMany({ + model: Mongoose.model('role'), + payload: response + .filter(obj => obj.name !== 'test1') + .map(obj => ({ + _id: obj._id, + hardDelete: true + })), + restCall: true + }) + }) + // + + // + .then(function(response) { + t.deepEquals( + response.message, + 'Invalid request payload input', + 'rejected soft delete' + ) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) + // delete many non-rest call handles objectIds + .then(function() { + return t.test('delete many non-rest call handles objectIds', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + enableSoftDelete: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-1/models' + ) + } + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: Mongoose.model('role'), + payload: [ + { name: 'test1' }, + { name: 'test2' }, + { name: 'test3' } + ], + restCall: true + }) + }) + .then(function(response) { + return RestHapi.deleteMany({ + model: Mongoose.model('role'), + payload: response + .filter(obj => obj.name !== 'test1') + .map(obj => ({ + _id: obj._id, + hardDelete: true + })), + restCall: false + }) + }) + // + + // + .then(function(response) { + return Mongoose.model('role') + .find() + .then(function(response) { + t.deepEquals( + response.length, + 1, + 'correct number of roles deleted' + ) + t.deepEquals( + response[0].name, + 'test1', + 'correct roles deleted' + ) + }) + }) + // + + // + .then(function(response) { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + }) + }) ) }) diff --git a/tests/e2e/basic-embed.tests.js b/tests/e2e/basic-embed-rest.tests.js similarity index 99% rename from tests/e2e/basic-embed.tests.js rename to tests/e2e/basic-embed-rest.tests.js index 1b6fb36f..6af026af 100644 --- a/tests/e2e/basic-embed.tests.js +++ b/tests/e2e/basic-embed-rest.tests.js @@ -7,7 +7,7 @@ const Q = require('q') const Hapi = require('@hapi/hapi') module.exports = (t, Mongoose, internals, Log) => { - return t.test('basic embedded association tests', function(t) { + return t.test('basic embedded association tests (REST)', function(t) { let users = [] const userProfiles = [] let roles = [] diff --git a/tests/e2e/basic-embed-wrapper.tests.js b/tests/e2e/basic-embed-wrapper.tests.js new file mode 100644 index 00000000..45ec7d27 --- /dev/null +++ b/tests/e2e/basic-embed-wrapper.tests.js @@ -0,0 +1,1387 @@ +'use strict' + +const path = require('path') +const TestHelper = require('../../utilities/test-helper') +const Decache = require('decache') +const Q = require('q') +const Hapi = require('@hapi/hapi') + +module.exports = (t, Mongoose, internals, Log) => { + return t.test('basic embedded association tests (WRAPPER)', function(t) { + let users = [] + const userProfiles = [] + let roles = [] + let permissions = [] + let hashtags = [] + return ( + Q.when() + // ONE_ONE associations work + .then(function() { + // For some reason the models don't always get deleted properly on the previous tests + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + + return t.test('ONE_ONE associations work', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + RestHapi.config = config + + let user = {} + let userProfile = {} + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + return RestHapi.create({ + model: 'user', + payload: { + email: 'test@user.com', + password: 'root' + }, + restCall: false + }) + }) + .then(function(response) { + user = response + + return RestHapi.create({ + model: 'userProfile', + payload: { + status: 'Enabled', + user: user._id + }, + restCall: false + }) + }) + .then(function(response) { + userProfile = response + userProfiles.push(userProfiles) + + return RestHapi.update({ + model: 'user', + _id: user._id, + payload: { + profile: userProfile._id + }, + restCall: false + }) + }) + .then(function(response) { + user = response + users.push(user) + + return RestHapi.list({ + model: 'user', + query: { $embed: ['profile'] }, + restCall: false + }) + }) + // + + // + .then(function(response) { + t.deepEquals( + response.docs[0].profile, + userProfile, + 'ONE_ONE association correct' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + }) + ) + // + }) + }) + // adding and retrieving ONE_MANY/MANY_ONE associations works + .then(function() { + return t.test( + 'adding and retrieving ONE_MANY/MANY_ONE associations works', + function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + const payload = [ + { + name: 'User', + description: 'A standard user account.' + }, + { + name: 'Admin', + description: 'A user with advanced permissions.' + }, + { + name: 'SuperAdmin', + description: 'A user with full permissions.' + } + ] + + return RestHapi.create({ + model: 'role', + payload, + restCall: false + }) + }) + .then(function(response) { + roles = roles.concat(response) + + const payload = [ + { + email: 'test@user2.com', + password: 'root' + }, + { + email: 'test@user3.com', + password: 'root' + }, + { + email: 'test@admin.com', + password: 'root', + title: roles[1]._id + } + ] + + return RestHapi.create({ + model: 'user', + payload, + restCall: false + }) + }) + .then(function(response) { + users = users.concat(response) + + return RestHapi.addOne({ + ownerModel: 'role', + childModel: 'user', + associationName: 'users', + ownerId: roles[0]._id, + childId: users[0]._id, + restCall: false + }) + }) + .then(function(response) { + const payload = [users[1]._id, users[2]._id] + + return RestHapi.addMany({ + ownerModel: 'role', + childModel: 'user', + associationName: 'users', + ownerId: roles[0]._id, + payload, + restCall: false + }) + }) + .then(function(response) { + promises.push( + RestHapi.find({ + model: 'role', + _id: roles[0]._id, + query: { $embed: ['users'] }, + restCall: false + }) + ) + }) + .then(function(response) { + promises.push( + RestHapi.getAll({ + ownerModel: 'role', + childModel: 'user', + associationName: 'users', + ownerId: roles[0]._id, + query: { $embed: ['title'] }, + restCall: false + }) + ) + }) + .then(function(response) { + promises.push( + RestHapi.list({ + model: 'user', + _id: roles[0]._id, + query: { + $embed: ['title'], + email: [ + users[0].email, + users[1].email, + users[2].email + ] + }, + restCall: false + }) + ) + }) + .then(function(response) { + promises.push( + RestHapi.list({ + model: 'user', + _id: roles[0]._id, + query: { + email: [ + users[0].email, + users[1].email, + users[2].email + ] + }, + restCall: false + }) + ) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + // EXPL: rearrange results to match order + const result1 = [] + response[0].users.forEach(function(user) { + result1.push( + response[3].docs.find(function(u) { + return u.email === user.email + }) + ) + }) + const result2 = [] + response[1].docs.forEach(function(user) { + result2.push( + response[2].docs.find(function(u) { + return u.email === user.email + }) + ) + }) + t.equals( + response[0].users.length, + 3, + 'users length correct 1' + ) + t.equals( + response[1].docs.length, + 3, + 'users length correct 2' + ) + t.equals( + response[2].docs.length, + 3, + 'users length correct 3' + ) + t.equals( + response[3].docs.length, + 3, + 'users length correct 4' + ) + t.deepEquals( + response[0].users, + result1, + 'ONE_MANY association correct' + ) + t.deepEquals( + response[1].docs, + result2, + 'MANY_ONE association correct' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + + // NOTE: We decache mongoose here instead of clearing the schemas to prevent an + // undefined schema bug in mongoose arrays that only seems to appear in the next + // test. + Decache('mongoose') + }) + ) + // + } + ) + }) + // adding and retrieving MANY_MANY associations works + .then(function() { + return t.test( + 'adding and retrieving MANY_MANY associations works', + function(t) { + // + Mongoose = require('mongoose') + Mongoose.Promise = Promise + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + const payload = [ + { + name: 'root', + description: 'Access to all endpoints' + }, + { + name: 'create', + description: 'Access to all create endpoints' + }, + { + name: 'read', + description: 'Access to all read endpoints' + }, + { + name: 'update', + description: 'Access to all update endpoints' + }, + { + name: 'delete', + description: 'Access to all delete endpoints' + }, + { + name: 'associate', + description: 'Access to all association endpoints' + }, + { + name: 'nothing', + description: 'Permission with no use.' + } + ] + + const request = { + method: 'POST', + url: '/permission', + params: {}, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + permissions = permissions.concat(response.result) + + const payload = [ + permissions.find(function(p) { + return p.name === 'create' + })._id, + permissions.find(function(p) { + return p.name === 'read' + })._id, + permissions.find(function(p) { + return p.name === 'update' + })._id, + permissions.find(function(p) { + return p.name === 'delete' + })._id + ] + + const request = { + method: 'POST', + url: '/role/{ownerId}/permission', + params: { ownerId: roles[1]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = [ + { + enabled: true, + childId: permissions.find(function(p) { + return p.name === 'nothing' + })._id + }, + { + enabled: false, + childId: permissions.find(function(p) { + return p.name === 'associate' + })._id + } + ] + + const request = { + method: 'POST', + url: '/user/{ownerId}/permissions', + params: { ownerId: users[0]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const childId = permissions.find(function(p) { + return p.name === 'root' + })._id + + const request = { + method: 'PUT', + url: '/role/{ownerId}/permission/{childId}', + params: { ownerId: roles[1]._id, childId: childId }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const childId = permissions.find(function(p) { + return p.name === 'root' + })._id + const payload = { enabled: false } + const request = { + method: 'PUT', + url: '/user/{ownerId}/permissions/{childId}', + params: { ownerId: users[0]._id, childId: childId }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/role/{_id}', + params: { _id: roles[1]._id }, + query: { $embed: ['permissions'] }, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/role/{ownerId}/permission', + params: { ownerId: roles[1]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{_id}', + params: { _id: users[0]._id }, + query: { $embed: ['permissions'] }, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{ownerId}/permissions', + params: { ownerId: users[0]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + promises.push( + Mongoose.model('user') + .find({ _id: users[0]._id }) + .exec() + ) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + const result1Orig = response[0].result.permissions.map( + function(obj) { + return obj.permission + } + ) + const result2Orig = response[2].result.permissions.map( + function(obj) { + obj.permission.user_permission = { + enabled: obj.enabled + } + return obj.permission + } + ) + // EXPL: rearrange results to match order + const result1 = [] + response[1].result.docs.forEach(function(permission) { + result1.push( + result1Orig.find(function(perm) { + return perm.name === permission.name + }) + ) + }) + const result2 = [] + response[3].result.docs.forEach(function(permission) { + result2.push( + result2Orig.find(function(perm) { + return perm.name === permission.name + }) + ) + }) + t.deepEquals( + response[1].result.docs, + result1, + 'MANY_MANY association correct' + ) + t.deepEquals( + response[3].result.docs, + result2, + 'MANY_MANY association correct' + ) + t.deepEquals( + response[4][0].permissions[0].enabled, + true, + 'MANY_MANY associations embedded' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + }) + ) + // + } + ) + }) + // adding and retrieving _MANY associations works + .then(function() { + return t.test( + 'adding and retrieving _MANY associations works', + function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function() { + server.start() + + const payload = [ + { + text: '#cool' + }, + { + text: '#notcool' + }, + { + text: '#soso' + }, + { + text: '#ilovetags' + }, + { + text: '#enough' + } + ] + + const request = { + method: 'POST', + url: '/hashtag', + params: {}, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + hashtags = hashtags.concat(response.result) + + const payload = { + tags: [hashtags[0]._id, hashtags[1]._id] + } + + const request = { + method: 'PUT', + url: '/user/{_id}', + params: { _id: users[0]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = { + tags: [hashtags[0]._id, hashtags[2]._id, hashtags[4]._id] + } + + const request = { + method: 'PUT', + url: '/user/{_id}', + params: { _id: users[1]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const request = { + method: 'PUT', + url: '/user/{ownerId}/hashtag/{childId}', + params: { + ownerId: users[0]._id, + childId: hashtags[2]._id + }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = [ + hashtags[2]._id, // NOTE: duplicate, should only be added once + hashtags[3]._id + ] + + const request = { + method: 'POST', + url: '/user/{ownerId}/hashtag', + params: { ownerId: users[0]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{_id}', + params: { _id: users[0]._id }, + query: { $embed: ['tags'] }, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{ownerId}/hashtag', + params: { ownerId: users[1]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + const result1 = [ + hashtags[0], + hashtags[1], + hashtags[2], + hashtags[3] + ] + const result2 = [hashtags[0], hashtags[2], hashtags[4]] + t.deepEquals( + response[0].result.tags, + result1, + '_MANY association correct' + ) + t.deepEquals( + response[1].result.docs, + result2, + '_MANY association correct' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + }) + ) + // + } + ) + }) + // removing ONE_MANY/MANY_ONE associations works + .then(function() { + return t.test( + 'removing ONE_MANY/MANY_ONE associations works', + function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function(response) { + return RestHapi.removeOne({ + ownerModel: 'role', + ownerId: roles[0]._id, + childModel: 'user', + childId: users[0]._id, + associationName: 'users', + restCall: false + }) + }) + .then(function(response) { + const payload = [ + users[1]._id, + users[2]._id, + users[3]._id // NOTE: this user doesn't belong to the role, so the association shouldn't be removed from the user + ] + + return RestHapi.removeMany({ + ownerModel: 'role', + ownerId: roles[0]._id, + childModel: 'user', + associationName: 'users', + payload, + restCall: false + }) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/role/{ownerId}/people', + params: { ownerId: roles[0]._id }, + query: { $embed: ['title'] }, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user', + params: {}, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + let result2 = true + let result3 = false + response[1].result.docs.forEach(function(user) { + if ( + user.title && + user.title.toString() !== roles[1]._id.toString() + ) { + result2 = false + } + if ( + user.title && + user.title.toString() === roles[1]._id.toString() + ) { + result3 = true + } + }) + t.deepEquals( + response[0].result.docs, + [], + 'ONE_MANY associations removed' + ) + t.ok(result2, 'MANY_ONE associations removed') + t.ok(result3, 'Admin role not removed') + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + delete Mongoose.models.role + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + }) + ) + // + } + ) + }) + // removing MANY_MANY associations works + .then(function() { + return t.test('removing MANY_MANY associations works', function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function(response) { + const childId = permissions.find(function(p) { + return p.name === 'root' + })._id + + const request = { + method: 'DELETE', + url: '/role/{ownerId}/permission/{childId}', + params: { ownerId: roles[1]._id, childId: childId }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const childId = permissions.find(function(p) { + return p.name === 'root' + })._id + const payload = { enabled: false } + const request = { + method: 'DELETE', + url: '/user/{ownerId}/permissions/{childId}', + params: { ownerId: users[0]._id, childId: childId }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = [ + permissions.find(function(p) { + return p.name === 'create' + })._id, + permissions.find(function(p) { + return p.name === 'read' + })._id, + permissions.find(function(p) { + return p.name === 'update' + })._id, + permissions.find(function(p) { + return p.name === 'delete' + })._id + ] + + const request = { + method: 'DELETE', + url: '/role/{ownerId}/permission', + params: { ownerId: roles[1]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = [ + permissions.find(function(p) { + return p.name === 'nothing' + })._id, + permissions.find(function(p) { + return p.name === 'associate' + })._id + ] + + const request = { + method: 'DELETE', + url: '/user/{ownerId}/permissions', + params: { ownerId: users[0]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/role/{ownerId}/permission', + params: { ownerId: roles[1]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{ownerId}/permissions', + params: { ownerId: users[0]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + t.deepEquals( + response[0].result.docs, + [], + 'MANY_MANY associations removed' + ) + t.deepEquals( + response[1].result.docs, + [], + 'MANY_MANY associations removed' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + }) + ) + // + }) + }) + // removing _MANY associations works + .then(function() { + return t.test( + 'removing ONE_MANY/MANY_ONE associations works', + function(t) { + // + const RestHapi = require('../../rest-hapi') + const server = new Hapi.Server() + + const config = { + loglevel: 'ERROR', + absoluteModelPath: true, + + modelPath: path.join( + __dirname, + '/test-scenarios/scenario-3/models' + ), + embedAssociations: true + } + + const promises = [] + + RestHapi.config = config + + return ( + server + .register({ + plugin: RestHapi, + options: { + mongoose: Mongoose, + config: config + } + }) + .then(function(response) { + const request = { + method: 'DELETE', + url: '/user/{ownerId}/hashtag/{childId}', + params: { + ownerId: users[0]._id, + childId: hashtags[0]._id + }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const payload = [ + hashtags[1]._id, + hashtags[2]._id, + hashtags[3]._id + ] + + const request = { + method: 'DELETE', + url: '/user/{ownerId}/hashtag', + params: { ownerId: users[0]._id }, + query: {}, + payload: payload, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + return server.inject(injectOptions) + }) + .then(function(response) { + const request = { + method: 'GET', + url: '/user/{ownerId}/hashtag', + params: { ownerId: users[0]._id }, + query: {}, + payload: {}, + credentials: {}, + headers: {} + } + + const injectOptions = TestHelper.mockInjection(request) + + promises.push(server.inject(injectOptions)) + }) + // + + // + .then(function() { + return Promise.all(promises) + }) + // + + // + .then(function(response) { + t.deepEquals( + response[0].result.docs, + [], + '_MANY associations removed' + ) + }) + // + + // + .then(function() { + Decache('../../rest-hapi') + + Decache('../config') + Object.keys(Mongoose.models).forEach(function(key) { + delete Mongoose.models[key] + }) + Object.keys(Mongoose.modelSchemas || []).forEach(function( + key + ) { + delete Mongoose?.modelSchemas[key] + }) + + return Mongoose.connection.db.dropDatabase() + }) + ) + // + } + ) + }) + ) + }) +} diff --git a/tests/e2e/end-to-end.tests.js b/tests/e2e/end-to-end.tests.js index eba93a71..66a03c58 100644 --- a/tests/e2e/end-to-end.tests.js +++ b/tests/e2e/end-to-end.tests.js @@ -8,7 +8,8 @@ const Decache = require('decache') // Import test groups const BasicCrudTests = require('./basic-crud.tests') const DocAuthTests = require('./doc-auth.tests') -const BasicEmbedTests = require('./basic-embed.tests') +const BasicEmbedRestTests = require('./basic-embed-rest.tests') +const BasicEmbedWrapperTests = require('./basic-embed-wrapper.tests') const BasicNonEmbedTests = require('./basic-non-embed.tests') const AuditLogTests = require('./audit-log.tests') const AdvanceAssocTests = require('./advance-assoc.tests') @@ -70,7 +71,10 @@ Test('end to end tests', function(t) { return DocAuthTests(t, Mongoose, internals, Log, restore) }) .then(function() { - return BasicEmbedTests(t, Mongoose, internals, Log, restore) + return BasicEmbedRestTests(t, Mongoose, internals, Log, restore) + }) + .then(function() { + return BasicEmbedWrapperTests(t, Mongoose, internals, Log, restore) }) .then(function() { return BasicNonEmbedTests(t, Mongoose, internals, Log, restore) diff --git a/utilities/handler-helper.js b/utilities/handler-helper.js index cc8c5bc1..0dc6aa1e 100644 --- a/utilities/handler-helper.js +++ b/utilities/handler-helper.js @@ -809,6 +809,7 @@ async function _deleteOneV2({ method: 'Delete', url: `/${model.routeOptions.alias || model.modelName}/${_id}`, params: { _id }, + payload: { hardDelete }, credentials, headers: { authorization: 'Bearer' } } @@ -961,7 +962,7 @@ async function _deleteManyV2({ }) { model = getModel(model) const RestHapi = require('../rest-hapi') - Log = Log || RestHapi.getLogger('deleteOne') + Log = Log || RestHapi.getLogger('deleteMany') if (restCall) { assertServer() @@ -1553,7 +1554,7 @@ async function _addManyHandler( try { // EXPL: make a copy of the payload so that request.payload remains unchanged let payload = request.payload.map(item => { - return _.isObject(item) ? _.assignIn({}, item) : item + return _.isObject(item) ? _.cloneDeep(item) : item }) if (_.isEmpty(request.payload)) { throw Boom.badRequest('Payload is empty.') @@ -1764,7 +1765,7 @@ async function _removeManyHandler( try { // EXPL: make a copy of the payload so that request.payload remains unchanged let payload = request.payload.map(item => { - return _.isObject(item) ? _.assignIn({}, item) : item + return _.isObject(item) ? _.cloneDeep(item) : item }) if (_.isEmpty(request.payload)) { throw Boom.badRequest('Payload is empty.')