From 049d59186ec1616b5a9706713a15bdd41a351879 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 5 Mar 2026 16:22:30 +0800 Subject: [PATCH 1/2] fix(server): restructure Jest tests and standardize test scripts --- packages/server/package.json | 4 +- packages/server/test/index.test.ts | 30 --- .../routes/v1/organization-user.route.test.ts | 39 --- .../server/test/routes/v1/user.route.test.ts | 54 ---- .../server/test/utils/api-key.util.test.ts | 12 +- .../test/utils/sanitizeFlowData.test.ts | 244 +++++++++--------- 6 files changed, 128 insertions(+), 255 deletions(-) delete mode 100644 packages/server/test/index.test.ts delete mode 100644 packages/server/test/routes/v1/organization-user.route.test.ts delete mode 100644 packages/server/test/routes/v1/user.route.test.ts diff --git a/packages/server/package.json b/packages/server/package.json index e66afc208e6..3ea7a993537 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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", diff --git a/packages/server/test/index.test.ts b/packages/server/test/index.test.ts deleted file mode 100644 index 04778fe2a80..00000000000 --- a/packages/server/test/index.test.ts +++ /dev/null @@ -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() -}) diff --git a/packages/server/test/routes/v1/organization-user.route.test.ts b/packages/server/test/routes/v1/organization-user.route.test.ts deleted file mode 100644 index d143c2e547a..00000000000 --- a/packages/server/test/routes/v1/organization-user.route.test.ts +++ /dev/null @@ -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) - }) - }) - }) - }) -} diff --git a/packages/server/test/routes/v1/user.route.test.ts b/packages/server/test/routes/v1/user.route.test.ts deleted file mode 100644 index cdab9d99558..00000000000 --- a/packages/server/test/routes/v1/user.route.test.ts +++ /dev/null @@ -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) - }) - }) - }) - }) -} diff --git a/packages/server/test/utils/api-key.util.test.ts b/packages/server/test/utils/api-key.util.test.ts index ccf1bfd5a1a..0606067ab72 100644 --- a/packages/server/test/utils/api-key.util.test.ts +++ b/packages/server/test/utils/api-key.util.test.ts @@ -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) }) -} +}) diff --git a/packages/server/test/utils/sanitizeFlowData.test.ts b/packages/server/test/utils/sanitizeFlowData.test.ts index fd1a6a83341..d7b3cb7d8a5 100644 --- a/packages/server/test/utils/sanitizeFlowData.test.ts +++ b/packages/server/test/utils/sanitizeFlowData.test.ts @@ -16,145 +16,141 @@ const makeNode = (inputs: Record, 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') - }) + ]) + 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' } - ]) + 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') - }) + ]) + 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' } - ]) + 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') - }) + ]) + 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 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' } - ]) + 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') - }) + ]) + 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 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 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('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' } - ]) + 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') - }) + ]) + 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('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 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('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') - }) + 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') }) -} +}) From 81198b06b4df154e26a0430901e9fd2821e896ee Mon Sep 17 00:00:00 2001 From: yau-wd Date: Thu, 5 Mar 2026 16:50:21 +0800 Subject: [PATCH 2/2] chore(main.yml): run build before test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93e13b50d6d..64eea0f8a1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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)