add batch delete for users

Signed-off-by: Pierre Noël <petersg83@gmail.com>
This commit is contained in:
Pierre Noël 2020-07-15 15:46:59 +02:00
parent 615dffda6f
commit c73aadbed5
7 changed files with 203 additions and 47 deletions

View File

@ -214,7 +214,17 @@
{
"method": "DELETE",
"path": "/users/:id",
"handler": "user.delete",
"handler": "user.deleteOne",
"config": {
"policies": [
["admin::hasPermissions", ["admin::users.delete"]]
]
}
},
{
"method": "POST",
"path": "/users/batch-delete",
"handler": "user.deleteMany",
"config": {
"policies": [
["admin::hasPermissions", ["admin::users.delete"]]

View File

@ -1,7 +1,11 @@
'use strict';
const _ = require('lodash');
const { validateUserCreationInput, validateUserUpdateInput } = require('../validation/user');
const {
validateUserCreationInput,
validateUserUpdateInput,
validateUsersDeleteInput,
} = require('../validation/user');
module.exports = {
async create(ctx) {
@ -90,7 +94,7 @@ module.exports = {
};
},
async delete(ctx) {
async deleteOne(ctx) {
const { id } = ctx.params;
const deletedUser = await strapi.admin.services.user.deleteById(id);
@ -103,4 +107,24 @@ module.exports = {
data: strapi.admin.services.user.sanitizeUser(deletedUser),
});
},
/**
* Delete several users
* @param {KoaContext} ctx - koa context
*/
async deleteMany(ctx) {
const { body } = ctx.request;
try {
await validateUsersDeleteInput(body);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const users = await strapi.admin.services.user.deleteByIds(body.ids);
const sanitizedUsers = users.map(strapi.admin.services.user.sanitizeUser);
return ctx.deleted({
data: sanitizedUsers,
});
},
};

View File

@ -70,9 +70,9 @@ module.exports = {
const sanitizedRole = roles.map(strapi.admin.services.role.sanitizeRole)[0] || null;
ctx.body = {
return ctx.deleted({
data: sanitizedRole,
};
});
},
/**
@ -90,9 +90,9 @@ module.exports = {
const roles = await strapi.admin.services.role.deleteByIds(body.ids);
const sanitizedRoles = roles.map(strapi.admin.services.role.sanitizeRole);
ctx.body = {
return ctx.deleted({
data: sanitizedRoles,
};
});
},
/**

View File

@ -203,7 +203,7 @@ describe('User', () => {
);
});
test('Can delete a super admin if he/she is not the last one', async () => {
const user = { id: 11, roles: [{ code: SUPER_ADMIN_CODE }] };
const user = { id: 2, roles: [{ code: SUPER_ADMIN_CODE }] };
const findOne = jest.fn(() => Promise.resolve(user));
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 2 }));
const deleteFn = jest.fn(() => user);
@ -218,6 +218,53 @@ describe('User', () => {
});
});
describe('deleteByIds', () => {
test('Cannot delete last super admin', async () => {
const find = jest.fn(() =>
Promise.resolve([
{ id: 2, roles: [{ code: SUPER_ADMIN_CODE }] },
{ id: 3, roles: [{ code: SUPER_ADMIN_CODE }] },
])
);
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 2 }));
const badRequest = jest.fn();
global.strapi = {
query: () => ({ find }),
admin: { services: { role: { getSuperAdminWithUsersCount } } },
errors: { badRequest },
};
try {
await userService.deleteByIds([2, 3]);
} catch (e) {
// nothing
}
expect(badRequest).toHaveBeenCalledWith(
'ValidationError',
'You must have at least one user with super admin role.'
);
});
test('Can delete a super admin if he/she is not the last one', async () => {
const users = [
{ id: 2, roles: [{ code: SUPER_ADMIN_CODE }] },
{ id: 3, roles: [{ code: SUPER_ADMIN_CODE }] },
];
const find = jest.fn(() => Promise.resolve(users));
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 3 }));
const deleteFn = jest.fn(() => users);
global.strapi = {
query: () => ({ find, delete: deleteFn }),
admin: { services: { role: { getSuperAdminWithUsersCount } } },
};
const res = await userService.deleteByIds([2, 3]);
expect(deleteFn).toHaveBeenCalledWith({ id_in: [2, 3] });
expect(res).toEqual(users);
});
});
describe('exists', () => {
test('Return true if the user already exists', async () => {
const count = jest.fn(() => Promise.resolve(1));

View File

@ -170,8 +170,8 @@ const searchPage = async query => {
return strapi.query('user', 'admin').searchPage(query);
};
/** Delete users
* @param query
/** Delete a user
* @param id id of the user to delete
* @returns {Promise<user>}
*/
const deleteById = async id => {
@ -194,6 +194,26 @@ const deleteById = async id => {
return strapi.query('user', 'admin').delete({ id });
};
/** Delete a user
* @param ids ids of the users to delete
* @returns {Promise<user>}
*/
const deleteByIds = async ids => {
// Check at least one super admin remains
const usersToDelete = await strapi.query('user', 'admin').find({ id_in: ids }, ['roles']);
const superAdminUsers = usersToDelete.filter(hasSuperAdminRole);
if (superAdminUsers.length > 0) {
const superAdminRole = await strapi.admin.services.role.getSuperAdminWithUsersCount();
if (superAdminRole.usersCount === superAdminUsers.length) {
throw strapi.errors.badRequest(
'ValidationError',
'You must have at least one user with super admin role.'
);
}
}
return strapi.query('user', 'admin').delete({ id_in: ids });
};
/** Count the users that don't have any associated roles
* @returns {Promise<number>}
*/
@ -286,6 +306,7 @@ module.exports = {
findPage,
searchPage,
deleteById,
deleteByIds,
countUsersWithoutRole,
assignARoleToAll,
displayWarningIfUsersDontHaveRole,

View File

@ -53,28 +53,30 @@ let rq;
*
* N° Description
* -------------------------------------------
* 1. Create a user (fail/body)
* 2. Create a user (success)
* 3. Update a user (success)
* 4. Create a user with superAdmin role (success)
* 5. Update a user (fail/body)
* 6. Get a user (success)
* 7. Get a list of users (success/full)
* 8. Delete a user (success)
* 9. Delete a user (fail/notFound)
* 10. Deletes a super admin user (successfully)
* 11. Deletes last super admin user (bad request)
* 12. Update a user (fail/notFound)
* 13. Get a user (fail/notFound)
* 14. Get a list of users (success/empty)
* 1. Creates a user (wrong body)
* 2. Creates a user (successfully)
* 3. Creates users with superAdmin role (success)
* 4. Updates a user (wrong body)
* 5. Updates a user (successfully)
* 6. Finds a user (successfully)
* 7. Finds a list of users (contains user)
* 8. Deletes a user (successfully)
* 9. Deletes a user (not found)
* 10. Deletes 2 super admin users (successfully)
* 11. Deletes a super admin user (successfully)
* 12. Deletes last super admin user (bad request)
* 13. Deletes last super admin user in batch (bad request)
* 14. Updates a user (not found)
* 15. Finds a user (not found)
* 16. Finds a list of users (missing user)
*/
describe('Admin User CRUD (e2e)', () => {
// Local test data used across the test suite
let testData = {
firstSuperAdminUser: undefined,
otherSuperAdminUsers: [],
user: undefined,
secondSuperAdminUser: undefined,
role: undefined,
superAdminRole: undefined,
};
@ -138,25 +140,28 @@ describe('Admin User CRUD (e2e)', () => {
testData.user = res.body.data;
});
test('3. Creates a user with superAdmin role (success)', async () => {
const body = {
email: 'user-tests2@strapi-e2e.com',
firstname: 'user_tests-firstname',
lastname: 'user_tests-lastname',
roles: [testData.superAdminRole.id],
test('3. Creates users with superAdmin role (success)', async () => {
const getBody = index => {
return {
email: `user-tests${index}@strapi-e2e.com`,
firstname: 'user_tests-firstname',
lastname: 'user_tests-lastname',
roles: [testData.superAdminRole.id],
};
};
const res = await rq({
url: '/admin/users',
method: 'POST',
body,
});
for (let i = 0; i < 3; i++) {
const res = await rq({
url: '/admin/users',
method: 'POST',
body: getBody(i),
});
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
expect(res.statusCode).toBe(201);
expect(res.body.data).not.toBeNull();
// Using the created user as an example for the rest of the tests
testData.secondSuperAdminUser = res.body.data;
testData.otherSuperAdminUsers.push(res.body.data);
}
});
test('4. Updates a user (wrong body)', async () => {
@ -268,17 +273,32 @@ describe('Admin User CRUD (e2e)', () => {
expect(res.statusCode).toBe(404);
});
test('10. Deletes a super admin user (successfully)', async () => {
test('10. Deletes 2 super admin users (successfully)', async () => {
const users = testData.otherSuperAdminUsers.splice(0, 2);
const res = await rq({
url: `/admin/users/${testData.secondSuperAdminUser.id}`,
url: `/admin/users/batch-delete`,
method: 'POST',
body: {
ids: users.map(u => u.id),
},
});
expect(res.statusCode).toBe(200);
expect(res.body.data).toMatchObject(users);
});
test('11. Deletes a super admin user (successfully)', async () => {
const user = testData.otherSuperAdminUsers.pop();
const res = await rq({
url: `/admin/users/${user.id}`,
method: 'DELETE',
});
expect(res.statusCode).toBe(200);
expect(res.body.data).toMatchObject(testData.secondSuperAdminUser);
expect(res.body.data).toMatchObject(user);
});
test('11. Deletes last super admin user (bad request)', async () => {
test('12. Deletes last super admin user (bad request)', async () => {
const res = await rq({
url: `/admin/users/${testData.firstSuperAdminUser.id}`,
method: 'DELETE',
@ -293,7 +313,25 @@ describe('Admin User CRUD (e2e)', () => {
});
});
test('12. Updates a user (not found)', async () => {
test('13. Deletes last super admin user in batch (bad request)', async () => {
const res = await rq({
url: `/admin/users/batch-delete`,
method: 'POST',
body: {
ids: [testData.firstSuperAdminUser.id],
},
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: 'You must have at least one user with super admin role.',
});
});
test('14. Updates a user (not found)', async () => {
const body = {
lastname: 'doe',
};
@ -312,7 +350,7 @@ describe('Admin User CRUD (e2e)', () => {
});
});
test('13. Finds a user (not found)', async () => {
test('15. Finds a user (not found)', async () => {
const res = await rq({
url: `/admin/users/${testData.user.id}`,
method: 'GET',
@ -326,7 +364,7 @@ describe('Admin User CRUD (e2e)', () => {
});
});
test('14. Finds a list of users (missing user)', async () => {
test('16. Finds a list of users (missing user)', async () => {
const res = await rq({
url: `/admin/users?email=${testData.user.email}`,
method: 'GET',

View File

@ -53,8 +53,24 @@ const validateUserUpdateInput = data => {
return userUpdateSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
};
const usersDeleteSchema = yup
.object()
.shape({
ids: yup
.array()
.of(yup.strapiID())
.min(1)
.required(),
})
.noUnknown();
const validateUsersDeleteInput = async data => {
return usersDeleteSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
};
module.exports = {
validateUserCreationInput,
validateProfileUpdateInput,
validateUserUpdateInput,
validateUsersDeleteInput,
};