Merge pull request #2139 from automatisch/aut-1338

test(flow): write model tests
This commit is contained in:
Ömer Faruk Aydın
2024-10-30 14:55:21 +01:00
committed by GitHub
5 changed files with 268 additions and 35 deletions

View File

@@ -1,11 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let flow = await request.currentUser.$relatedQuery('flows').insert({
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({
name: 'Name your flow',
});
flow = await flow.createInitialSteps();
await flow.createInitialSteps();
renderObject(response, flow, { status: 201 });
};

View File

@@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Flow model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"publishedAt": {
"type": "string",
},
"remoteWebhookId": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`;

View File

@@ -123,13 +123,14 @@ class Flow extends Base {
return lastExecutions.map((execution) => execution.internalId);
}
get IncompleteStepsError() {
static get IncompleteStepsError() {
return new ValidationError({
data: {
flow: [
{
message: 'All steps should be completed before updating flow status!'
}
message:
'All steps should be completed before updating flow status!',
},
],
},
type: 'incompleteStepsError',
@@ -148,8 +149,6 @@ class Flow extends Base {
type: 'action',
position: 2,
});
return this.$query().withGraphFetched('steps');
}
async createActionStep(previousStepId) {
@@ -291,6 +290,18 @@ class Flow extends Base {
return duplicatedFlowWithSteps;
}
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
async updateStatus(newActiveValue) {
if (this.active === newActiveValue) {
return this;
@@ -299,7 +310,7 @@ class Flow extends Base {
const triggerStep = await this.getTriggerStep();
if (triggerStep.status === 'incomplete') {
throw this.IncompleteStepsError;
throw Flow.IncompleteStepsError;
}
const trigger = await triggerStep.getTriggerCommand();
@@ -365,7 +376,7 @@ class Flow extends Base {
});
if (incompleteStep) {
throw this.IncompleteStepsError;
throw Flow.IncompleteStepsError;
}
const allSteps = await oldFlow.$relatedQuery('steps');
@@ -375,8 +386,9 @@ class Flow extends Base {
data: {
flow: [
{
message: 'There should be at least one trigger and one action steps in the flow!'
}
message:
'There should be at least one trigger and one action steps in the flow!',
},
],
},
type: 'insufficientStepsError',
@@ -395,18 +407,6 @@ class Flow extends Base {
await super.$afterUpdate(opt, queryContext);
Telemetry.flowUpdated(this);
}
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
}
export default Flow;

View File

@@ -0,0 +1,203 @@
import { describe, it, expect, vi } from 'vitest';
import Flow from './flow.js';
import User from './user.js';
import Base from './base.js';
import Step from './step.js';
import Execution from './execution.js';
import { createFlow } from '../../test/factories/flow.js';
import { createExecution } from '../../test/factories/execution.js';
describe('Flow model', () => {
it('tableName should return correct name', () => {
expect(Flow.tableName).toBe('flows');
});
it('jsonSchema should have correct validations', () => {
expect(Flow.jsonSchema).toMatchSnapshot();
});
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Flow.relationMappings();
const expectedRelations = {
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
triggerStep: {
relation: Base.HasOneRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
executions: {
relation: Base.HasManyRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
},
lastExecution: {
relation: Base.HasOneRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
filter: expect.any(Function),
},
user: {
relation: Base.HasOneRelation,
modelClass: User,
join: {
from: 'flows.user_id',
to: 'users.id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('steps should return the steps', () => {
const relations = Flow.relationMappings();
const orderBySpy = vi.fn();
relations.steps.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('position', 'asc');
});
it('triggerStep should return the trigger step', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const whereSpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.triggerStep.filter({ where: whereSpy });
expect(whereSpy).toHaveBeenCalledWith('type', 'trigger');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
it('lastExecution should return the last execution', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.lastExecution.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
});
describe.todo('afterFind - possibly refactor to persist');
describe('lastInternalId', () => {
it('should return internal ID of last execution when exists', async () => {
const flow = await createFlow();
await createExecution({ flowId: flow.id });
await createExecution({ flowId: flow.id });
const lastExecution = await createExecution({ flowId: flow.id });
expect(await flow.lastInternalId()).toBe(lastExecution.internalId);
});
it('should return null when no flow execution exists', async () => {
const flow = await createFlow();
expect(await flow.lastInternalId()).toBe(null);
});
});
describe('lastInternalIds', () => {
it('should return last internal IDs', async () => {
const flow = await createFlow();
const internalIds = [
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
].map((execution) => execution.internalId);
expect(await flow.lastInternalIds()).toStrictEqual(internalIds);
});
it('should return last 50 internal IDs by default', async () => {
const flow = new Flow();
const limitSpy = vi.fn().mockResolvedValue([]);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
select: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: limitSpy,
});
await flow.lastInternalIds();
expect(limitSpy).toHaveBeenCalledWith(50);
});
});
it('IncompleteStepsError should return validation error for incomplete steps', () => {
expect(() => {
throw Flow.IncompleteStepsError;
}).toThrowError(
'flow: All steps should be completed before updating flow status!'
);
});
it('createInitialSteps should create one trigger and one action step', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
const steps = await flow.$relatedQuery('steps');
expect(steps.length).toBe(2);
expect(steps[0]).toMatchObject({
flowId: flow.id,
type: 'trigger',
position: 1,
});
expect(steps[1]).toMatchObject({
flowId: flow.id,
type: 'action',
position: 2,
});
});
it.todo('createActionStep');
it.todo('delete');
});

View File

@@ -6,18 +6,6 @@ const createFlowMock = async (flow) => {
status: flow.status,
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: [
{
position: 1,
status: 'incomplete',
type: 'trigger',
},
{
position: 2,
status: 'incomplete',
type: 'action',
},
],
};
return {