test(flow): write model tests
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
export default async (request, response) => {
|
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',
|
name: 'Name your flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
flow = await flow.createInitialSteps();
|
await flow.createInitialSteps();
|
||||||
|
|
||||||
renderObject(response, flow, { status: 201 });
|
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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -128,8 +128,9 @@ class Flow extends Base {
|
|||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message: 'All steps should be completed before updating flow status!'
|
message:
|
||||||
}
|
'All steps should be completed before updating flow status!',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'incompleteStepsError',
|
type: 'incompleteStepsError',
|
||||||
@@ -148,8 +149,6 @@ class Flow extends Base {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
position: 2,
|
position: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.$query().withGraphFetched('steps');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createActionStep(previousStepId) {
|
async createActionStep(previousStepId) {
|
||||||
@@ -291,6 +290,18 @@ class Flow extends Base {
|
|||||||
return duplicatedFlowWithSteps;
|
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) {
|
async updateStatus(newActiveValue) {
|
||||||
if (this.active === newActiveValue) {
|
if (this.active === newActiveValue) {
|
||||||
return this;
|
return this;
|
||||||
@@ -375,8 +386,9 @@ class Flow extends Base {
|
|||||||
data: {
|
data: {
|
||||||
flow: [
|
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',
|
type: 'insufficientStepsError',
|
||||||
@@ -395,18 +407,6 @@ class Flow extends Base {
|
|||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
Telemetry.flowUpdated(this);
|
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;
|
export default Flow;
|
||||||
|
194
packages/backend/src/models/flow.test.js
Normal file
194
packages/backend/src/models/flow.test.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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();
|
||||||
|
const execution = await createExecution({ flowId: flow.id });
|
||||||
|
|
||||||
|
expect(await flow.lastInternalId()).toBe(execution.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', () => {
|
||||||
|
const flow = new Flow();
|
||||||
|
|
||||||
|
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,
|
status: flow.status,
|
||||||
createdAt: flow.createdAt.getTime(),
|
createdAt: flow.createdAt.getTime(),
|
||||||
updatedAt: flow.updatedAt.getTime(),
|
updatedAt: flow.updatedAt.getTime(),
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
status: 'incomplete',
|
|
||||||
type: 'trigger',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 2,
|
|
||||||
status: 'incomplete',
|
|
||||||
type: 'action',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user