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:
yau-wd
2026-03-05 18:18:18 +08:00
committed by GitHub
parent d8da27c0fb
commit 84758d794d
7 changed files with 145 additions and 272 deletions
+1 -1
View File
@@ -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)
+3 -1
View File
@@ -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",
-30
View File
@@ -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')
})
})