Merge pull request #2139 from automatisch/aut-1338
test(flow): write model tests
This commit is contained in:
@@ -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 });
|
||||
};
|
||||
|
42
packages/backend/src/models/__snapshots__/flow.test.js.snap
Normal file
42
packages/backend/src/models/__snapshots__/flow.test.js.snap
Normal 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",
|
||||
}
|
||||
`;
|
@@ -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;
|
||||
|
203
packages/backend/src/models/flow.test.js
Normal file
203
packages/backend/src/models/flow.test.js
Normal 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');
|
||||
});
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user