Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BranchCondition, BranchOperator } from '@activepieces/shared'
|
||||
import { evaluateConditions } from '../../src/lib/handler/router-executor'
|
||||
|
||||
describe('Branch evaluateConditions', () => {
|
||||
describe('DATE_IS_AFTER', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is after second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02',
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-02',
|
||||
])('should return false when first date is before or equal to second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '1st January 2021',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:02Z',
|
||||
secondValue: '2021-01-01T00:00:01Z',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DATE_IS_BEFORE', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is before second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01',
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-02',
|
||||
])('should return false when first date is after or equal to second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '2nd January 2021',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:01Z',
|
||||
secondValue: '2021-01-01T00:00:02Z',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DATE_IS_EQUAL', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is equal to second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01',
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-03',
|
||||
])('should return false when first date is after or before the second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '2nd January 2021',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:01Z',
|
||||
secondValue: '2021-01-01T00:00:01Z',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_IS_EMPTY', () => {
|
||||
test.each([
|
||||
[],
|
||||
'[]',
|
||||
])('should return true when list is empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
[1],
|
||||
'[1]',
|
||||
])('should return false when list is not empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a list',
|
||||
{},
|
||||
])('should return false when the value is not a list %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_IS_NOT_EMPTY', () => {
|
||||
test.each([
|
||||
[1],
|
||||
'[1]',
|
||||
])('should return true when list is not empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
[],
|
||||
'[]',
|
||||
])('should return false when list is empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a list',
|
||||
{},
|
||||
])('should return false when the value is not a list %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_CONTAINS', () => {
|
||||
test.each([
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'banana', caseSensitive: false },
|
||||
{ expected: false, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: true },
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: false },
|
||||
{ expected: true, list: '["apple", "banana", "cherry"]', value: 'banana', caseSensitive: false },
|
||||
{ expected: true, list: 'apple', value: 'apple', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: '4', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: 4, caseSensitive: false },
|
||||
{ expected: true, list: [true, false, true], value: 'true', caseSensitive: false },
|
||||
{ expected: true, list: [true, false, true], value: true, caseSensitive: false },
|
||||
{ expected: true, list: ['true', 'false', 'true'], value: true, caseSensitive: false },
|
||||
{ expected: true, list: ['true', 'false', 'true'], value: 'true', caseSensitive: false },
|
||||
])('should return $expected for list $list containing $value (case sensitive: $caseSensitive)', ({ expected, list, value, caseSensitive }) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: list as any,
|
||||
secondValue: value as any,
|
||||
operator: BranchOperator.LIST_CONTAINS,
|
||||
caseSensitive,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_DOES_NOT_CONTAIN', () => {
|
||||
test.each([
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: true },
|
||||
{ expected: false, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: false },
|
||||
{ expected: true, list: '["apple", "banana", "cherry"]', value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: 'apple', value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: '6', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: 6, caseSensitive: false },
|
||||
{ expected: false, list: [true, false, true], value: 'false', caseSensitive: false },
|
||||
{ expected: false, list: [true, false, true], value: false, caseSensitive: false },
|
||||
{ expected: false, list: ['true', 'false', 'true'], value: false, caseSensitive: false },
|
||||
{ expected: false, list: ['true', 'false', 'true'], value: 'false', caseSensitive: false },
|
||||
])('should return $expected for list $list not containing $value (case sensitive: $caseSensitive)', ({ expected, list, value, caseSensitive }) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: list as any,
|
||||
secondValue: value as any,
|
||||
operator: BranchOperator.LIST_DOES_NOT_CONTAIN,
|
||||
caseSensitive,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,521 @@
|
||||
import { BranchCondition, BranchOperator, FlowRunStatus, RouterExecutionType } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildRouterWithOneCondition, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
function executeBranchActionWithOneCondition(condition: BranchCondition): Promise<FlowExecutorContext> {
|
||||
return flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({
|
||||
conditions: [condition],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
children: [
|
||||
buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
condition: true,
|
||||
},
|
||||
}),
|
||||
buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {
|
||||
condition: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
}
|
||||
|
||||
describe('flow with branching different branches', () => {
|
||||
|
||||
it('should execute branch with text contains condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_CONTAINS,
|
||||
firstValue: 'test',
|
||||
secondValue: 'TeSt',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not contain condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_CONTAIN,
|
||||
firstValue: 'test',
|
||||
secondValue: 'ExAmPlE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text exactly matches condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'TeSt',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not exactly match condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_EXACTLY_MATCH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'ExAmPlE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text starts with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_STARTS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'tE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not start with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_START_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'eS',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text ends with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_ENDS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'sT',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not end with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_END_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'eS',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text contains condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_CONTAINS,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not contain condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_CONTAIN,
|
||||
firstValue: 'test',
|
||||
secondValue: 'example',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text exactly matches condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not exactly match condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_EXACTLY_MATCH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'example',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text starts with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_STARTS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'te',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not start with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_START_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'es',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text ends with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_ENDS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'st',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not end with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_END_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'es',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with exists condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.EXISTS,
|
||||
firstValue: 'test',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with does not exist condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.DOES_NOT_EXIST,
|
||||
firstValue: '',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with boolean is true condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with boolean is false condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_FALSE,
|
||||
firstValue: '{{false}}',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with two equal numbers', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_EQUAL_TO,
|
||||
firstValue: '1',
|
||||
secondValue: '1',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with the first number greater than the second one', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_GREATER_THAN,
|
||||
firstValue: '2',
|
||||
secondValue: '1',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with the first number less than the second one', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_LESS_THAN,
|
||||
firstValue: '1',
|
||||
secondValue: '2',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip router', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({ children: [
|
||||
buildCodeAction({ name: 'echo_step', input: {}, skip: true }),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], executionType: RouterExecutionType.EXECUTE_FIRST_MATCH, skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FlowAction, FlowRunStatus } from '@activepieces/shared'
|
||||
import { codeExecutor } from '../../src/lib/handler/code-executor'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('codeExecutor', () => {
|
||||
|
||||
it('should execute code that echo parameters action successfully', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute code a code that throws an error', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'runtime',
|
||||
displayName: 'Your Action Name',
|
||||
message: expect.stringContaining('Custom Runtime Error'),
|
||||
},
|
||||
})
|
||||
expect(result.steps.runtime.status).toEqual('FAILED')
|
||||
expect(result.steps.runtime.errorMessage).toContain('Custom Runtime Error')
|
||||
})
|
||||
|
||||
it('should skip code action', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
skip: true,
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step).toBeUndefined()
|
||||
})
|
||||
it('should skip flow action', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: true,
|
||||
input: {},
|
||||
}),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step).toBeUndefined()
|
||||
expect(result.steps.echo_step_1.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { codeExecutor } from '../../src/lib/handler/code-executor'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { pieceExecutor } from '../../src/lib/handler/piece-executor'
|
||||
import { buildCodeAction, buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('code piece with error handling', () => {
|
||||
|
||||
it('should continue on failure when execute code a code that throws an error', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.runtime.status).toEqual('FAILED')
|
||||
expect(result.steps.runtime.errorMessage).toContain('Custom Runtime Error')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('piece with error handling', () => {
|
||||
|
||||
it('should continue on failure when piece fails', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'method': 'POST',
|
||||
'url': 'https://cloud.activepieces.com/api/v1/flags',
|
||||
'headers': {},
|
||||
'queryParams': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const expectedError = {
|
||||
response: {
|
||||
status: 404,
|
||||
body: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Route not found',
|
||||
},
|
||||
},
|
||||
request: {},
|
||||
}
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.send_http.status).toBe('FAILED')
|
||||
expect(result.steps.send_http.errorMessage).toEqual(JSON.stringify(expectedError, null, 2))
|
||||
|
||||
}, 10000)
|
||||
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FlowAction, FlowRunStatus, LoopStepOutput } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildSimpleLoopAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
|
||||
describe('flow with looping', () => {
|
||||
|
||||
it('should execute iterations', async () => {
|
||||
const codeAction = buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'index': '{{loop.index}}',
|
||||
},
|
||||
})
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ [4,5,6] }}',
|
||||
firstLoopAction: codeAction,
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const loopOut = result.steps.loop as LoopStepOutput
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(loopOut.output?.iterations.length).toBe(3)
|
||||
expect(loopOut.output?.index).toBe(3)
|
||||
expect(loopOut.output?.item).toBe(6)
|
||||
})
|
||||
|
||||
it('should execute iterations and fail on first iteration', async () => {
|
||||
const generateArray = buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'array': '{{ [4,5,6] }}',
|
||||
},
|
||||
nextAction: buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ echo_step.array }}',
|
||||
firstLoopAction: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
const result = await flowExecutor.execute({
|
||||
action: generateArray,
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const loopOut = result.steps.loop as LoopStepOutput
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(loopOut.output?.iterations.length).toBe(1)
|
||||
expect(loopOut.output?.index).toBe(1)
|
||||
expect(loopOut.output?.item).toBe(4)
|
||||
})
|
||||
|
||||
it('should skip loop', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildSimpleLoopAction({ name: 'loop', loopItems: '{{ [4,5,6] }}', skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.loop).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should skip loop in flow', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildSimpleLoopAction({ name: 'loop', loopItems: '{{ [4,5,6] }}', skip: true }),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: false,
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.loop).toBeUndefined()
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import { FlowAction, FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { pieceExecutor } from '../../src/lib/handler/piece-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('pieceExecutor', () => {
|
||||
|
||||
it('should execute data mapper successfully', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute fail gracefully when pieces fail', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/asd',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const expectedError = {
|
||||
response: {
|
||||
status: 404,
|
||||
body: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Route not found',
|
||||
},
|
||||
},
|
||||
request: {},
|
||||
}
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'send_http',
|
||||
displayName: 'Your Action Name',
|
||||
message: JSON.stringify(expectedError, null, 2),
|
||||
},
|
||||
})
|
||||
expect(result.steps.send_http.status).toBe('FAILED')
|
||||
expect(result.steps.send_http.errorMessage).toEqual(JSON.stringify(expectedError, null, 2))
|
||||
}, 10000)
|
||||
it('should skip piece action', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
input: {},
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
})
|
||||
it('should skip piece action in flow', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
}),
|
||||
nextAction: {
|
||||
...buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {},
|
||||
skip: true,
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.send_http).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
const failedHttpAction = buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/asd',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
})
|
||||
|
||||
const successHttpAction = buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/pieces',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
describe('flow retry', () => {
|
||||
const context = FlowExecutorContext.empty()
|
||||
it('should retry entire flow', async () => {
|
||||
const failedResult = await flowExecutor.execute({
|
||||
action: failedHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
const retryEntireFlow = await flowExecutor.execute({
|
||||
action: successHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(failedResult.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(retryEntireFlow.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
}, 10000)
|
||||
|
||||
it('should retry flow from failed step', async () => {
|
||||
const failedResult = await flowExecutor.execute({
|
||||
action: failedHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const retryFromFailed = await flowExecutor.execute({
|
||||
action: successHttpAction, executionState: context, constants: generateMockEngineConstants({}),
|
||||
})
|
||||
expect(failedResult.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(retryFromFailed.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
}, 10000)
|
||||
})
|
||||
@@ -0,0 +1,275 @@
|
||||
import { BranchOperator, FlowRunStatus, LoopStepOutput, RouterExecutionType, RouterStepOutput } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { StepExecutionPath } from '../../src/lib/handler/context/step-execution-path'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildPieceAction, buildRouterWithOneCondition, buildSimpleLoopAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
|
||||
const simplePauseFlow = buildPieceAction({
|
||||
name: 'approval',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
}),
|
||||
})
|
||||
|
||||
const flawWithTwoPause = buildPieceAction({
|
||||
name: 'approval',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
nextAction: buildPieceAction({
|
||||
name: 'approval-1',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const pauseFlowWithLoopAndBranch = buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ [false, true ] }}',
|
||||
firstLoopAction: buildRouterWithOneCondition({
|
||||
conditions: [
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: '{{ loop.item }}',
|
||||
},
|
||||
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
children: [
|
||||
simplePauseFlow,
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
describe('flow with pause', () => {
|
||||
|
||||
it('should pause and resume successfully with loops and branch', async () => {
|
||||
const pauseResult = await flowExecutor.execute({
|
||||
action: pauseFlowWithLoopAndBranch,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(pauseResult.verdict).toEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
expect(Object.keys(pauseResult.steps)).toEqual(['loop'])
|
||||
|
||||
// Verify that the first iteration (true) triggered the branch condition
|
||||
const loopOutputBeforeResume = pauseResult.steps.loop as LoopStepOutput
|
||||
expect(loopOutputBeforeResume.output?.iterations.length).toBe(2)
|
||||
expect(loopOutputBeforeResume.output?.item).toBe(true)
|
||||
expect(Object.keys(loopOutputBeforeResume.output?.iterations[0] ?? {})).toContain('router')
|
||||
|
||||
|
||||
const resumeResultTwo = await flowExecutor.execute({
|
||||
action: pauseFlowWithLoopAndBranch,
|
||||
executionState: pauseResult.setCurrentPath(StepExecutionPath.empty()).setVerdict({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
}),
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(resumeResultTwo.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
},
|
||||
)
|
||||
expect(Object.keys(resumeResultTwo.steps)).toEqual(['loop'])
|
||||
|
||||
const loopOut = resumeResultTwo.steps.loop as LoopStepOutput
|
||||
expect(Object.keys(loopOut.output?.iterations[1] ?? {})).toEqual(['router', 'approval', 'echo_step'])
|
||||
expect((loopOut.output?.iterations[0].router as RouterStepOutput).output?.branches[0].evaluation).toBe(false)
|
||||
expect((loopOut.output?.iterations[1].router as RouterStepOutput).output?.branches[0].evaluation).toBe(true)
|
||||
|
||||
|
||||
})
|
||||
|
||||
it('should pause and resume with two different steps in same flow successfully', async () => {
|
||||
const pauseResult1 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
const resumeResult1 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: pauseResult1,
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult1.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
const resumeResult2 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: resumeResult1.setVerdict({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
}),
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult2.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
it('should pause and resume successfully', async () => {
|
||||
const pauseResult = await flowExecutor.execute({
|
||||
action: simplePauseFlow,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(pauseResult.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
const currentState = pauseResult.currentState()
|
||||
expect(Object.keys(currentState).length).toBe(1)
|
||||
|
||||
const resumeResult = await flowExecutor.execute({
|
||||
action: simplePauseFlow,
|
||||
executionState: pauseResult,
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(resumeResult.currentState()).toEqual({
|
||||
'approval': {
|
||||
approved: true,
|
||||
},
|
||||
echo_step: {},
|
||||
})
|
||||
})
|
||||
|
||||
it('should pause at most one action when router has multiple branches with pause actions', async () => {
|
||||
const routerWithTwoPauseActions = buildRouterWithOneCondition({
|
||||
conditions: [
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_ALL_MATCH,
|
||||
children: [
|
||||
buildPieceAction({
|
||||
name: 'approval_1',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'approval_2',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = await flowExecutor.execute({
|
||||
action: routerWithTwoPauseActions,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
|
||||
const routerOutput = result.steps.router as RouterStepOutput
|
||||
expect(routerOutput).toBeDefined()
|
||||
expect(routerOutput.output).toBeDefined()
|
||||
|
||||
const executedBranches = routerOutput.output?.branches?.filter((branch) => branch.evaluation === true)
|
||||
expect(executedBranches).toHaveLength(2)
|
||||
|
||||
expect(result.steps.approval_1).toBeDefined()
|
||||
expect(result.steps.approval_1.status).toBe('PAUSED')
|
||||
expect(result.steps.approval_2).toBeUndefined()
|
||||
|
||||
expect(Object.keys(result.steps)).toEqual(['router', 'approval_1'])
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('flow with response', () => {
|
||||
|
||||
it('should execute return response successfully', async () => {
|
||||
const input = {
|
||||
responseType: 'json',
|
||||
fields: {
|
||||
status: 200,
|
||||
headers: {
|
||||
'random': 'header',
|
||||
},
|
||||
body: {
|
||||
'hello': 'world',
|
||||
},
|
||||
},
|
||||
respond: 'stop',
|
||||
}
|
||||
const response = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'random': 'header',
|
||||
},
|
||||
body: {
|
||||
'hello': 'world',
|
||||
},
|
||||
}
|
||||
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildPieceAction({
|
||||
name: 'http',
|
||||
pieceName: '@activepieces/piece-webhook',
|
||||
actionName: 'return_response',
|
||||
input,
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.SUCCEEDED,
|
||||
stopResponse: response,
|
||||
})
|
||||
expect(result.steps.http.output).toEqual(response)
|
||||
})
|
||||
|
||||
})
|
||||
433
activepieces-fork/packages/engine/test/handler/router-branching.test.ts
Executable file
433
activepieces-fork/packages/engine/test/handler/router-branching.test.ts
Executable file
@@ -0,0 +1,433 @@
|
||||
import { BranchCondition, BranchOperator, FlowAction, FlowRunStatus, RouterExecutionType } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildPieceAction, buildRouterWithOneCondition, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
function executeRouterActionWithOneCondition(children: FlowAction[], conditions: (BranchCondition | null)[], executionType: RouterExecutionType): Promise<FlowExecutorContext> {
|
||||
return flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({
|
||||
children,
|
||||
conditions,
|
||||
executionType,
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
}
|
||||
describe('router with branching different conditions', () => {
|
||||
it('should execute router with the first matching condition', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'anything',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should execute router with the all matching conditions', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute router but no branch will match', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
const routerOutput = result.steps.router.output as { branches: boolean[] }
|
||||
expect(routerOutput.branches).toEqual([
|
||||
{
|
||||
branchName: 'Test Branch',
|
||||
branchIndex: 1,
|
||||
evaluation: false,
|
||||
},
|
||||
{
|
||||
branchName: 'Test Branch',
|
||||
branchIndex: 2,
|
||||
evaluation: false,
|
||||
},
|
||||
])
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should execute fallback branch with first match execution type', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
expect(result.steps.fallback_mapper.output).toEqual({ 'key': 11 })
|
||||
})
|
||||
|
||||
it('should execute fallback branch with all match execution type', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
expect(result.steps.fallback_mapper.output).toEqual({ 'key': 11 })
|
||||
})
|
||||
|
||||
it('should not execute fallback branch when there is a matching condition in EXECUTE_FIRST_MATCH mode', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.fallback_mapper).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not execute fallback branch when there is a matching condition in EXECUTE_ALL_MATCH mode', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1.output).toEqual({ 'key': 6 })
|
||||
expect(result.steps.fallback_mapper).toBeUndefined()
|
||||
})
|
||||
it('should skip router', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({ children: [
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {},
|
||||
}),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], executionType: RouterExecutionType.EXECUTE_FIRST_MATCH, skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
})
|
||||
it('should skip router action in flow', async () => {
|
||||
const router: FlowAction = {
|
||||
...buildRouterWithOneCondition({ children: [
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {},
|
||||
}),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
skip: true }),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: false,
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: router, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
})
|
||||
122
activepieces-fork/packages/engine/test/handler/test-helper.ts
Normal file
122
activepieces-fork/packages/engine/test/handler/test-helper.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { ActionErrorHandlingOptions, BranchCondition, BranchExecutionType, CodeAction, FlowAction, FlowActionType, FlowVersionState, LoopOnItemsAction, PieceAction, ProgressUpdateType, PropertyExecutionType, RouterExecutionType, RunEnvironment } from '@activepieces/shared'
|
||||
import { EngineConstants } from '../../src/lib/handler/context/engine-constants'
|
||||
|
||||
export const generateMockEngineConstants = (params?: Partial<EngineConstants>): EngineConstants => {
|
||||
return new EngineConstants(
|
||||
{
|
||||
platformId: params?.platformId ?? 'platformId',
|
||||
timeoutInSeconds: params?.timeoutInSeconds ?? 10,
|
||||
flowId: params?.flowId ?? 'flowId',
|
||||
flowVersionId: params?.flowVersionId ?? 'flowVersionId',
|
||||
flowVersionState: params?.flowVersionState ?? FlowVersionState.DRAFT,
|
||||
flowRunId: params?.flowRunId ?? 'flowRunId',
|
||||
publicApiUrl: params?.publicApiUrl ?? 'http://127.0.0.1:4200/api/',
|
||||
internalApiUrl: params?.internalApiUrl ?? 'http://127.0.0.1:3000/',
|
||||
retryConstants: params?.retryConstants ?? {
|
||||
maxAttempts: 2,
|
||||
retryExponential: 1,
|
||||
retryInterval: 1,
|
||||
},
|
||||
engineToken: params?.engineToken ?? 'engineToken',
|
||||
projectId: params?.projectId ?? 'projectId',
|
||||
triggerPieceName: params?.triggerPieceName ?? 'mcp-trigger-piece-name',
|
||||
progressUpdateType: params?.progressUpdateType ?? ProgressUpdateType.NONE,
|
||||
serverHandlerId: params?.serverHandlerId ?? null,
|
||||
httpRequestId: params?.httpRequestId ?? null,
|
||||
resumePayload: params?.resumePayload,
|
||||
runEnvironment: params?.runEnvironment ?? RunEnvironment.TESTING,
|
||||
stepNameToTest: params?.stepNameToTest ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export function buildSimpleLoopAction({
|
||||
name,
|
||||
loopItems,
|
||||
firstLoopAction,
|
||||
skip,
|
||||
}: {
|
||||
name: string
|
||||
loopItems: string
|
||||
firstLoopAction?: FlowAction
|
||||
skip?: boolean
|
||||
}): LoopOnItemsAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Loop',
|
||||
type: FlowActionType.LOOP_ON_ITEMS,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
items: loopItems,
|
||||
},
|
||||
firstLoopAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRouterWithOneCondition({ children, conditions, executionType, skip }: { children: FlowAction[], conditions: (BranchCondition | null)[], executionType: RouterExecutionType, skip?: boolean }): FlowAction {
|
||||
return {
|
||||
name: 'router',
|
||||
displayName: 'Your Router Name',
|
||||
type: FlowActionType.ROUTER,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
branches: conditions.map((condition) => {
|
||||
if (condition === null) {
|
||||
return {
|
||||
branchType: BranchExecutionType.FALLBACK,
|
||||
branchName: 'Fallback Branch',
|
||||
}
|
||||
}
|
||||
return {
|
||||
conditions: [[condition]],
|
||||
branchType: BranchExecutionType.CONDITION,
|
||||
branchName: 'Test Branch',
|
||||
}
|
||||
}),
|
||||
executionType,
|
||||
},
|
||||
children,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCodeAction({ name, input, skip, nextAction, errorHandlingOptions }: { name: 'echo_step' | 'runtime' | 'echo_step_1', input: Record<string, unknown>, skip?: boolean, errorHandlingOptions?: ActionErrorHandlingOptions, nextAction?: FlowAction }): CodeAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Your Action Name',
|
||||
type: FlowActionType.CODE,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
input,
|
||||
sourceCode: {
|
||||
packageJson: '',
|
||||
code: '',
|
||||
},
|
||||
errorHandlingOptions,
|
||||
},
|
||||
nextAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPieceAction({ name, input, skip, pieceName, actionName, nextAction, errorHandlingOptions }: { errorHandlingOptions?: ActionErrorHandlingOptions, name: string, input: Record<string, unknown>, skip?: boolean, pieceName: string, actionName: string, nextAction?: FlowAction }): PieceAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Your Action Name',
|
||||
type: FlowActionType.PIECE,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
input,
|
||||
pieceName,
|
||||
pieceVersion: '1.0.0', // Not required since it's running in development mode
|
||||
actionName,
|
||||
propertySettings: Object.fromEntries(Object.entries(input).map(([key]) => [key, {
|
||||
type: PropertyExecutionType.MANUAL,
|
||||
schema: undefined,
|
||||
}])),
|
||||
errorHandlingOptions,
|
||||
},
|
||||
nextAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { runWithExponentialBackoff } from '../../src/lib/helper/error-handling'
|
||||
import { buildCodeAction, generateMockEngineConstants } from '../handler/test-helper'
|
||||
|
||||
describe('runWithExponentialBackoff', () => {
|
||||
const executionState = FlowExecutorContext.empty()
|
||||
const action = buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const constants = generateMockEngineConstants()
|
||||
const requestFunction = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return resultExecutionState when verdict is not FAILED', async () => {
|
||||
const resultExecutionState = FlowExecutorContext.empty().setVerdict({
|
||||
status: FlowRunStatus.SUCCEEDED,
|
||||
stopResponse: undefined,
|
||||
})
|
||||
requestFunction.mockResolvedValue(resultExecutionState)
|
||||
|
||||
const output = await runWithExponentialBackoff(executionState, action, constants, requestFunction)
|
||||
|
||||
expect(output).toEqual(resultExecutionState)
|
||||
expect(requestFunction).toHaveBeenCalledWith({ action, executionState, constants })
|
||||
})
|
||||
|
||||
|
||||
it('should retry and return resultExecutionState when verdict is FAILED and retry is enabled', async () => {
|
||||
const resultExecutionState = FlowExecutorContext.empty().setVerdict({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'runtime',
|
||||
displayName: 'runtime',
|
||||
message: 'Custom Runtime Error',
|
||||
},
|
||||
})
|
||||
|
||||
requestFunction.mockResolvedValue(resultExecutionState)
|
||||
|
||||
const output = await runWithExponentialBackoff(executionState, action, constants, requestFunction)
|
||||
|
||||
expect(output).toEqual(resultExecutionState)
|
||||
// Mock applies for the first attempt and second attempt is a real call which return success
|
||||
expect(requestFunction).toHaveBeenCalledTimes(2)
|
||||
expect(requestFunction).toHaveBeenCalledWith({ action, executionState, constants })
|
||||
expect(requestFunction).toHaveBeenCalledWith({ action, executionState, constants })
|
||||
})
|
||||
|
||||
it('should not retry and return resultExecutionState when verdict is FAILED but retry is disabled', async () => {
|
||||
const resultExecutionState = FlowExecutorContext.empty().setVerdict({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'runtime',
|
||||
displayName: 'runtime',
|
||||
message: 'Custom Runtime Error',
|
||||
},
|
||||
})
|
||||
|
||||
requestFunction.mockResolvedValue(resultExecutionState)
|
||||
|
||||
|
||||
const actionWithDisabledRetry = buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const output = await runWithExponentialBackoff(executionState, actionWithDisabledRetry, constants, requestFunction)
|
||||
|
||||
expect(output).toEqual(resultExecutionState)
|
||||
expect(requestFunction).toHaveBeenCalledTimes(1)
|
||||
expect(requestFunction).toHaveBeenCalledWith({ action: actionWithDisabledRetry, executionState, constants })
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
FlowActionType,
|
||||
GenericStepOutput,
|
||||
StepOutputStatus,
|
||||
} from '@activepieces/shared'
|
||||
import { loggingUtils } from '../../src/lib/helper/logging-utils'
|
||||
|
||||
describe('Logging Utils', () => {
|
||||
it('Should not truncate whole step if its log size exceeds limit', async () => {
|
||||
const steps = {
|
||||
mockStep: GenericStepOutput.create({
|
||||
type: FlowActionType.CODE,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {
|
||||
a: 'a'.repeat(1024 * 1024 * 12),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
// act
|
||||
const result = await loggingUtils.trimExecution(steps)
|
||||
|
||||
// assert
|
||||
expect((result.mockStep.input as Record<string, string>).a.length).toBeLessThan(1024 * 1024 * 12)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
code: async (params) => {
|
||||
return params;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
code: async (params) => {
|
||||
return params;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
code: async (params) => {
|
||||
throw new Error('Custom Runtime Error');
|
||||
}
|
||||
};
|
||||
|
||||
753
activepieces-fork/packages/engine/test/services/props-resolver.test.ts
Executable file
753
activepieces-fork/packages/engine/test/services/props-resolver.test.ts
Executable file
@@ -0,0 +1,753 @@
|
||||
import { ApFile, LATEST_CONTEXT_VERSION, PieceAuth, Property } from '@activepieces/pieces-framework'
|
||||
import { FlowActionType, FlowTriggerType, GenericStepOutput, PropertyExecutionType, PropertySettings, StepOutputStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { StepExecutionPath } from '../../src/lib/handler/context/step-execution-path'
|
||||
import { propsProcessor } from '../../src/lib/variables/props-processor'
|
||||
import { createPropsResolver } from '../../src/lib/variables/props-resolver'
|
||||
|
||||
const propsResolverService = createPropsResolver({
|
||||
projectId: 'PROJECT_ID',
|
||||
engineToken: 'WORKER_TOKEN',
|
||||
apiUrl: 'http://127.0.0.1:3000',
|
||||
contextVersion: LATEST_CONTEXT_VERSION,
|
||||
})
|
||||
|
||||
const executionState = FlowExecutorContext.empty()
|
||||
.upsertStep(
|
||||
'trigger',
|
||||
GenericStepOutput.create({
|
||||
type: FlowTriggerType.PIECE,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: {
|
||||
items: [5, 'a'],
|
||||
name: 'John',
|
||||
price: 6.4,
|
||||
users: [
|
||||
{
|
||||
name: 'Alice',
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
},
|
||||
],
|
||||
lastNames: [
|
||||
'Smith',
|
||||
'Doe',
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
.upsertStep('step_1',
|
||||
GenericStepOutput.create({
|
||||
|
||||
type: FlowActionType.PIECE,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}))
|
||||
.upsertStep('step_2', GenericStepOutput.create({
|
||||
type: FlowActionType.PIECE,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
|
||||
}))
|
||||
|
||||
|
||||
|
||||
describe('Props resolver', () => {
|
||||
|
||||
|
||||
test('Test resolve inside nested loops', async () => {
|
||||
|
||||
const modifiedExecutionState = executionState.upsertStep('step_3', GenericStepOutput.create({
|
||||
type: FlowActionType.LOOP_ON_ITEMS,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: {
|
||||
iterations: [
|
||||
{
|
||||
'step_8': GenericStepOutput.create({
|
||||
type: FlowActionType.PIECE,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: {
|
||||
delayForInMs: 20000,
|
||||
success: true,
|
||||
},
|
||||
}),
|
||||
'step_4': GenericStepOutput.create({
|
||||
type: FlowActionType.LOOP_ON_ITEMS,
|
||||
status: StepOutputStatus.SUCCEEDED,
|
||||
input: {},
|
||||
output: {
|
||||
iterations: [
|
||||
{
|
||||
'step_7': GenericStepOutput.create({
|
||||
'type': FlowActionType.PIECE,
|
||||
'status': StepOutputStatus.SUCCEEDED,
|
||||
'input': {
|
||||
'unit': 'seconds',
|
||||
'delayFor': '20',
|
||||
},
|
||||
'output': {
|
||||
'delayForInMs': 20000,
|
||||
'success': true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
item: 1,
|
||||
index: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
item: 1,
|
||||
index: 0,
|
||||
},
|
||||
})).setCurrentPath(StepExecutionPath.empty()
|
||||
.loopIteration({
|
||||
loopName: 'step_3',
|
||||
iteration: 0,
|
||||
})
|
||||
.loopIteration({
|
||||
loopName: 'step_4',
|
||||
iteration: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
const { resolvedInput: secondLevelResolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_7.delayForInMs}}', executionState: modifiedExecutionState })
|
||||
expect(secondLevelResolvedInput).toEqual(20000)
|
||||
const { resolvedInput: firstLevelResolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_8.delayForInMs}}', executionState: modifiedExecutionState })
|
||||
expect(firstLevelResolvedInput).toEqual(20000)
|
||||
|
||||
})
|
||||
test('Test resolve text with no variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'Hello world!', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
'Hello world!',
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve text with double variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'Price is {{ trigger.price }}', executionState })
|
||||
expect(resolvedInput,
|
||||
).toEqual('Price is 6.4')
|
||||
})
|
||||
|
||||
|
||||
test('Test resolve object steps variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{ {"where": "a"} }}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
{
|
||||
where: 'a',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve object steps variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
{
|
||||
items: [5, 'a'],
|
||||
name: 'John',
|
||||
price: 6.4,
|
||||
users: [
|
||||
{
|
||||
name: 'Alice',
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
},
|
||||
],
|
||||
lastNames: [
|
||||
'Smith',
|
||||
'Doe',
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('flatten array path', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{flattenNestedKeys(trigger, [\'users\',\'name\'])}}', executionState })
|
||||
expect(resolvedInput).toEqual(['Alice', 'Bob'])
|
||||
})
|
||||
|
||||
test('merge multiple flatten array paths', async ()=>{
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{flattenNestedKeys(trigger, [\'users\',\'name\'])}} {{trigger.lastNames}}', executionState })
|
||||
expect(resolvedInput).toEqual(['Alice Smith', 'Bob Doe'])
|
||||
})
|
||||
|
||||
test('Test resolve steps variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.name}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
'John',
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve multiple variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.name}} {{trigger.name}}', executionState })
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual('John John')
|
||||
})
|
||||
|
||||
test('Test resolve variable array items', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput:
|
||||
'{{trigger.items[0]}} {{trigger.items[1]}}',
|
||||
executionState,
|
||||
})
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual('5 a')
|
||||
})
|
||||
|
||||
test('Test resolve array variable', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.items}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
[5, 'a'],
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve integer from variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.items[0]}}', executionState })
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual(5)
|
||||
})
|
||||
|
||||
test('Test resolve text with undefined variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput:
|
||||
'test {{configs.bar}} {{trigger.items[4]}}',
|
||||
executionState,
|
||||
})
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual('test ')
|
||||
})
|
||||
|
||||
test('Test resolve empty text', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '', executionState })
|
||||
expect(resolvedInput).toEqual('')
|
||||
})
|
||||
|
||||
|
||||
test('Test resolve empty variable operator', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{}}', executionState })
|
||||
expect(resolvedInput).toEqual('')
|
||||
})
|
||||
|
||||
test('Test resolve object', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput:
|
||||
{
|
||||
input: {
|
||||
foo: 'bar',
|
||||
nums: [1, 2, '{{trigger.items[0]}}'],
|
||||
var: '{{trigger.price}}',
|
||||
},
|
||||
},
|
||||
executionState,
|
||||
})
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual({ input: { foo: 'bar', nums: [1, 2, 5], var: 6.4 } })
|
||||
})
|
||||
|
||||
test('Test resolve boolean from variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{step_1.success}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve addition from variables', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{trigger.price + 2 - 3}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
6.4 + 2 - 3,
|
||||
)
|
||||
})
|
||||
|
||||
test('Test resolve text with array variable', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: 'items are {{trigger.items}}', executionState })
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual('items are [5,"a"]')
|
||||
})
|
||||
|
||||
test('Test resolve text with object variable', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput:
|
||||
'values from trigger step: {{trigger}}',
|
||||
executionState,
|
||||
})
|
||||
expect(
|
||||
resolvedInput,
|
||||
).toEqual('values from trigger step: {"items":[5,"a"],"name":"John","price":6.4,"users":[{"name":"Alice"},{"name":"Bob"}],"lastNames":["Smith","Doe"]}')
|
||||
})
|
||||
|
||||
test('Test use built-in Math Min function', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{Math.min(trigger.price + 2 - 3, 2)}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
2,
|
||||
)
|
||||
})
|
||||
|
||||
test('Test use built-in Math Max function', async () => {
|
||||
const { resolvedInput } = await propsResolverService.resolve({ unresolvedInput: '{{Math.max(trigger.price + 2, 2)}}', executionState })
|
||||
expect(resolvedInput).toEqual(
|
||||
8.4,
|
||||
)
|
||||
})
|
||||
|
||||
it('should not compress memory file in native value in non-logs mode', async () => {
|
||||
const input = {
|
||||
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
|
||||
}
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput: input,
|
||||
executionState,
|
||||
})
|
||||
expect(resolvedInput).toEqual({
|
||||
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not compress memory file in referenced value in non-logs mode', async () => {
|
||||
const input = {
|
||||
base64: '{{step_2}}',
|
||||
}
|
||||
const { resolvedInput } = await propsResolverService.resolve({
|
||||
unresolvedInput: input,
|
||||
executionState,
|
||||
})
|
||||
expect(resolvedInput).toEqual({
|
||||
base64: 'memory://{"fileName":"hello.png","data":"iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z"}',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
it('should return base64 from base64 with mime only', async () => {
|
||||
const input = {
|
||||
base64WithMime: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
|
||||
base64: 'iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
|
||||
}
|
||||
const props = {
|
||||
base64WithMime: Property.File({
|
||||
displayName: 'Base64',
|
||||
required: true,
|
||||
}),
|
||||
base64: Property.File({
|
||||
displayName: 'Base64',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
expect(processedInput).toEqual({
|
||||
base64: null,
|
||||
base64WithMime: new ApFile('unknown.png', Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z', 'base64'), 'png'),
|
||||
})
|
||||
expect(errors).toEqual({
|
||||
'base64': [
|
||||
'Expected file url or base64 with mimeType, received: iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAYAAADaI1cbAAA0h0lEQVR4AezdA5AlPx7A8Zxt27Z9r5PB2SidWTqbr26S9Hr/tm3btu3723eDJD3r15ec17vzXr+Z',
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve files inside the array properties', async () => {
|
||||
const input = {
|
||||
documents: [
|
||||
{
|
||||
file: 'https://cdn.activepieces.com/brand/logo.svg?token=123',
|
||||
},
|
||||
],
|
||||
}
|
||||
const props = {
|
||||
documents: Property.Array({
|
||||
displayName: 'Documents',
|
||||
required: true,
|
||||
properties: {
|
||||
file: Property.File({
|
||||
displayName: 'File',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
expect(processedInput.documents[0].file).toBeDefined()
|
||||
expect(processedInput.documents[0].file.extension).toBe('svg')
|
||||
expect(processedInput.documents[0].file.filename).toBe('logo.svg')
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
|
||||
|
||||
it('should return error for invalid file inside the array properties', async () => {
|
||||
const input = {
|
||||
documents: [
|
||||
{
|
||||
file: 'invalid-url',
|
||||
},
|
||||
],
|
||||
}
|
||||
const props = {
|
||||
documents: Property.Array({
|
||||
displayName: 'Documents',
|
||||
required: true,
|
||||
properties: {
|
||||
file: Property.File({
|
||||
displayName: 'File',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
expect(processedInput.documents[0].file).toBeNull()
|
||||
expect(errors).toEqual({
|
||||
'documents': {
|
||||
properties: [{
|
||||
file: [
|
||||
'Expected file url or base64 with mimeType, received: invalid-url',
|
||||
],
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should return images for image url', async () => {
|
||||
const input = {
|
||||
file: 'https://cdn.activepieces.com/brand/logo.svg?token=123',
|
||||
}
|
||||
const props = {
|
||||
file: Property.File({
|
||||
displayName: 'File',
|
||||
required: true,
|
||||
}),
|
||||
|
||||
}
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
expect(processedInput.file).toBeDefined()
|
||||
expect(processedInput.file.extension).toBe('svg')
|
||||
expect(processedInput.file.filename).toBe('logo.svg')
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
// Test with invalid url
|
||||
it('should return error for invalid data', async () => {
|
||||
const input = {
|
||||
file: 'https://google.com',
|
||||
nullFile: null,
|
||||
nullOptionalFile: null,
|
||||
}
|
||||
const props = {
|
||||
file: Property.File({
|
||||
displayName: 'File',
|
||||
required: true,
|
||||
}),
|
||||
nullFile: Property.File({
|
||||
displayName: 'File',
|
||||
required: true,
|
||||
}),
|
||||
nullOptionalFile: Property.File({
|
||||
displayName: 'File',
|
||||
required: false,
|
||||
}),
|
||||
}
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.file).toBeDefined()
|
||||
expect(processedInput.file.extension).toBe('html')
|
||||
expect(processedInput.file.filename).toBe('unknown.html')
|
||||
expect(processedInput.nullFile).toBeNull()
|
||||
expect(processedInput.nullOptionalFile).toBeNull()
|
||||
|
||||
expect(errors).toEqual({
|
||||
'nullFile': [
|
||||
'Expected file url or base64 with mimeType, received: null',
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it('should return casted number for text', async () => {
|
||||
const input = {
|
||||
price: '0',
|
||||
auth: {
|
||||
age: '12',
|
||||
},
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, {
|
||||
price: Property.Number({
|
||||
displayName: 'Price',
|
||||
required: true,
|
||||
}),
|
||||
}, PieceAuth.CustomAuth({
|
||||
required: true,
|
||||
props: {
|
||||
age: Property.Number({
|
||||
displayName: 'age',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}), true, {})
|
||||
|
||||
expect(processedInput).toEqual({
|
||||
auth: {
|
||||
age: 12,
|
||||
},
|
||||
price: 0,
|
||||
})
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
it('should not error if auth configured, but no auth provided in input', async () => {
|
||||
const input = {
|
||||
price: '0',
|
||||
}
|
||||
const props = {
|
||||
price: Property.Number({
|
||||
displayName: 'Price',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.CustomAuth({
|
||||
required: true,
|
||||
props: {},
|
||||
}), false, {})
|
||||
|
||||
expect(processedInput).toEqual({
|
||||
price: 0,
|
||||
})
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
|
||||
it('should flatten arrays inside DYNAMIC properties', async () => {
|
||||
const input = {
|
||||
dynamicProp: {
|
||||
items: {
|
||||
id: [1, 2],
|
||||
name: ['Item 1', 'Item 2'],
|
||||
},
|
||||
},
|
||||
}
|
||||
const propertySettings: Record<string, PropertySettings> = {
|
||||
dynamicProp: {
|
||||
type: PropertyExecutionType.MANUAL,
|
||||
schema: {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.Number({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
dynamicProp: Property.DynamicProperties({
|
||||
auth: undefined,
|
||||
displayName: 'Dynamic Property',
|
||||
required: true,
|
||||
props: async () => {
|
||||
return {}
|
||||
},
|
||||
refreshers: [],
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, propertySettings)
|
||||
|
||||
expect(processedInput.dynamicProp.items).toEqual([
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Array Flatter Processor', () => {
|
||||
it('should flatten array of objects', async () => {
|
||||
const input = {
|
||||
items: {
|
||||
id: [1, 2],
|
||||
name: ['Item 1', 'Item 2'],
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.Number({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.items).toEqual([
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle non-array properties gracefully', async () => {
|
||||
const input = {
|
||||
items: {
|
||||
id: [1, 2],
|
||||
name: 'Single Item', // Non-array property
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.Number({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.items).toEqual([
|
||||
{ id: 1, name: 'Single Item' },
|
||||
{ id: 2, name: 'Single Item' },
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle arrays of unequal length', async () => {
|
||||
const input = {
|
||||
items: {
|
||||
id: [1, 2, 3], // Longer array
|
||||
name: ['Item 1', 'Item 2'], // Shorter array
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.Number({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.items).toEqual([
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
{ id: 3, name: undefined }, // Handle missing name
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
it('should convert number to string for ShortText properties', async () => {
|
||||
const input = {
|
||||
items: {
|
||||
id: 123,
|
||||
name: 'Item Name',
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.ShortText({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.items).toEqual([
|
||||
{ id: '123', name: 'Item Name' },
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
|
||||
|
||||
it('should handle arrays with string values', async () => {
|
||||
const input = {
|
||||
items: {
|
||||
id: '1',
|
||||
name: 'item1',
|
||||
},
|
||||
}
|
||||
const props = {
|
||||
items: Property.Array({
|
||||
displayName: 'Items',
|
||||
required: true,
|
||||
properties: {
|
||||
id: Property.ShortText({
|
||||
displayName: 'ID',
|
||||
required: true,
|
||||
}),
|
||||
name: Property.LongText({
|
||||
displayName: 'Name',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(input, props, PieceAuth.None(), false, {})
|
||||
|
||||
expect(processedInput.items).toEqual([
|
||||
{ id: '1', name: 'item1' },
|
||||
])
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,342 @@
|
||||
import { PieceAuth, Property } from '@activepieces/pieces-framework'
|
||||
import { propsProcessor } from '../../src/lib/variables/props-processor'
|
||||
describe('Property Validation', () => {
|
||||
describe('required properties', () => {
|
||||
it('should validate required string property', async () => {
|
||||
const props = {
|
||||
text: Property.ShortText({
|
||||
displayName: 'Text',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ text: 'valid text' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ text: null },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(nullErrors).toEqual({
|
||||
text: ['Expected string, received: null'],
|
||||
})
|
||||
|
||||
const { errors: undefinedErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ text: undefined },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(undefinedErrors).toEqual({
|
||||
text: ['Expected string, received: undefined'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate required number property', async () => {
|
||||
const props = {
|
||||
number: Property.Number({
|
||||
displayName: 'Number',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ number: 42 },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ number: null },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(nullErrors).toEqual({
|
||||
number: ['Expected number, received: null'],
|
||||
})
|
||||
|
||||
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ number: 'not a number' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(typeErrors).toEqual({
|
||||
number: ['Expected number, received: not a number'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate required datetime property', async () => {
|
||||
const props = {
|
||||
date: Property.DateTime({
|
||||
displayName: 'DateTime',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ date: '2024-03-14T12:00:00.000Z' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: invalidErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ date: 'not a date' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(invalidErrors).toEqual({
|
||||
date: ['Invalid datetime format. Expected ISO format (e.g. 2024-03-14T12:00:00.000Z), received: not a date'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate required array property', async () => {
|
||||
const props = {
|
||||
array: Property.Array({
|
||||
displayName: 'Array',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ array: [1, 2, 3] },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ array: 'not an array' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(typeErrors).toEqual({
|
||||
array: ['Expected array, received: not an array'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate required json property', async () => {
|
||||
const props = {
|
||||
json: Property.Json({
|
||||
displayName: 'JSON',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: { key: 'value' } },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: validJsonStringErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: '{"key": "value"}' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validJsonStringErrors).toEqual({})
|
||||
|
||||
const { errors: validArrayErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: [1, 2, 3] },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validArrayErrors).toEqual({})
|
||||
|
||||
const { errors: validArrayStringErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: '[1, 2, 3]' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validArrayStringErrors).toEqual({})
|
||||
|
||||
const { errors: invalidJsonErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: 'not a json object' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(invalidJsonErrors).toEqual({
|
||||
json: ['Expected JSON, received: not a json object'],
|
||||
})
|
||||
|
||||
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ json: null },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(nullErrors).toEqual({
|
||||
json: ['Expected JSON, received: null'],
|
||||
})
|
||||
})
|
||||
it('should validate required object property', async () => {
|
||||
const props = {
|
||||
object: Property.Object({
|
||||
displayName: 'Object',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors: validErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ object: { key: 'value' } },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(validErrors).toEqual({})
|
||||
|
||||
const { errors: nullErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ object: null },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(nullErrors).toEqual({
|
||||
object: ['Expected object, received: null'],
|
||||
})
|
||||
|
||||
const { errors: typeErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ object: 'not an object' },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(typeErrors).toEqual({
|
||||
object: ['Expected object, received: not an object'],
|
||||
})
|
||||
|
||||
const { errors: jsonStringErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ object: JSON.stringify({ key: 'value' }) },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(jsonStringErrors).toEqual({})
|
||||
|
||||
const { errors: undefinedErrors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{ object: { key: 'value' } },
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(undefinedErrors).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional properties', () => {
|
||||
it('should validate optional properties', async () => {
|
||||
const props = {
|
||||
text: Property.ShortText({
|
||||
displayName: 'Text',
|
||||
required: false,
|
||||
}),
|
||||
number: Property.Number({
|
||||
displayName: 'Number',
|
||||
required: false,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{
|
||||
text: null,
|
||||
number: undefined,
|
||||
},
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
expect(errors).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('type validation', () => {
|
||||
it('should validate property types', async () => {
|
||||
const props = {
|
||||
string: Property.ShortText({
|
||||
displayName: 'Text',
|
||||
required: true,
|
||||
}),
|
||||
number: Property.Number({
|
||||
displayName: 'Number',
|
||||
required: true,
|
||||
}),
|
||||
boolean: Property.Checkbox({
|
||||
displayName: 'Checkbox',
|
||||
required: true,
|
||||
}),
|
||||
array: Property.Array({
|
||||
displayName: 'Array',
|
||||
required: true,
|
||||
}),
|
||||
object: Property.Object({
|
||||
displayName: 'Object',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const { errors } = await propsProcessor.applyProcessorsAndValidators(
|
||||
{
|
||||
string: 42,
|
||||
number: 'not a number',
|
||||
boolean: 'not a boolean',
|
||||
array: 'not an array',
|
||||
object: 'not an object',
|
||||
},
|
||||
props,
|
||||
PieceAuth.None(),
|
||||
false,
|
||||
{},
|
||||
)
|
||||
|
||||
expect(errors).toEqual({
|
||||
number: ['Expected number, received: not a number'],
|
||||
boolean: ['Expected boolean, received: not a boolean'],
|
||||
array: ['Expected array, received: not an array'],
|
||||
object: ['Expected object, received: not an object'],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user