From 9519ec53ef63d335048fbe3022b74386c6b3058f Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 29 Aug 2024 14:16:17 +0000 Subject: [PATCH] feat: write PATCH /v1/flows/:flowId --- .../controllers/api/v1/flows/update-flow.js | 15 ++ .../api/v1/flows/update-flow.test.js | 168 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/routes/api/v1/flows.js | 2 + .../test/mocks/rest/api/v1/flows/get-flow.js | 11 +- 5 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 packages/backend/src/controllers/api/v1/flows/update-flow.js create mode 100644 packages/backend/src/controllers/api/v1/flows/update-flow.test.js diff --git a/packages/backend/src/controllers/api/v1/flows/update-flow.js b/packages/backend/src/controllers/api/v1/flows/update-flow.js new file mode 100644 index 00000000..7554d840 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/update-flow.js @@ -0,0 +1,15 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const flow = await request.currentUser.authorizedFlows + .findOne({ + id: request.params.flowId, + }) + .throwIfNotFound(); + + await flow.$query().patchAndFetch({ + name: request.body.name, + }); + + renderObject(response, flow); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/update-flow.test.js b/packages/backend/src/controllers/api/v1/flows/update-flow.test.js new file mode 100644 index 00000000..519fcd18 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/update-flow.test.js @@ -0,0 +1,168 @@ +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 { createFlow } from '../../../../../test/factories/flow.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getFlowMock from '../../../../../test/mocks/rest/api/v1/flows/get-flow.js'; + +describe('PATCH /api/v1/flows/:flowId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the updated flow data of current user', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const newFlowName = 'Updated flow'; + + const response = await request(app) + .patch(`/api/v1/flows/${currentUserFlow.id}`) + .set('Authorization', token) + .send({ + name: newFlowName, + }) + .expect(200); + + const refetchedCurrentUserFlow = await currentUserFlow.$query(); + + const expectedPayload = await getFlowMock({ + ...refetchedCurrentUserFlow, + name: newFlowName, + }); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return the updated flow data of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .patch(`/api/v1/flows/${anotherUserFlow.id}`) + .set('Authorization', token) + .send({ + name: 'Updated flow', + }) + .expect(200); + + const refetchedAnotherUserFlow = await anotherUserFlow.$query(); + + const expectedPayload = await getFlowMock({ + ...refetchedAnotherUserFlow, + name: 'Updated flow', + }); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return not found response for not existing flow UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .patch(`/api/v1/flows/${notExistingFlowUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .patch('/api/v1/flows/invalidFlowUUID') + .set('Authorization', token) + .expect(400); + }); + + it('should return unprocessable entity response for invalid data', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .patch(`/api/v1/flows/${currentUserFlow.id}`) + .set('Authorization', token) + .send({ + name: 123123, + }) + .expect(422); + + expect(response.body.errors).toStrictEqual({ + name: ['must be string'], + }); + expect(response.body.meta.type).toStrictEqual('ModelValidation'); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 2ad150f3..567d9351 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -81,6 +81,10 @@ const authorizationList = { action: 'create', subject: 'Connection', }, + 'PATCH /api/v1/flows/:flowId': { + action: 'update', + subject: 'Flow', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js index 6b00c871..125ad23a 100644 --- a/packages/backend/src/routes/api/v1/flows.js +++ b/packages/backend/src/routes/api/v1/flows.js @@ -3,10 +3,12 @@ import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import getFlowsAction from '../../../controllers/api/v1/flows/get-flows.js'; import getFlowAction from '../../../controllers/api/v1/flows/get-flow.js'; +import updateFlowAction from '../../../controllers/api/v1/flows/update-flow.js'; const router = Router(); router.get('/', authenticateUser, authorizeUser, getFlowsAction); router.get('/:flowId', authenticateUser, authorizeUser, getFlowAction); +router.patch('/:flowId', authenticateUser, authorizeUser, updateFlowAction); export default router; diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js index 54032e1c..db1e4a47 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js @@ -1,4 +1,4 @@ -const getFlowMock = async (flow, steps) => { +const getFlowMock = async (flow, steps = []) => { const data = { active: flow.active, id: flow.id, @@ -6,7 +6,10 @@ const getFlowMock = async (flow, steps) => { status: flow.active ? 'published' : 'draft', createdAt: flow.createdAt.getTime(), updatedAt: flow.updatedAt.getTime(), - steps: steps.map((step) => ({ + }; + + if (steps.length) { + data.steps = steps.map((step) => ({ appKey: step.appKey, iconUrl: step.iconUrl, id: step.id, @@ -16,8 +19,8 @@ const getFlowMock = async (flow, steps) => { status: step.status, type: step.type, webhookUrl: step.webhookUrl, - })), - }; + })); + } return { data: data,