From d0aa2bca69c3cb62c25a5c82b95c4463ac82bcc1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 28 Aug 2024 17:49:38 +0300 Subject: [PATCH 1/4] feat: Implement rest API endpoint to update users for admin --- .../api/v1/admin/users/update-user.ee.js | 18 ++++ .../api/v1/admin/users/update-user.ee.test.js | 87 +++++++++++++++++++ .../src/routes/api/v1/admin/users.ee.js | 2 + .../rest/api/v1/admin/users/update-user.js | 29 +++++++ 4 files changed, 136 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/admin/users/update-user.ee.js create mode 100644 packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js diff --git a/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.js b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.js new file mode 100644 index 00000000..efd1978f --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.js @@ -0,0 +1,18 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import User from '../../../../../models/user.js'; + +export default async (request, response) => { + const user = await User.query() + .withGraphFetched({ + role: true, + }) + .patchAndFetchById(request.params.userId, userParams(request)) + .throwIfNotFound(); + + renderObject(response, user); +}; + +const userParams = (request) => { + const { email, fullName, roleId } = request.body; + return { email, fullName, roleId }; +}; diff --git a/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js new file mode 100644 index 00000000..04bd7f3c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import updateUserMock from '../../../../../../test/mocks/rest/api/v1/admin/users/update-user.js'; + +describe('PATCH /api/v1/admin/users/:userId', () => { + let currentUser, adminRole, token; + + beforeEach(async () => { + adminRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: adminRole.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return updated user with valid data for another user', async () => { + const anotherUser = await createUser(); + const anotherRole = await createRole(); + + const anotherUserUpdatedData = { + email: 'updated@sample.com', + fullName: 'Updated Full Name', + roleId: anotherRole.id, + }; + + const response = await request(app) + .patch(`/api/v1/admin/users/${anotherUser.id}`) + .set('Authorization', token) + .send(anotherUserUpdatedData) + .expect(200); + + const refetchedAnotherUser = await anotherUser.$query(); + + const expectedPayload = updateUserMock( + { + ...refetchedAnotherUser, + ...anotherUserUpdatedData, + }, + anotherRole + ); + + expect(response.body).toMatchObject(expectedPayload); + }); + + it('should return HTTP 422 with invalid user data', async () => { + const anotherUser = await createUser(); + + const anotherUserUpdatedData = { + email: null, + fullName: null, + roleId: null, + }; + + const response = await request(app) + .patch(`/api/v1/admin/users/${anotherUser.id}`) + .set('Authorization', token) + .send(anotherUserUpdatedData) + .expect(422); + + expect(response.body.meta.type).toEqual('ModelValidation'); + expect(response.body.errors).toMatchObject({ + email: ['must be string'], + fullName: ['must be string'], + roleId: ['must be string'], + }); + }); + + it('should return not found response for not existing user UUID', async () => { + const notExistingUserUUID = Crypto.randomUUID(); + + await request(app) + .patch(`/api/v1/admin/users/${notExistingUserUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .get('/api/v1/admin/users/invalidUserUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/routes/api/v1/admin/users.ee.js b/packages/backend/src/routes/api/v1/admin/users.ee.js index 685003e8..357e0bd9 100644 --- a/packages/backend/src/routes/api/v1/admin/users.ee.js +++ b/packages/backend/src/routes/api/v1/admin/users.ee.js @@ -3,12 +3,14 @@ import { authenticateUser } from '../../../../helpers/authentication.js'; import { authorizeAdmin } from '../../../../helpers/authorization.js'; import getUsersAction from '../../../../controllers/api/v1/admin/users/get-users.ee.js'; import getUserAction from '../../../../controllers/api/v1/admin/users/get-user.ee.js'; +import updateUserAction from '../../../../controllers/api/v1/admin/users/update-user.ee.js'; import deleteUserAction from '../../../../controllers/api/v1/admin/users/delete-user.js'; const router = Router(); router.get('/', authenticateUser, authorizeAdmin, getUsersAction); router.get('/:userId', authenticateUser, authorizeAdmin, getUserAction); +router.patch('/:userId', authenticateUser, authorizeAdmin, updateUserAction); router.delete('/:userId', authenticateUser, authorizeAdmin, deleteUserAction); export default router; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js b/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js new file mode 100644 index 00000000..d253d60d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js @@ -0,0 +1,29 @@ +const updateCurrentUserMock = (user, role) => { + return { + data: { + createdAt: user.createdAt.getTime(), + email: user.email, + fullName: user.fullName, + id: user.id, + status: user.status, + updatedAt: user.updatedAt.getTime(), + role: { + id: role.id, + key: role.key, + name: role.name, + isAdmin: role.isAdmin, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + }, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'User', + }, + }; +}; + +export default updateCurrentUserMock; From cf37c43bc7dc1b388face0ceb84504d3dfbe43e3 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 28 Aug 2024 17:50:06 +0300 Subject: [PATCH 2/4] chore: Add update user mutation to converted mutations --- packages/backend/src/graphql/mutation-resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 74195133..97228915 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -20,11 +20,11 @@ import updateFlow from './mutations/update-flow.js'; import updateFlowStatus from './mutations/update-flow-status.js'; import updateRole from './mutations/update-role.ee.js'; import updateStep from './mutations/update-step.js'; -import updateUser from './mutations/update-user.ee.js'; import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee.js'; import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee.js'; // Converted mutations +import updateUser from './mutations/update-user.ee.js'; import deleteStep from './mutations/delete-step.js'; import verifyConnection from './mutations/verify-connection.js'; From a153787ae64d3b65d3ff9d55676f560be8738c48 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 28 Aug 2024 17:54:15 +0300 Subject: [PATCH 3/4] chore: Use patch instead of get for update user tests --- .../src/controllers/api/v1/admin/users/update-user.ee.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js index 04bd7f3c..67fd8ecc 100644 --- a/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/users/update-user.ee.test.js @@ -80,7 +80,7 @@ describe('PATCH /api/v1/admin/users/:userId', () => { it('should return bad request response for invalid UUID', async () => { await request(app) - .get('/api/v1/admin/users/invalidUserUUID') + .patch('/api/v1/admin/users/invalidUserUUID') .set('Authorization', token) .expect(400); }); From e45dfa94edd332c06674f823dec259d17f664622 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 28 Aug 2024 17:55:06 +0300 Subject: [PATCH 4/4] fix: Use updateUserMock instead of updateCurrentUserMock for admin API endpoint --- .../backend/test/mocks/rest/api/v1/admin/users/update-user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js b/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js index d253d60d..bc692875 100644 --- a/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js +++ b/packages/backend/test/mocks/rest/api/v1/admin/users/update-user.js @@ -1,4 +1,4 @@ -const updateCurrentUserMock = (user, role) => { +const updateUserMock = (user, role) => { return { data: { createdAt: user.createdAt.getTime(), @@ -26,4 +26,4 @@ const updateCurrentUserMock = (user, role) => { }; }; -export default updateCurrentUserMock; +export default updateUserMock;