mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
Fix server Jest test structure and standardize test scripts (#5909)
* fix(server): restructure Jest tests and standardize test scripts * chore(main.yml): run build before test
This commit is contained in:
@@ -31,10 +31,10 @@ jobs:
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- run: pnpm install
|
||||
- run: pnpm lint
|
||||
- run: pnpm test:coverage
|
||||
- run: pnpm build
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
- run: pnpm test:coverage
|
||||
- name: Cypress install
|
||||
run: pnpm cypress install
|
||||
- name: Install dependencies (Cypress Action)
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"cypress:run": "cypress run",
|
||||
"e2e": "start-server-and-test dev http://localhost:3000 cypress:run",
|
||||
"cypress:ci": "START_SERVER_AND_TEST_INSECURE=1 start-server-and-test start https-get://localhost:3000 cypress:run",
|
||||
"test": "jest --runInBand --detectOpenHandles --forceExit"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"homepage": "https://flowiseai.com",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as Server from '../src'
|
||||
import { getRunningExpressApp } from '../src/utils/getRunningExpressApp'
|
||||
import { organizationUserRouteTest } from './routes/v1/organization-user.route.test'
|
||||
import { userRouteTest } from './routes/v1/user.route.test'
|
||||
import { apiKeyTest } from './utils/api-key.util.test'
|
||||
import { sanitizeFlowDataTest } from './utils/sanitizeFlowData.test'
|
||||
|
||||
// ⏱️ Extend test timeout to 6 minutes for long setups (increase as tests grow)
|
||||
jest.setTimeout(360000)
|
||||
|
||||
beforeAll(async () => {
|
||||
await Server.start()
|
||||
|
||||
// ⏳ Wait 3 minutes for full server and database init (esp. on lower end hardware)
|
||||
await new Promise((resolve) => setTimeout(resolve, 3 * 60 * 1000))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await getRunningExpressApp().stopApp()
|
||||
})
|
||||
|
||||
describe('Routes Test', () => {
|
||||
userRouteTest()
|
||||
organizationUserRouteTest()
|
||||
})
|
||||
|
||||
describe('Utils Test', () => {
|
||||
apiKeyTest()
|
||||
sanitizeFlowDataTest()
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import supertest from 'supertest'
|
||||
import { getRunningExpressApp } from '../../../src/utils/getRunningExpressApp'
|
||||
|
||||
export function organizationUserRouteTest() {
|
||||
describe('Organization User Route', () => {
|
||||
const route = '/api/v1/user'
|
||||
|
||||
describe(`GET ${route}/test successful without user status`, () => {
|
||||
const statusCode = StatusCodes.OK
|
||||
const message = 'Hello World'
|
||||
|
||||
it(`should return a ${statusCode} status and message of ${message}`, async () => {
|
||||
await supertest(getRunningExpressApp().app)
|
||||
.get(`${route + '/test'}`)
|
||||
.expect(statusCode)
|
||||
.then((response) => {
|
||||
const body = response.body
|
||||
expect(body.message).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`POST ${route}/test successful without user status`, () => {
|
||||
const statusCode = StatusCodes.OK
|
||||
const message = 'Hello World'
|
||||
|
||||
it(`should return a ${statusCode} status and message of ${message}`, async () => {
|
||||
await supertest(getRunningExpressApp().app)
|
||||
.get(`${route + '/test'}`)
|
||||
.expect(statusCode)
|
||||
.then((response) => {
|
||||
const body = response.body
|
||||
expect(body.message).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import supertest from 'supertest'
|
||||
import { getRunningExpressApp } from '../../../src/utils/getRunningExpressApp'
|
||||
|
||||
export function userRouteTest() {
|
||||
describe('User Route', () => {
|
||||
const route = '/api/v1/user'
|
||||
|
||||
describe(`GET ${route}/test successful without user status`, () => {
|
||||
const statusCode = StatusCodes.OK
|
||||
const message = 'Hello World'
|
||||
|
||||
it(`should return a ${statusCode} status and message of ${message}`, async () => {
|
||||
await supertest(getRunningExpressApp().app)
|
||||
.get(`${route + '/test'}`)
|
||||
.expect(statusCode)
|
||||
.then((response) => {
|
||||
const body = response.body
|
||||
expect(body.message).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`POST ${route}/test successful without user status`, () => {
|
||||
const statusCode = StatusCodes.OK
|
||||
const message = 'Hello World'
|
||||
|
||||
it(`should return a ${statusCode} status and message of ${message}`, async () => {
|
||||
await supertest(getRunningExpressApp().app)
|
||||
.get(`${route + '/test'}`)
|
||||
.expect(statusCode)
|
||||
.then((response) => {
|
||||
const body = response.body
|
||||
expect(body.message).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`PUT ${route}/test successful without user status`, () => {
|
||||
const statusCode = StatusCodes.OK
|
||||
const message = 'Hello World'
|
||||
|
||||
it(`should return a ${statusCode} status and message of ${message}`, async () => {
|
||||
await supertest(getRunningExpressApp().app)
|
||||
.get(`${route + '/test'}`)
|
||||
.expect(statusCode)
|
||||
.then((response) => {
|
||||
const body = response.body
|
||||
expect(body.message).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { generateAPIKey } from '../../src/utils/apiKey'
|
||||
|
||||
export function apiKeyTest() {
|
||||
describe('Api Key', () => {
|
||||
it('should be able to generate a new api key', () => {
|
||||
const apiKey = generateAPIKey()
|
||||
expect(typeof apiKey === 'string').toEqual(true)
|
||||
})
|
||||
describe('Api Key', () => {
|
||||
it('should be able to generate a new api key', () => {
|
||||
const apiKey = generateAPIKey()
|
||||
expect(typeof apiKey === 'string').toEqual(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,145 +16,141 @@ const makeNode = (inputs: Record<string, unknown>, inputParams: object[], extra:
|
||||
}
|
||||
})
|
||||
|
||||
export function sanitizeFlowDataTest() {
|
||||
describe('sanitizeFlowDataForPublicEndpoint', () => {
|
||||
it('strips password-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ apiKey: 'sk-secret', model: 'gpt-4' }, [
|
||||
{ name: 'apiKey', type: 'password' },
|
||||
{ name: 'model', type: 'string' }
|
||||
])
|
||||
describe('sanitizeFlowDataForPublicEndpoint', () => {
|
||||
it('strips password-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ apiKey: 'sk-secret', model: 'gpt-4' }, [
|
||||
{ name: 'apiKey', type: 'password' },
|
||||
{ name: 'model', type: 'string' }
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('apiKey')
|
||||
expect(result.nodes[0].data.inputs.model).toBe('gpt-4')
|
||||
})
|
||||
|
||||
it('strips file-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ filePath: '/data/secret.pdf', label: 'loader' }, [
|
||||
{ name: 'filePath', type: 'file' },
|
||||
{ name: 'label', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('filePath')
|
||||
expect(result.nodes[0].data.inputs.label).toBe('loader')
|
||||
})
|
||||
|
||||
it('strips folder-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ folderPath: '/home/user/docs', name: 'ingest' }, [
|
||||
{ name: 'folderPath', type: 'folder' },
|
||||
{ name: 'name', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('folderPath')
|
||||
expect(result.nodes[0].data.inputs.name).toBe('ingest')
|
||||
})
|
||||
|
||||
it('removes credential field from node data', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ model: 'gpt-4' }, [{ name: 'model', type: 'string' }], { credential: 'cred-uuid-123' })
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data).not.toHaveProperty('credential')
|
||||
})
|
||||
|
||||
it('removes Authorization header from headers input, preserves other headers', () => {
|
||||
const headers = JSON.stringify({ Authorization: 'Bearer secret-token', 'Content-Type': 'application/json' })
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ headers, url: 'https://example.com' }, [
|
||||
{ name: 'headers', type: 'json' },
|
||||
{ name: 'url', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers)
|
||||
expect(sanitizedHeaders).not.toHaveProperty('Authorization')
|
||||
expect(sanitizedHeaders['Content-Type']).toBe('application/json')
|
||||
})
|
||||
|
||||
it('removes x-api-key header case-insensitively', () => {
|
||||
const headers = JSON.stringify({ 'X-API-Key': 'my-key', Accept: 'application/json' })
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'json' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers)
|
||||
expect(sanitizedHeaders).not.toHaveProperty('X-API-Key')
|
||||
expect(sanitizedHeaders.Accept).toBe('application/json')
|
||||
})
|
||||
|
||||
it('removes Authorization from array-format headers (HTTP agentflow node)', () => {
|
||||
const headers = [
|
||||
{ key: 'Authorization', value: 'Bearer secret-token' },
|
||||
{ key: 'Content-Type', value: 'application/json' }
|
||||
]
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = result.nodes[0].data.inputs.headers
|
||||
expect(sanitizedHeaders).toEqual([{ key: 'Content-Type', value: 'application/json' }])
|
||||
})
|
||||
|
||||
it('removes x-api-key case-insensitively from array-format headers', () => {
|
||||
const headers = [
|
||||
{ key: 'X-API-Key', value: 'secret' },
|
||||
{ key: 'Accept', value: 'application/json' }
|
||||
]
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = result.nodes[0].data.inputs.headers
|
||||
expect(sanitizedHeaders).toEqual([{ key: 'Accept', value: 'application/json' }])
|
||||
})
|
||||
|
||||
it('preserves non-sensitive string inputs unchanged', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ temperature: '0.7', maxTokens: '1024' }, [
|
||||
{ name: 'temperature', type: 'string' },
|
||||
{ name: 'maxTokens', type: 'number' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs.temperature).toBe('0.7')
|
||||
expect(result.nodes[0].data.inputs.maxTokens).toBe('1024')
|
||||
})
|
||||
|
||||
it('preserves startAgentflow node inputs used by the embed widget', () => {
|
||||
const formInputTypes = [{ name: 'email', type: 'string', label: 'Email' }]
|
||||
const flowData = makeFlowData([
|
||||
makeNode(
|
||||
{ startInputType: 'formInput', formTitle: 'Contact Us', formDescription: 'Fill out the form', formInputTypes },
|
||||
[
|
||||
{ name: 'startInputType', type: 'options' },
|
||||
{ name: 'formTitle', type: 'string' },
|
||||
{ name: 'formDescription', type: 'string' },
|
||||
{ name: 'formInputTypes', type: 'datagrid' }
|
||||
],
|
||||
{ name: 'startAgentflow' }
|
||||
)
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const inputs = result.nodes[0].data.inputs
|
||||
expect(inputs.startInputType).toBe('formInput')
|
||||
expect(inputs.formTitle).toBe('Contact Us')
|
||||
expect(inputs.formDescription).toBe('Fill out the form')
|
||||
expect(inputs.formInputTypes).toEqual(formInputTypes)
|
||||
})
|
||||
|
||||
it('returns empty nodes/edges structure for malformed flowData', () => {
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint('not-valid-json'))
|
||||
expect(result).toEqual({ nodes: [], edges: [] })
|
||||
})
|
||||
|
||||
it('returns the original string unchanged when flowDataString is empty', () => {
|
||||
expect(sanitizeFlowDataForPublicEndpoint('')).toBe('')
|
||||
})
|
||||
|
||||
it('does not crash when a node has no inputParams', () => {
|
||||
const flowData = makeFlowData([{ id: 'n0', type: 'x', data: { inputs: { foo: 'bar' } } }])
|
||||
expect(() => sanitizeFlowDataForPublicEndpoint(flowData)).not.toThrow()
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs.foo).toBe('bar')
|
||||
})
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('apiKey')
|
||||
expect(result.nodes[0].data.inputs.model).toBe('gpt-4')
|
||||
})
|
||||
}
|
||||
|
||||
it('strips file-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ filePath: '/data/secret.pdf', label: 'loader' }, [
|
||||
{ name: 'filePath', type: 'file' },
|
||||
{ name: 'label', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('filePath')
|
||||
expect(result.nodes[0].data.inputs.label).toBe('loader')
|
||||
})
|
||||
|
||||
it('strips folder-type inputs', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ folderPath: '/home/user/docs', name: 'ingest' }, [
|
||||
{ name: 'folderPath', type: 'folder' },
|
||||
{ name: 'name', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs).not.toHaveProperty('folderPath')
|
||||
expect(result.nodes[0].data.inputs.name).toBe('ingest')
|
||||
})
|
||||
|
||||
it('removes credential field from node data', () => {
|
||||
const flowData = makeFlowData([makeNode({ model: 'gpt-4' }, [{ name: 'model', type: 'string' }], { credential: 'cred-uuid-123' })])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data).not.toHaveProperty('credential')
|
||||
})
|
||||
|
||||
it('removes Authorization header from headers input, preserves other headers', () => {
|
||||
const headers = JSON.stringify({ Authorization: 'Bearer secret-token', 'Content-Type': 'application/json' })
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ headers, url: 'https://example.com' }, [
|
||||
{ name: 'headers', type: 'json' },
|
||||
{ name: 'url', type: 'string' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers)
|
||||
expect(sanitizedHeaders).not.toHaveProperty('Authorization')
|
||||
expect(sanitizedHeaders['Content-Type']).toBe('application/json')
|
||||
})
|
||||
|
||||
it('removes x-api-key header case-insensitively', () => {
|
||||
const headers = JSON.stringify({ 'X-API-Key': 'my-key', Accept: 'application/json' })
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'json' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = JSON.parse(result.nodes[0].data.inputs.headers)
|
||||
expect(sanitizedHeaders).not.toHaveProperty('X-API-Key')
|
||||
expect(sanitizedHeaders.Accept).toBe('application/json')
|
||||
})
|
||||
|
||||
it('removes Authorization from array-format headers (HTTP agentflow node)', () => {
|
||||
const headers = [
|
||||
{ key: 'Authorization', value: 'Bearer secret-token' },
|
||||
{ key: 'Content-Type', value: 'application/json' }
|
||||
]
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = result.nodes[0].data.inputs.headers
|
||||
expect(sanitizedHeaders).toEqual([{ key: 'Content-Type', value: 'application/json' }])
|
||||
})
|
||||
|
||||
it('removes x-api-key case-insensitively from array-format headers', () => {
|
||||
const headers = [
|
||||
{ key: 'X-API-Key', value: 'secret' },
|
||||
{ key: 'Accept', value: 'application/json' }
|
||||
]
|
||||
const flowData = makeFlowData([makeNode({ headers }, [{ name: 'headers', type: 'array' }])])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const sanitizedHeaders = result.nodes[0].data.inputs.headers
|
||||
expect(sanitizedHeaders).toEqual([{ key: 'Accept', value: 'application/json' }])
|
||||
})
|
||||
|
||||
it('preserves non-sensitive string inputs unchanged', () => {
|
||||
const flowData = makeFlowData([
|
||||
makeNode({ temperature: '0.7', maxTokens: '1024' }, [
|
||||
{ name: 'temperature', type: 'string' },
|
||||
{ name: 'maxTokens', type: 'number' }
|
||||
])
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs.temperature).toBe('0.7')
|
||||
expect(result.nodes[0].data.inputs.maxTokens).toBe('1024')
|
||||
})
|
||||
|
||||
it('preserves startAgentflow node inputs used by the embed widget', () => {
|
||||
const formInputTypes = [{ name: 'email', type: 'string', label: 'Email' }]
|
||||
const flowData = makeFlowData([
|
||||
makeNode(
|
||||
{ startInputType: 'formInput', formTitle: 'Contact Us', formDescription: 'Fill out the form', formInputTypes },
|
||||
[
|
||||
{ name: 'startInputType', type: 'options' },
|
||||
{ name: 'formTitle', type: 'string' },
|
||||
{ name: 'formDescription', type: 'string' },
|
||||
{ name: 'formInputTypes', type: 'datagrid' }
|
||||
],
|
||||
{ name: 'startAgentflow' }
|
||||
)
|
||||
])
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
const inputs = result.nodes[0].data.inputs
|
||||
expect(inputs.startInputType).toBe('formInput')
|
||||
expect(inputs.formTitle).toBe('Contact Us')
|
||||
expect(inputs.formDescription).toBe('Fill out the form')
|
||||
expect(inputs.formInputTypes).toEqual(formInputTypes)
|
||||
})
|
||||
|
||||
it('returns empty nodes/edges structure for malformed flowData', () => {
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint('not-valid-json'))
|
||||
expect(result).toEqual({ nodes: [], edges: [] })
|
||||
})
|
||||
|
||||
it('returns the original string unchanged when flowDataString is empty', () => {
|
||||
expect(sanitizeFlowDataForPublicEndpoint('')).toBe('')
|
||||
})
|
||||
|
||||
it('does not crash when a node has no inputParams', () => {
|
||||
const flowData = makeFlowData([{ id: 'n0', type: 'x', data: { inputs: { foo: 'bar' } } }])
|
||||
expect(() => sanitizeFlowDataForPublicEndpoint(flowData)).not.toThrow()
|
||||
const result = JSON.parse(sanitizeFlowDataForPublicEndpoint(flowData))
|
||||
expect(result.nodes[0].data.inputs.foo).toBe('bar')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user