@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
|
@@ -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
|
+
})
|