516 lines
19 KiB
TypeScript
516 lines
19 KiB
TypeScript
import {
|
|
FlowOperationType,
|
|
FlowStatus,
|
|
FlowVersionState,
|
|
PrincipalType,
|
|
} from '@activepieces/shared'
|
|
import { FastifyInstance } from 'fastify'
|
|
import { StatusCodes } from 'http-status-codes'
|
|
import { initializeDatabase } from '../../../../src/app/database'
|
|
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
|
import { setupServer } from '../../../../src/app/server'
|
|
import { generateMockToken } from '../../../helpers/auth'
|
|
import {
|
|
createMockFlow,
|
|
createMockFlowVersion,
|
|
mockAndSaveBasicSetup,
|
|
} from '../../../helpers/mocks'
|
|
|
|
let app: FastifyInstance | null = null
|
|
|
|
beforeAll(async () => {
|
|
await initializeDatabase({ runMigrations: false })
|
|
app = await setupServer()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await databaseConnection().destroy()
|
|
await app?.close()
|
|
})
|
|
|
|
describe('Flow API', () => {
|
|
describe('Create Flow endpoint', () => {
|
|
it('Adds an empty flow', async () => {
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
const mockCreateFlowRequest = {
|
|
displayName: 'test flow',
|
|
projectId: mockProject.id,
|
|
metadata: {
|
|
foo: 'bar',
|
|
},
|
|
}
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'POST',
|
|
url: '/v1/flows',
|
|
query: {
|
|
projectId: mockProject.id,
|
|
},
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
body: mockCreateFlowRequest,
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
|
const responseBody = response?.json()
|
|
|
|
expect(Object.keys(responseBody)).toHaveLength(12)
|
|
expect(responseBody?.id).toHaveLength(21)
|
|
expect(responseBody?.created).toBeDefined()
|
|
expect(responseBody?.updated).toBeDefined()
|
|
expect(responseBody?.projectId).toBe(mockProject.id)
|
|
expect(responseBody?.folderId).toBeNull()
|
|
expect(responseBody?.status).toBe('DISABLED')
|
|
expect(responseBody?.publishedVersionId).toBeNull()
|
|
expect(responseBody?.schedule).toBeNull()
|
|
expect(responseBody?.metadata).toMatchObject({ foo: 'bar' })
|
|
expect(responseBody?.handshakeConfiguration).toBeNull()
|
|
|
|
expect(Object.keys(responseBody?.version)).toHaveLength(11)
|
|
expect(responseBody?.version?.id).toHaveLength(21)
|
|
expect(responseBody?.version?.created).toBeDefined()
|
|
expect(responseBody?.version?.updated).toBeDefined()
|
|
expect(responseBody?.version?.updatedBy).toBeNull()
|
|
expect(responseBody?.version?.flowId).toBe(responseBody?.id)
|
|
expect(responseBody?.version?.displayName).toBe('test flow')
|
|
expect(Object.keys(responseBody?.version?.trigger)).toHaveLength(5)
|
|
expect(responseBody?.version?.trigger.type).toBe('EMPTY')
|
|
expect(responseBody?.version?.trigger.name).toBe('trigger')
|
|
expect(responseBody?.version?.trigger.settings).toMatchObject({})
|
|
expect(responseBody?.version?.trigger.valid).toBe(false)
|
|
expect(responseBody?.version?.trigger.displayName).toBe('Select Trigger')
|
|
expect(responseBody?.version?.valid).toBe(false)
|
|
expect(responseBody?.version?.state).toBe('DRAFT')
|
|
})
|
|
})
|
|
|
|
describe('Update status endpoint', () => {
|
|
it('Enables a disabled Flow', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.DISABLED,
|
|
})
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({
|
|
flowId: mockFlow.id,
|
|
updatedBy: mockOwner.id,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
await databaseConnection().getRepository('flow').update(mockFlow.id, {
|
|
publishedVersionId: mockFlowVersion.id,
|
|
})
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
const mockUpdateFlowStatusRequest = {
|
|
type: FlowOperationType.CHANGE_STATUS,
|
|
request: {
|
|
status: 'ENABLED',
|
|
},
|
|
}
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'POST',
|
|
url: `/v1/flows/${mockFlow.id}`,
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
body: mockUpdateFlowStatusRequest,
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(Object.keys(responseBody)).toHaveLength(12)
|
|
expect(responseBody?.id).toBe(mockFlow.id)
|
|
expect(responseBody?.created).toBeDefined()
|
|
expect(responseBody?.updated).toBeDefined()
|
|
expect(responseBody?.projectId).toBe(mockProject.id)
|
|
expect(responseBody?.folderId).toBeNull()
|
|
expect(responseBody?.status).toBe('ENABLED')
|
|
expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
|
|
expect(responseBody?.schedule).toBeNull()
|
|
expect(responseBody?.metadata).toBeNull()
|
|
expect(responseBody?.handshakeConfiguration).toBeNull()
|
|
|
|
expect(Object.keys(responseBody?.version)).toHaveLength(11)
|
|
expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
|
|
})
|
|
|
|
it('Disables an enabled Flow', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.ENABLED,
|
|
})
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({
|
|
flowId: mockFlow.id,
|
|
updatedBy: mockOwner.id,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
await databaseConnection().getRepository('flow').update(mockFlow.id, {
|
|
publishedVersionId: mockFlowVersion.id,
|
|
})
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
const mockUpdateFlowStatusRequest = {
|
|
type: FlowOperationType.CHANGE_STATUS,
|
|
request: {
|
|
status: 'DISABLED',
|
|
},
|
|
}
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'POST',
|
|
url: `/v1/flows/${mockFlow.id}`,
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
body: mockUpdateFlowStatusRequest,
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(Object.keys(responseBody)).toHaveLength(12)
|
|
expect(responseBody?.id).toBe(mockFlow.id)
|
|
expect(responseBody?.created).toBeDefined()
|
|
expect(responseBody?.updated).toBeDefined()
|
|
expect(responseBody?.projectId).toBe(mockProject.id)
|
|
expect(responseBody?.folderId).toBeNull()
|
|
expect(responseBody?.status).toBe('DISABLED')
|
|
expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
|
|
expect(responseBody?.schedule).toBeNull()
|
|
expect(responseBody?.metadata).toBeNull()
|
|
expect(responseBody?.handshakeConfiguration).toBeNull()
|
|
|
|
expect(Object.keys(responseBody?.version)).toHaveLength(11)
|
|
expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
|
|
})
|
|
})
|
|
|
|
describe('Update published version id endpoint', () => {
|
|
it('Publishes latest draft version', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.DISABLED,
|
|
})
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({
|
|
flowId: mockFlow.id,
|
|
updatedBy: mockOwner.id,
|
|
state: FlowVersionState.DRAFT,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
const mockToken = await generateMockToken({
|
|
id: mockOwner.id,
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
})
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'POST',
|
|
url: `/v1/flows/${mockFlow.id}`,
|
|
body: {
|
|
type: FlowOperationType.LOCK_AND_PUBLISH,
|
|
request: {},
|
|
},
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(Object.keys(responseBody)).toHaveLength(12)
|
|
expect(responseBody?.id).toBe(mockFlow.id)
|
|
expect(responseBody?.created).toBeDefined()
|
|
expect(responseBody?.updated).toBeDefined()
|
|
expect(responseBody?.projectId).toBe(mockProject.id)
|
|
expect(responseBody?.folderId).toBeNull()
|
|
expect(responseBody?.status).toBe('ENABLED')
|
|
expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
|
|
expect(responseBody?.schedule).toBeNull()
|
|
expect(responseBody?.metadata).toBeNull()
|
|
expect(responseBody?.handshakeConfiguration).toBeNull()
|
|
|
|
expect(Object.keys(responseBody?.version)).toHaveLength(11)
|
|
expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
|
|
expect(responseBody?.version?.state).toBe('LOCKED')
|
|
})
|
|
})
|
|
|
|
describe('List Flows endpoint', () => {
|
|
it('Filters Flows by status', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockEnabledFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.ENABLED,
|
|
})
|
|
const mockDisabledFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.DISABLED,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow')
|
|
.save([mockEnabledFlow, mockDisabledFlow])
|
|
|
|
const mockEnabledFlowVersion = createMockFlowVersion({
|
|
flowId: mockEnabledFlow.id,
|
|
})
|
|
const mockDisabledFlowVersion = createMockFlowVersion({
|
|
flowId: mockDisabledFlow.id,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockEnabledFlowVersion, mockDisabledFlowVersion])
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'GET',
|
|
url: '/v1/flows',
|
|
query: {
|
|
projectId: mockProject.id,
|
|
status: 'ENABLED',
|
|
},
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(responseBody.data).toHaveLength(1)
|
|
expect(responseBody.data[0].id).toBe(mockEnabledFlow.id)
|
|
})
|
|
|
|
it('Populates Flow version', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({ projectId: mockProject.id })
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'GET',
|
|
url: '/v1/flows',
|
|
query: {
|
|
projectId: mockProject.id,
|
|
},
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(responseBody?.data).toHaveLength(1)
|
|
expect(responseBody?.data?.[0]?.id).toBe(mockFlow.id)
|
|
expect(responseBody?.data?.[0]?.version?.id).toBe(mockFlowVersion.id)
|
|
})
|
|
|
|
it('Fails if a flow with no version exists', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({ projectId: mockProject.id })
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'GET',
|
|
url: '/v1/flows',
|
|
query: {
|
|
projectId: mockProject.id,
|
|
},
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
|
const responseBody = response?.json()
|
|
|
|
expect(responseBody?.code).toBe('ENTITY_NOT_FOUND')
|
|
expect(responseBody?.params?.entityType).toBe('FlowVersion')
|
|
expect(responseBody?.params?.message).toBe(`flowId=${mockFlow.id}`)
|
|
})
|
|
})
|
|
|
|
describe('Update Metadata endpoint', () => {
|
|
it('Updates flow metadata', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
// create a flow with no metadata
|
|
const mockFlow = createMockFlow({ projectId: mockProject.id })
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.USER,
|
|
projectId: mockProject.id,
|
|
id: mockOwner.id,
|
|
})
|
|
|
|
const updatedMetadata = { foo: 'bar' }
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'POST',
|
|
url: `/v1/flows/${mockFlow.id}`,
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
body: {
|
|
type: FlowOperationType.UPDATE_METADATA,
|
|
request: {
|
|
metadata: updatedMetadata,
|
|
},
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(responseBody.id).toBe(mockFlow.id)
|
|
expect(responseBody.metadata).toEqual(updatedMetadata)
|
|
|
|
// Verify metadata was actually persisted in the database
|
|
const updatedFlow = await databaseConnection()
|
|
.getRepository('flow')
|
|
.findOneBy({ id: mockFlow.id })
|
|
|
|
expect(updatedFlow?.metadata).toEqual(updatedMetadata)
|
|
})
|
|
})
|
|
|
|
describe('Export Flow Template endpoint', () => {
|
|
it('Exports a flow template using an API key', async () => {
|
|
// arrange
|
|
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
|
|
|
|
const mockFlow = createMockFlow({
|
|
projectId: mockProject.id,
|
|
status: FlowStatus.ENABLED,
|
|
})
|
|
await databaseConnection().getRepository('flow').save([mockFlow])
|
|
|
|
const mockFlowVersion = createMockFlowVersion({
|
|
flowId: mockFlow.id,
|
|
updatedBy: mockOwner.id,
|
|
})
|
|
await databaseConnection()
|
|
.getRepository('flow_version')
|
|
.save([mockFlowVersion])
|
|
|
|
const mockApiKey = 'test_api_key'
|
|
const mockToken = await generateMockToken({
|
|
type: PrincipalType.SERVICE,
|
|
projectId: mockProject.id,
|
|
id: mockApiKey,
|
|
})
|
|
|
|
// act
|
|
const response = await app?.inject({
|
|
method: 'GET',
|
|
url: `/v1/flows/${mockFlow.id}/template`,
|
|
headers: {
|
|
authorization: `Bearer ${mockToken}`,
|
|
},
|
|
})
|
|
|
|
// assert
|
|
expect(response?.statusCode).toBe(StatusCodes.OK)
|
|
const responseBody = response?.json()
|
|
|
|
expect(responseBody).toHaveProperty('name')
|
|
expect(responseBody).toHaveProperty('description')
|
|
expect(responseBody).toHaveProperty('template')
|
|
expect(responseBody.template).toHaveProperty('trigger')
|
|
})
|
|
})
|
|
})
|