diff --git a/packages/backend/src/controllers/api/v1/steps/test-step.js b/packages/backend/src/controllers/api/v1/steps/test-step.js new file mode 100644 index 00000000..3b71b979 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/test-step.js @@ -0,0 +1,12 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let step = await request.currentUser.authorizedSteps + .clone() + .findById(request.params.stepId) + .throwIfNotFound(); + + step = await step.test(); + + renderObject(response, step); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/test-step.test.js b/packages/backend/src/controllers/api/v1/steps/test-step.test.js new file mode 100644 index 00000000..b7574e85 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/test-step.test.js @@ -0,0 +1,209 @@ +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 { createExecution } from '../../../../../test/factories/execution.js'; +import { createExecutionStep } from '../../../../../test/factories/execution-step.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import testStepMock from '../../../../../test/mocks/rest/api/v1/steps/test-step.js'; + +describe('POST /api/v1/steps/:stepId/test', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should test the step of the current user and return step data', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + const currentUserConnection = await createConnection(); + + const triggerStep = await createStep({ + flowId: currentUserFlow.id, + connectionId: currentUserConnection.id, + appKey: 'webhook', + key: 'catchRawWebhook', + type: 'trigger', + parameters: { + workSynchronously: false, + }, + }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + connectionId: currentUserConnection.id, + appKey: 'formatter', + key: 'text', + type: 'action', + parameters: { + input: `{{step.${triggerStep.id}.body.name}}`, + transform: 'capitalize', + }, + }); + + const execution = await createExecution({ + flowId: currentUserFlow.id, + testRun: true, + }); + + await createExecutionStep({ + dataIn: { workSynchronously: false }, + dataOut: { body: { name: 'john doe' } }, + stepId: triggerStep.id, + executionId: execution.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/test`) + .set('Authorization', token) + .expect(200); + + const expectedLastExecutionStep = await actionStep.$relatedQuery( + 'lastExecutionStep' + ); + + const expectedPayload = await testStepMock( + actionStep, + expectedLastExecutionStep + ); + + expect(response.body).toMatchObject(expectedPayload); + }); + + it('should test the step of the another user and return step data', 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, + appKey: 'webhook', + key: 'catchRawWebhook', + type: 'trigger', + parameters: { + workSynchronously: false, + }, + }); + + const actionStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + appKey: 'formatter', + key: 'text', + type: 'action', + parameters: { + input: `{{step.${triggerStep.id}.body.name}}`, + transform: 'capitalize', + }, + }); + + const execution = await createExecution({ + flowId: anotherUserFlow.id, + testRun: true, + }); + + await createExecutionStep({ + dataIn: { workSynchronously: false }, + dataOut: { body: { name: 'john doe' } }, + stepId: triggerStep.id, + executionId: execution.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) + .post(`/api/v1/steps/${actionStep.id}/test`) + .set('Authorization', token) + .expect(200); + + const expectedLastExecutionStep = await actionStep.$relatedQuery( + 'lastExecutionStep' + ); + + const expectedPayload = await testStepMock( + actionStep, + expectedLastExecutionStep + ); + + expect(response.body).toMatchObject(expectedPayload); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingStepUUID = Crypto.randomUUID(); + + await request(app) + .post(`/api/v1/steps/${notExistingStepUUID}/test`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .post('/api/v1/steps/invalidStepUUID/test') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index f9afd202..00ba8449 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -7,7 +7,6 @@ import deleteCurrentUser from './mutations/delete-current-user.ee.js'; import deleteFlow from './mutations/delete-flow.js'; import deleteRole from './mutations/delete-role.ee.js'; import duplicateFlow from './mutations/duplicate-flow.js'; -import executeFlow from './mutations/execute-flow.js'; import generateAuthUrl from './mutations/generate-auth-url.js'; import registerUser from './mutations/register-user.ee.js'; import resetConnection from './mutations/reset-connection.js'; @@ -20,6 +19,7 @@ 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 executeFlow from './mutations/execute-flow.js'; import updateUser from './mutations/update-user.ee.js'; import deleteStep from './mutations/delete-step.js'; import verifyConnection from './mutations/verify-connection.js'; diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 567d9351..ffd961e2 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -25,6 +25,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'POST /api/v1/steps/:stepId/test': { + action: 'update', + subject: 'Flow', + }, 'GET /api/v1/steps/:stepId/previous-steps': { action: 'update', subject: 'Flow', diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index 42bbc4e8..01f46190 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -9,6 +9,7 @@ import Telemetry from '../helpers/telemetry/index.js'; import appConfig from '../config/app.js'; import globalVariable from '../helpers/global-variable.js'; import computeParameters from '../helpers/compute-parameters.js'; +import testRun from '../services/test-run.js'; class Step extends Base { static tableName = 'steps'; @@ -156,6 +157,16 @@ class Step extends Base { return await App.findOneByKey(this.appKey); } + async test() { + await testRun({ stepId: this.id }); + + const updatedStep = await this.$query() + .withGraphFetched('lastExecutionStep') + .patchAndFetch({ status: 'completed' }); + + return updatedStep; + } + async getLastExecutionStep() { const lastExecutionStep = await this.$relatedQuery('executionSteps') .orderBy('created_at', 'desc') diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js index ed905afc..36305d9f 100644 --- a/packages/backend/src/routes/api/v1/steps.js +++ b/packages/backend/src/routes/api/v1/steps.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; +import testStepAction from '../../../controllers/api/v1/steps/test-step.js'; import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js'; import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js'; @@ -16,6 +17,8 @@ router.get( getConnectionAction ); +router.post('/:stepId/test', authenticateUser, authorizeUser, testStepAction); + router.get( '/:stepId/previous-steps', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/steps/test-step.js b/packages/backend/test/mocks/rest/api/v1/steps/test-step.js new file mode 100644 index 00000000..85ef6ca8 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/test-step.js @@ -0,0 +1,34 @@ +const testStepMock = async (step, lastExecutionStep) => { + const data = { + id: step.id, + appKey: step.appKey, + key: step.key, + iconUrl: step.iconUrl, + lastExecutionStep: { + id: lastExecutionStep.id, + status: lastExecutionStep.status, + dataIn: lastExecutionStep.dataIn, + dataOut: lastExecutionStep.dataOut, + errorDetails: lastExecutionStep.errorDetails, + createdAt: lastExecutionStep.createdAt.getTime(), + updatedAt: lastExecutionStep.updatedAt.getTime(), + }, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Step', + }, + }; +}; + +export default testStepMock;