Merge pull request #1688 from automatisch/rest-get-execution

feat: Implement get execution API endpoint
This commit is contained in:
Ömer Faruk Aydın
2024-03-03 19:06:10 +01:00
committed by GitHub
11 changed files with 241 additions and 1 deletions

View File

@@ -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);
};

View File

@@ -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);
});
});

View File

@@ -11,6 +11,10 @@ const authorizationList = {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'GET /api/v1/executions/:executionId': {
action: 'read',
subject: 'Execution',
},
}; };
export const authorizeUser = async (request, response, next) => { export const authorizeUser = async (request, response, next) => {

View File

@@ -149,6 +149,13 @@ class User extends Base {
return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query(); return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query();
} }
get authorizedExecutions() {
const conditions = this.can('read', 'Execution');
return conditions.isCreator
? this.$relatedQuery('executions')
: Execution.query();
}
login(password) { login(password) {
return bcrypt.compare(password, this.password); return bcrypt.compare(password, this.password);
} }

View File

@@ -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;

View File

@@ -9,6 +9,7 @@ import paymentRouter from './api/v1/payment.ee.js';
import appAuthClientsRouter from './api/v1/app-auth-clients.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js';
import flowsRouter from './api/v1/flows.js'; import flowsRouter from './api/v1/flows.js';
import appsRouter from './api/v1/apps.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 samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
import rolesRouter from './api/v1/admin/roles.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js';
import permissionsRouter from './api/v1/admin/permissions.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/app-auth-clients', appAuthClientsRouter);
router.use('/api/v1/flows', flowsRouter); router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/apps', appsRouter); 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/saml-auth-providers', samlAuthProvidersRouter);
router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/roles', rolesRouter);
router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/permissions', permissionsRouter);

View File

@@ -0,0 +1,18 @@
import flowSerializer from './flow.js';
const executionSerializer = (execution) => {
let executionData = {
id: execution.id,
testRun: execution.testRun,
createdAt: execution.createdAt.getTime(),
updatedAt: execution.updatedAt.getTime(),
};
if (execution.flow) {
executionData.flow = flowSerializer(execution.flow);
}
return executionData;
};
export default executionSerializer;

View File

@@ -0,0 +1,38 @@
import { describe, it, expect, beforeEach } from 'vitest';
import executionSerializer from './execution';
import flowSerializer from './flow';
import { createExecution } from '../../test/factories/execution';
import { createFlow } from '../../test/factories/flow';
describe('executionSerializer', () => {
let flow, execution;
beforeEach(async () => {
flow = await createFlow();
execution = await createExecution({
flowId: flow.id,
});
});
it('should return the execution data', async () => {
const expectedPayload = {
id: execution.id,
testRun: execution.testRun,
createdAt: execution.createdAt.getTime(),
updatedAt: execution.updatedAt.getTime(),
};
expect(executionSerializer(execution)).toEqual(expectedPayload);
});
it('should return the execution data with the flow', async () => {
execution.flow = flow;
const expectedPayload = {
flow: flowSerializer(flow),
};
expect(executionSerializer(execution)).toMatchObject(expectedPayload);
});
});

View File

@@ -8,7 +8,7 @@ const flowSerializer = (flow) => {
status: flow.status, status: flow.status,
}; };
if (flow.steps) { if (flow.steps?.length > 0) {
flowData.steps = flow.steps.map((step) => stepSerializer(step)); flowData.steps = flow.steps.map((step) => stepSerializer(step));
} }

View File

@@ -9,6 +9,7 @@ import appSerializer from './app.js';
import authSerializer from './auth.js'; import authSerializer from './auth.js';
import triggerSerializer from './trigger.js'; import triggerSerializer from './trigger.js';
import actionSerializer from './action.js'; import actionSerializer from './action.js';
import executionSerializer from './execution.js';
const serializers = { const serializers = {
User: userSerializer, User: userSerializer,
@@ -22,6 +23,7 @@ const serializers = {
Auth: authSerializer, Auth: authSerializer,
Trigger: triggerSerializer, Trigger: triggerSerializer,
Action: actionSerializer, Action: actionSerializer,
Execution: executionSerializer,
}; };
export default serializers; export default serializers;

View File

@@ -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;