import { jest } from '@jest/globals' import { expect as c_expect } from 'chai' import request from 'supertest' import { z } from 'zod' import { HttpMethod } from '../../../src/API/Request.js' import Response from '../../../src/API/Response.js' import Globals from '../../../src/Globals.js' import ContainerServer from '../../../src/Server/lib/ContainerServer.js' import { defaultUrl } from '../../Test.utils.js' export const ViewSchema = z.object({ id: z.string(), content: z.string(), createdAt: z.string(), updatedAt: z.string(), }) export const QuerySchema = z.object({ id: z.string(), order: z.string().refine( v => { const n = Number(v) return !isNaN(n) && v?.length > 0 }, { message: 'Invalid number' } ), }) describe('Container server routing', () => { // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) }) afterAll(() => { mockExit.mockRestore() }) beforeEach(() => { mockExit.mockReset() }) test('Health & 404 handler', async () => { const server = new ContainerServer({ routes: [] }) await server.start() // Health check const res = await request(defaultUrl) .get(Globals.Listener_HTTP_DefaultHealthCheckRoute) .expect('Content-Type', 'text/html; charset=utf-8') .expect(200) c_expect(res.text).to.be.equals('Healthy!') // Not handled await request(defaultUrl) .get(`/abc`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(404) // Unload await server.stop('Error') expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toBeCalledWith(1) }) test('Simple route', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.GET, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Health check const res = await request(defaultUrl) .get(Globals.Listener_HTTP_DefaultHealthCheckRoute) .expect('Content-Type', 'text/html; charset=utf-8') .expect(200) c_expect(res.text).to.be.equals('Healthy!') // Found route const resG = await request(defaultUrl) .get(`/abc`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(200) c_expect(resG.body['name']).to.be.deep.equals('abc') c_expect(resG.body['transactionID']).to.not.be.null // Found route, but not method await request(defaultUrl) .post(`/abc`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(404) // Not found route await request(defaultUrl) .get(`/abc/123`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(404) // Unload await server.stop('Error') expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toBeCalledWith(1) }) }) describe('Container server basics', () => { // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) }) afterAll(() => { mockExit.mockRestore() }) beforeEach(() => { mockExit.mockReset() }) test('Starts with exports', async () => { const server = new ContainerServer({ routes: [] }) server.getExport() return new Promise(resolve => { setTimeout(async () => { // Health check const res = await request(defaultUrl) .get(Globals.Listener_HTTP_DefaultHealthCheckRoute) .expect('Content-Type', 'text/html; charset=utf-8') .expect(200) c_expect(res.text).to.be.equals('Healthy!') // Unload await server.stop() expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toBeCalledWith(0) resolve(null) }, 2000) }) }) test('Stops if process sends message SIGINT', async () => { const server = new ContainerServer({ routes: [] }) await server.start() process.emit('SIGINT') return new Promise(resolve => { setTimeout(async () => { expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toBeCalledWith(0) resolve(null) }, 2000) }) }, 10000) test('Stops if process sends message unhandledRejection', async () => { const server = new ContainerServer({ routes: [] }) await server.start() // @ts-ignore process.emit('unhandledRejection') return new Promise(resolve => { setTimeout(async () => { expect(mockExit).toHaveBeenCalledTimes(1) expect(mockExit).toBeCalledWith(0) resolve(null) }, 2000) }) }, 10000) }) describe('Container server validation (body)', () => { // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) }) afterAll(() => { mockExit.mockRestore() }) beforeEach(() => { mockExit.mockReset() }) function validateValidationFailure(res: any, failureCount?: number) { c_expect(res.body['err']).to.be.equals(Globals.ErrorResponseValidationFail) c_expect(res.body['errCode']).to.be.equals(Globals.ErrorCode_InvalidInput) c_expect(res.body['transactionID']).to.not.be.null c_expect(res.body['transactionID']).to.be.an('string') c_expect(res.body['validationFailure']?.length).to.be.equals(failureCount || 4) } test('Validates empty body', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG) await server.stop() }) test('Validates empty string body', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .send('') .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG) await server.stop() }) test('Validates empty object body', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .send({}) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG) await server.stop() }) test('Validates missing props body', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .send({ id: '123' }) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 3) await server.stop() }) test('Validates wrong type props body', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .send({ id: 123 }) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG) await server.stop() }) test('Validates successfully', async () => { const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, inputSchema: ViewSchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() // Validation fails, empty body await request(defaultUrl) .post(`/abc`) .send({ id: '123', content: '123', createdAt: new Date(), updatedAt: new Date(), }) .expect('Content-Type', 'application/json; charset=utf-8') .expect(200) await server.stop() }) }) describe('Container server validation (query)', () => { let server: ContainerServer // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) }) afterAll(() => { mockExit.mockRestore() }) beforeEach(async () => { mockExit.mockReset() server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, querySchema: QuerySchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() }) afterEach(async () => { await server.stop() }) function validateValidationFailure(res: any, failureCount?: number) { c_expect(res.body['err']).to.be.equals(Globals.ErrorResponseValidationFail) c_expect(res.body['errCode']).to.be.equals(Globals.ErrorCode_InvalidInput) c_expect(res.body['transactionID']).to.not.be.null c_expect(res.body['transactionID']).to.be.an('string') c_expect(res.body['validationFailure']?.length).to.be.equals(failureCount || 2) } test('Validates empty query', async () => { // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 2) }) test('Validates empty query differently', async () => { // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc?`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 2) }) test('Validates missing props body', async () => { // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc?id=myname`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 1) }) test('Validates wrong type props body', async () => { // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc?id=name&order=myname`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 1) }) test('Validates successfully2', async () => { // Validation fails, empty body await request(defaultUrl) .post(`/abc`) .query({ id: 'name', order: 123 }) .expect('Content-Type', 'application/json; charset=utf-8') .expect(200) }) }) describe('Container server validation (path)', () => { let server: ContainerServer // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) }) afterAll(() => { mockExit.mockRestore() }) beforeEach(async () => { mockExit.mockReset() server = new ContainerServer({ routes: [ { path: '/abc/:id/:order', method: HttpMethod.POST, pathSchema: QuerySchema, handler: async () => { return Response.SimpleResponse({ name: 'abc' }) }, } as any, ], }) await server.start() }) afterEach(async () => { await server.stop() }) function validateValidationFailure(res: any, failureCount?: number) { c_expect(res.body['err']).to.be.equals(Globals.ErrorResponseValidationFail) c_expect(res.body['errCode']).to.be.equals(Globals.ErrorCode_InvalidInput) c_expect(res.body['transactionID']).to.not.be.null c_expect(res.body['transactionID']).to.be.an('string') c_expect(res.body['validationFailure']?.length).to.be.equals(failureCount || 2) } test('Validates wrong type props body', async () => { // Validation fails, empty body const resG = await request(defaultUrl) .post(`/abc/name/myname`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(400) validateValidationFailure(resG, 1) }) test('Validates successfully4', async () => { // Validation fails, empty body await request(defaultUrl) .post(`/abc/name/123`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(200) }) }) describe('Container server raw body', () => { // @ts-ignore let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) beforeAll(() => { // @ts-ignore mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) process.env['HYBRIDLESS_RUNTIME'] = 'true' }) afterAll(() => { mockExit.mockRestore() process.env['HYBRIDLESS_RUNTIME'] = undefined }) beforeEach(() => { mockExit.mockReset() }) test('Raw body is available', async () => { let raw = null const server = new ContainerServer({ routes: [ { path: '/abc', method: HttpMethod.POST, handler: async t => { raw = t.request.getBody(true) return Response.SimpleResponse(null) }, } as any, ], }) await server.start() // it's formatted funky, because we're testing for exact match const data = ' { "name" : "abc" }' await request(defaultUrl) .post(`/abc`) .send(data) .set('Content-Type', 'application/json') .expect(200) const s = new TextDecoder('utf-8').decode(raw!) c_expect(s).to.be.equals(data) await server.stop() }) }) export {}