diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution.js b/packages/backend/src/controllers/api/v1/executions/get-execution.js new file mode 100644 index 00000000..c3b0d115 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution.js @@ -0,0 +1,15 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const execution = await request.currentUser.authorizedExecutions + .withGraphFetched({ + flow: { + steps: true, + }, + }) + .withSoftDeleted() + .findById(request.params.executionId) + .throwIfNotFound(); + + renderObject(response, execution); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution.test.js b/packages/backend/src/controllers/api/v1/executions/get-execution.test.js new file mode 100644 index 00000000..47473d90 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution.test.js @@ -0,0 +1,111 @@ +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 { createFlow } from '../../../../../test/factories/flow.js'; +import { createExecution } from '../../../../../test/factories/execution.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getExecutionMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution'; + +describe('GET /api/v1/executions/:executionId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the execution data of current user', async () => { + const currentUserFlow = await createFlow({ + userId: currentUser.id, + }); + + const currentUserExecution = await createExecution({ + flowId: currentUserFlow.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/executions/${currentUserExecution.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionMock( + currentUserExecution, + currentUserFlow + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the execution data of another user', async () => { + const anotherUser = await createUser(); + + const anotherUserFlow = await createFlow({ + userId: anotherUser.id, + }); + + const anotherUserExecution = await createExecution({ + flowId: anotherUserFlow.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/executions/${anotherUserExecution.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionMock( + anotherUserExecution, + anotherUserFlow + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing execution UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingExcecutionUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/executions/${notExistingExcecutionUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/executions/invalidExecutionUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 63ce286c..e6d09d5a 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -11,6 +11,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/executions/:executionId': { + action: 'read', + subject: 'Execution', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/routes/api/v1/executions.js b/packages/backend/src/routes/api/v1/executions.js new file mode 100644 index 00000000..8deee5a9 --- /dev/null +++ b/packages/backend/src/routes/api/v1/executions.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 getExecutionAction from '../../../controllers/api/v1/executions/get-execution.js'; + +const router = Router(); + +router.get( + '/:executionId', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 0933ac94..d7084881 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 flowsRouter from './api/v1/flows.js'; import appsRouter from './api/v1/apps.js'; +import executionsRouter from './api/v1/executions.js'; import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; @@ -27,6 +28,7 @@ router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter); router.use('/api/v1/flows', flowsRouter); router.use('/api/v1/apps', appsRouter); +router.use('/api/v1/executions', executionsRouter); router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js new file mode 100644 index 00000000..bf823ded --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js @@ -0,0 +1,27 @@ +const getExecutionMock = async (execution, flow) => { + const data = { + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + flow: { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.active ? 'published' : 'draft', + }, + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Execution', + }, + }; +}; + +export default getExecutionMock;