diff --git a/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.js b/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.js new file mode 100644 index 00000000..64c355a6 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.js @@ -0,0 +1,14 @@ +import Flow from '../../../../models/flow.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let flow = await Flow.query() + .findOne({ + id: request.params.flowId, + }) + .throwIfNotFound(); + + flow = await flow.updateStatus(request.body.active); + + renderObject(response, flow); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.test.js b/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.test.js new file mode 100644 index 00000000..f7dc7c67 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/update-flow-status.ee.test.js @@ -0,0 +1,79 @@ +import Crypto from 'crypto'; +import request from 'supertest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createApiToken } from '../../../../../test/factories/api-token.js'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import updateFlowStatusMock from '../../../../../test/mocks/rest/api/v1/flows/update-flow-status.js'; +import app from '../../../../app.js'; +import * as license from '../../../../helpers/license.ee.js'; +import Flow from '../../../../models/flow.js'; + +describe('PATCH /api/v1/flows/:flowId/status', () => { + let token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + token = (await createApiToken()).token; + }); + + it('should return updated flow data', async () => { + const flow = await createFlow({ + active: false, + }); + + const triggerStep = await createStep({ + flowId: flow.id, + type: 'trigger', + appKey: 'webhook', + key: 'catchRawWebhook', + }); + + await createStep({ + flowId: flow.id, + type: 'action', + appKey: 'ntfy', + key: 'sendMessage', + parameters: { + topic: 'Test notification', + message: `Message: {{step.${triggerStep.id}.body.message}} by {{step.${triggerStep.id}.body.sender}}`, + }, + }); + + const response = await request(app) + .patch(`/api/v1/flows/${flow.id}/status`) + .set('x-api-token', token) + .send({ active: true }) + .expect(200); + + const refetchedFlow = await Flow.query().findById(response.body.data.id); + + const refetchedFlowSteps = await refetchedFlow + .$relatedQuery('steps') + .orderBy('position', 'asc'); + + const expectedPayload = await updateFlowStatusMock( + refetchedFlow, + refetchedFlowSteps + ); + + expect(response.body).toStrictEqual(expectedPayload); + expect(response.body.data.status).toStrictEqual('published'); + }); + + it('should return not found response for not existing flow UUID', async () => { + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .patch(`/api/v1/flows/${notExistingFlowUUID}/status`) + .set('x-api-token', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .patch('/api/v1/flows/invalidFlowUUID/status') + .set('x-api-token', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/routes/api/v1/flows.ee.js b/packages/backend/src/routes/api/v1/flows.ee.js index 299e7740..d25055cb 100644 --- a/packages/backend/src/routes/api/v1/flows.ee.js +++ b/packages/backend/src/routes/api/v1/flows.ee.js @@ -1,10 +1,12 @@ import { Router } from 'express'; import getFlowAction from '../../../controllers/api/v1/flows/get-flow.ee.js'; import deleteFlowAction from '../../../controllers/api/v1/flows/delete-flow.ee.js'; +import updateFlowStatusAction from '../../../controllers/api/v1/flows/update-flow-status.ee.js'; const router = Router(); router.get('/:flowId', getFlowAction); router.delete('/:flowId', deleteFlowAction); +router.patch('/:flowId/status', updateFlowStatusAction); export default router; diff --git a/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js b/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js new file mode 100644 index 00000000..f7c32b3b --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js @@ -0,0 +1,38 @@ +const updateFlowStatusMock = async (flow, steps = []) => { + const data = { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + }; + + if (steps.length) { + data.steps = steps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + name: step.name, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + webhookUrl: step.webhookUrl, + })); + } + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Flow', + }, + }; +}; + +export default updateFlowStatusMock; diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 11379059..14354db5 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -28,10 +28,10 @@ export default defineConfig({ ], thresholds: { autoUpdate: true, - statements: 99.4, - branches: 98.33, + statements: 99.41, + branches: 98.34, functions: 99.06, - lines: 99.4, + lines: 99.41, }, }, },