@quatrain/api-server-express 1.1.12 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatrain/api-server-express",
3
- "version": "1.1.12",
3
+ "version": "1.1.13",
4
4
  "description": "Express Adapter for Quatrain API Server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,312 @@
1
+ import express from 'express'
2
+ import { ExpressAdapter } from './ExpressAdapter'
3
+ import { ApiRequest, ApiResponse } from '@quatrain/api'
4
+
5
+ describe('ExpressAdapter', () => {
6
+ let app: express.Application
7
+ let adapter: ExpressAdapter
8
+
9
+ beforeEach(() => {
10
+ app = express()
11
+ adapter = new ExpressAdapter(app)
12
+ })
13
+
14
+ describe('Constructor and Default Middlewares', () => {
15
+ it('should instantiate and configure default settings', () => {
16
+ const mockApp = {
17
+ disable: jest.fn(),
18
+ use: jest.fn(),
19
+ listen: jest.fn()
20
+ }
21
+ const ad = new ExpressAdapter(mockApp as any)
22
+ expect(mockApp.disable).toHaveBeenCalledWith('x-powered-by')
23
+ expect(mockApp.use).toHaveBeenCalled()
24
+ expect(ad.getNativeInstance()).toBe(mockApp)
25
+ })
26
+
27
+ it('should correctly configure default CORS headers and handle OPTIONS request', async () => {
28
+ let corsHandler: express.RequestHandler | null = null
29
+ const mockApp = {
30
+ disable: jest.fn(),
31
+ use: jest.fn((middleware) => {
32
+ if (typeof middleware === 'function' && middleware.length === 3) {
33
+ corsHandler = middleware
34
+ }
35
+ }),
36
+ listen: jest.fn()
37
+ }
38
+ new ExpressAdapter(mockApp as any)
39
+
40
+ expect(corsHandler).not.toBeNull()
41
+
42
+ const mockReq = { method: 'OPTIONS' } as express.Request
43
+ const headersSent: Record<string, string> = {}
44
+ let statusSent: number | null = null
45
+
46
+ const mockRes = {
47
+ header: (name: string, value: string) => {
48
+ headersSent[name] = value
49
+ },
50
+ sendStatus: (code: number) => {
51
+ statusSent = code
52
+ }
53
+ } as any
54
+
55
+ const mockNext = jest.fn()
56
+
57
+ if (corsHandler) {
58
+ (corsHandler as express.RequestHandler)(mockReq, mockRes, mockNext)
59
+ }
60
+
61
+ expect(headersSent['Access-Control-Allow-Origin']).toBe('*')
62
+ expect(headersSent['Access-Control-Allow-Methods']).toBe('GET, PUT, POST, DELETE, OPTIONS')
63
+ expect(headersSent['Access-Control-Allow-Headers']).toBe('*')
64
+ expect(statusSent).toBe(200)
65
+ expect(mockNext).not.toHaveBeenCalled()
66
+ })
67
+
68
+ it('should let non-OPTIONS requests pass through default CORS middleware', () => {
69
+ let corsHandler: express.RequestHandler | null = null
70
+ const mockApp = {
71
+ disable: jest.fn(),
72
+ use: jest.fn((middleware) => {
73
+ if (typeof middleware === 'function' && middleware.length === 3) {
74
+ corsHandler = middleware
75
+ }
76
+ }),
77
+ listen: jest.fn()
78
+ }
79
+ new ExpressAdapter(mockApp as any)
80
+
81
+ const mockReq = { method: 'GET' } as express.Request
82
+ const mockRes = { header: jest.fn(), sendStatus: jest.fn() } as any
83
+ const mockNext = jest.fn()
84
+
85
+ if (corsHandler) {
86
+ (corsHandler as express.RequestHandler)(mockReq, mockRes, mockNext)
87
+ }
88
+
89
+ expect(mockNext).toHaveBeenCalled()
90
+ })
91
+ })
92
+
93
+ describe('HTTP Verbs Registration', () => {
94
+ it('should register routes with express routes mapping', () => {
95
+ const mockRouter = {
96
+ get: jest.fn(),
97
+ post: jest.fn(),
98
+ put: jest.fn(),
99
+ delete: jest.fn()
100
+ }
101
+ const ad = new ExpressAdapter(mockRouter as any)
102
+
103
+ const handler = async (req: ApiRequest, res: ApiResponse) => {}
104
+
105
+ ad.get('/test', handler)
106
+ ad.post('/test', handler)
107
+ ad.put('/test', handler)
108
+ ad.delete('/test', handler)
109
+
110
+ expect(mockRouter.get).toHaveBeenCalledWith('/test', expect.any(Function))
111
+ expect(mockRouter.post).toHaveBeenCalledWith('/test', expect.any(Function))
112
+ expect(mockRouter.put).toHaveBeenCalledWith('/test', expect.any(Function))
113
+ expect(mockRouter.delete).toHaveBeenCalledWith('/test', expect.any(Function))
114
+ })
115
+
116
+ it('should map Request and Response objects correctly', async () => {
117
+ const mockRouter = {
118
+ get: jest.fn((path, wrapped) => {
119
+ const req = {
120
+ body: { key: 'value' },
121
+ params: { id: '123' },
122
+ query: { search: 'text' },
123
+ headers: { authorization: 'Bearer token' }
124
+ }
125
+ const res = {
126
+ status: jest.fn().mockReturnThis(),
127
+ json: jest.fn(),
128
+ send: jest.fn(),
129
+ setHeader: jest.fn(),
130
+ write: jest.fn(),
131
+ end: jest.fn()
132
+ }
133
+ const next = jest.fn()
134
+ wrapped(req, res, next)
135
+ })
136
+ }
137
+
138
+ const ad = new ExpressAdapter(mockRouter as any)
139
+ let capturedReq: ApiRequest | null = null
140
+ let capturedRes: ApiResponse | null = null
141
+
142
+ ad.get('/test', async (req, res) => {
143
+ capturedReq = req
144
+ capturedRes = res
145
+ res.status(201).json({ success: true })
146
+ res.send('Done')
147
+ res.setHeader('X-Custom', 'Value')
148
+ res.write('Chunk')
149
+ res.end()
150
+ })
151
+
152
+ // Force async block inside ExpressAdapter wrapped callback to run
153
+ await new Promise((resolve) => setTimeout(resolve, 10))
154
+
155
+ expect(capturedReq).not.toBeNull()
156
+ expect(capturedReq?.body).toEqual({ key: 'value' })
157
+ expect(capturedReq?.params).toEqual({ id: '123' })
158
+ expect(capturedReq?.query).toEqual({ search: 'text' })
159
+ expect(capturedReq?.headers?.authorization).toBe('Bearer token')
160
+
161
+ expect(capturedRes).not.toBeNull()
162
+ })
163
+ })
164
+
165
+ describe('Middleware Routing and Mounting', () => {
166
+ it('should support native Express middleware attach via use', () => {
167
+ const mockRouter = { use: jest.fn() }
168
+ const ad = new ExpressAdapter(mockRouter as any)
169
+ const dummyMiddleware = () => {}
170
+
171
+ ad.use(dummyMiddleware)
172
+ expect(mockRouter.use).toHaveBeenCalledWith(dummyMiddleware)
173
+ })
174
+
175
+ it('should add Quatrain-compatible API middleware', async () => {
176
+ let middlewareHandler: express.RequestHandler | null = null
177
+ const mockRouter = {
178
+ use: jest.fn((mw) => {
179
+ middlewareHandler = mw
180
+ })
181
+ }
182
+ const ad = new ExpressAdapter(mockRouter as any)
183
+
184
+ const apiMiddleware = jest.fn().mockResolvedValue(true)
185
+ ad.addMiddleware(apiMiddleware)
186
+
187
+ expect(middlewareHandler).not.toBeNull()
188
+
189
+ const mockReq = { headers: {} } as express.Request
190
+ const mockRes = {} as express.Response
191
+ const mockNext = jest.fn()
192
+
193
+ if (middlewareHandler) {
194
+ (middlewareHandler as express.RequestHandler)(mockReq, mockRes, mockNext)
195
+ }
196
+
197
+ await new Promise((resolve) => setTimeout(resolve, 10))
198
+
199
+ expect(apiMiddleware).toHaveBeenCalled()
200
+ expect(mockNext).toHaveBeenCalled()
201
+ })
202
+
203
+ it('should stop propagation if API middleware returns false', async () => {
204
+ let middlewareHandler: express.RequestHandler | null = null
205
+ const mockRouter = {
206
+ use: jest.fn((mw) => {
207
+ middlewareHandler = mw
208
+ })
209
+ }
210
+ const ad = new ExpressAdapter(mockRouter as any)
211
+
212
+ const apiMiddleware = jest.fn().mockResolvedValue(false)
213
+ ad.addMiddleware(apiMiddleware)
214
+
215
+ const mockReq = { headers: {} } as express.Request
216
+ const mockRes = {} as express.Response
217
+ const mockNext = jest.fn()
218
+
219
+ if (middlewareHandler) {
220
+ (middlewareHandler as express.RequestHandler)(mockReq, mockRes, mockNext)
221
+ }
222
+
223
+ await new Promise((resolve) => setTimeout(resolve, 10))
224
+
225
+ expect(apiMiddleware).toHaveBeenCalled()
226
+ expect(mockNext).not.toHaveBeenCalled()
227
+ })
228
+ })
229
+
230
+ describe('Subrouters and Endpoints block', () => {
231
+ it('should spawn subrouters correctly', () => {
232
+ const mockRouter = { use: jest.fn() }
233
+ const ad = new ExpressAdapter(mockRouter as any)
234
+
235
+ const sub = ad.createRouter('/sub')
236
+ expect(sub).toBeInstanceOf(ExpressAdapter)
237
+ expect(mockRouter.use).toHaveBeenCalledWith('/sub', expect.any(Function))
238
+ })
239
+
240
+ it('should mount composite endpoints using options and prefix', () => {
241
+ const mockRouter = { use: jest.fn() }
242
+ const ad = new ExpressAdapter(mockRouter as any, { apiPrefix: '/v1' })
243
+
244
+ const endpointHandler = jest.fn()
245
+ const mockMiddleware = () => {}
246
+
247
+ ad.addEndpoint(endpointHandler, '/items', {
248
+ middlewares: [mockMiddleware]
249
+ })
250
+
251
+ expect(mockRouter.use).toHaveBeenCalledWith('/v1/items', expect.any(Function))
252
+ expect(endpointHandler).toHaveBeenCalled()
253
+ })
254
+ })
255
+
256
+ describe('SPA Static Files and start mechanisms', () => {
257
+ it('should setup static assets and wildcard redirects for SPA paths', () => {
258
+ const mockRouter = {
259
+ use: jest.fn(),
260
+ get: jest.fn()
261
+ }
262
+ const ad = new ExpressAdapter(mockRouter as any)
263
+
264
+ ad.serveStatic('/dist', '/api')
265
+
266
+ expect(mockRouter.use).toHaveBeenCalled()
267
+ expect(mockRouter.get).toHaveBeenCalledWith('*', expect.any(Function))
268
+ })
269
+
270
+ it('should pass API request calls through sendFile redirects', () => {
271
+ let fallbackHandler: express.RequestHandler | null = null
272
+ const mockRouter = {
273
+ use: jest.fn(),
274
+ get: jest.fn((path, handler) => {
275
+ if (path === '*') fallbackHandler = handler
276
+ })
277
+ }
278
+ const ad = new ExpressAdapter(mockRouter as any)
279
+ ad.serveStatic('/dist', '/api')
280
+
281
+ expect(fallbackHandler).not.toBeNull()
282
+
283
+ const mockReq = { path: '/api/v1/items' } as express.Request
284
+ const mockRes = {} as express.Response
285
+ const mockNext = jest.fn()
286
+
287
+ if (fallbackHandler) {
288
+ (fallbackHandler as express.RequestHandler)(mockReq, mockRes, mockNext)
289
+ }
290
+
291
+ expect(mockNext).toHaveBeenCalled()
292
+ })
293
+
294
+ it('should boot and start standard web networks', () => {
295
+ const mockApp = {
296
+ disable: jest.fn(),
297
+ use: jest.fn(),
298
+ listen: jest.fn()
299
+ }
300
+ const ad = new ExpressAdapter(mockApp as any)
301
+ ad.start(4001)
302
+
303
+ expect(mockApp.listen).toHaveBeenCalledWith(4001, undefined)
304
+ })
305
+
306
+ it('should fail starting routing nodes directly', () => {
307
+ const router = express.Router()
308
+ const ad = new ExpressAdapter(router)
309
+ expect(() => ad.start(4001)).toThrow("Cannot start a server on a Router instance.")
310
+ })
311
+ })
312
+ })