From 9a0434be3268b813a2c506ca08feb72ded47e991 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 13 Mar 2024 15:24:50 +0100 Subject: [PATCH] feat: Implement get step connection API endpoint --- .../api/v1/steps/get-connection.js | 11 ++ .../api/v1/steps/get-connection.test.js | 121 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/models/user.js | 5 + packages/backend/src/routes/api/v1/steps.js | 16 +++ packages/backend/src/routes/index.js | 2 + .../mocks/rest/api/v1/steps/get-connection.js | 27 ++++ 7 files changed, 186 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/steps/get-connection.js create mode 100644 packages/backend/src/controllers/api/v1/steps/get-connection.test.js create mode 100644 packages/backend/src/routes/api/v1/steps.js create mode 100644 packages/backend/test/mocks/rest/api/v1/steps/get-connection.js diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.js b/packages/backend/src/controllers/api/v1/steps/get-connection.js new file mode 100644 index 00000000..ab1a403e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .findById(request.params.stepId) + .throwIfNotFound(); + + const connection = await step.$relatedQuery('connection').throwIfNotFound(); + + renderObject(response, connection); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.test.js b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js new file mode 100644 index 00000000..003b80bd --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js @@ -0,0 +1,121 @@ +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'; +import { createUser } from '../../../../../test/factories/user'; +import { createConnection } from '../../../../../test/factories/connection'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getConnectionMock from '../../../../../test/mocks/rest/api/v1/steps/get-connection'; + +describe('GET /api/v1/steps/:stepId/connection', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the current user connection data of specified step', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const currentUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: currentUserflow.id, + connectionId: currentUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(currentUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the current user connection data of specified step', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const anotherUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(anotherUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step without connection', async () => { + const stepWithoutConnection = await createStep(); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get(`/api/v1/steps/${stepWithoutConnection.id}/connection`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingFlowUUID}/connection`) + .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 request(app) + .get('/api/v1/steps/invalidFlowUUID/connection') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index f294cba8..1822c1e0 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -15,6 +15,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/steps/:stepId/connection': { + action: 'read', + subject: 'Flow', + }, 'GET /api/v1/connections/:connectionId/flows': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 2d199881..d3dcaffe 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -149,6 +149,11 @@ class User extends Base { return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query(); } + get authorizedSteps() { + const conditions = this.can('read', 'Flow'); + return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); + } + get authorizedExecutions() { const conditions = this.can('read', 'Execution'); return conditions.isCreator diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js new file mode 100644 index 00000000..80fc2f0b --- /dev/null +++ b/packages/backend/src/routes/api/v1/steps.js @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; + +const router = Router(); + +router.get( + '/:stepId/connection', + authenticateUser, + authorizeUser, + asyncHandler(getConnectionAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index a51700ac..a542f7fc 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -9,6 +9,7 @@ import paymentRouter from './api/v1/payment.ee.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js'; import appConfigsRouter from './api/v1/app-configs.ee.js'; import flowsRouter from './api/v1/flows.js'; +import stepsRouter from './api/v1/steps.js'; import appsRouter from './api/v1/apps.js'; import connectionsRouter from './api/v1/connections.js'; import executionsRouter from './api/v1/executions.js'; @@ -30,6 +31,7 @@ router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter); router.use('/api/v1/app-configs', appConfigsRouter); router.use('/api/v1/flows', flowsRouter); +router.use('/api/v1/steps', stepsRouter); router.use('/api/v1/apps', appsRouter); router.use('/api/v1/connections', connectionsRouter); router.use('/api/v1/executions', executionsRouter); diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js new file mode 100644 index 00000000..3f6c8abb --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js @@ -0,0 +1,27 @@ +const getConnectionMock = async (connection) => { + const data = { + id: connection.id, + key: connection.key, + verified: connection.verified, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default getConnectionMock;