-
-
Notifications
You must be signed in to change notification settings - Fork 137
Expand file tree
/
Copy pathsend.test.ts
More file actions
343 lines (286 loc) · 11.8 KB
/
send.test.ts
File metadata and controls
343 lines (286 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import fs from 'node:fs'
import path from 'node:path'
import { makeFetch } from 'supertest-fetch'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { App } from '../../packages/app/src'
import { json, send, sendFile, sendStatus, status } from '../../packages/send/src'
import { runServer } from '../../test_helpers/runServer'
const __dirname = import.meta.dirname
describe('json(body)', () => {
it('should send a json-stringified reply when an object is passed', async () => {
const app = runServer((_, res) => json(res)({ hello: 'world' }))
await makeFetch(app)('/').expect({ hello: 'world' })
})
it('should set a content-type header properly', async () => {
const app = runServer((_, res) => json(res)({ hello: 'world' }))
await makeFetch(app)('/').expectHeader('content-type', 'application/json')
})
it('should send a null reply when an null is passed', async () => {
const app = runServer((_, res) => json(res)(null))
await makeFetch(app)('/').expect(null)
})
it('should be able to respond with booleans', async () => {
const app = runServer((_, res) => json(res)(true))
await makeFetch(app)('/').expectBody('true')
})
it('should be able to respond with numbers', async () => {
const app = runServer((_, res) => json(res)(123))
await makeFetch(app)('/').expectBody('123')
})
it('should be able to respond with strings', async () => {
const app = runServer((_, res) => json(res)('hello'))
await makeFetch(app)('/').expectBody('hello')
})
})
describe('send(body)', () => {
it('should send a plain text', async () => {
const app = runServer((req, res) => send(req, res)('Hello World'))
await makeFetch(app)('/').expect('Hello World')
})
it('should set HTML content-type header when sending plain text', async () => {
const app = runServer((req, res) => send(req, res)('Hello World'))
await makeFetch(app)('/').expectHeader('Content-Type', 'text/html; charset=utf-8')
})
it('should generate an eTag on a plain text response', async () => {
const app = runServer((req, res) => send(req, res)('Hello World'))
await makeFetch(app)('/').expectHeader('etag', 'W/"b-Ck1VqNd45QIvq3AZd8XYQLvEhtA"')
})
it('should send a JSON response', async () => {
const app = runServer((req, res) => send(req, res)({ hello: 'world' }))
await makeFetch(app)('/').expectHeader('Content-Type', 'application/json').expectBody({ hello: 'world' })
})
it('should send a buffer', async () => {
const app = runServer((req, res) => send(req, res)(Buffer.from('Hello World')))
await makeFetch(app)('/').expect('Hello World')
})
it('should preserve Content-Type when sending buffer with Content-Type already set', async () => {
const app = runServer((req, res) => {
res.setHeader('Content-Type', 'application/custom-binary')
send(req, res)(Buffer.from('binary data'))
})
await makeFetch(app)('/').expectHeader('Content-Type', 'application/custom-binary')
})
it('should handle non-string body that needs toString conversion', async () => {
const app = runServer((req, res) => {
// Set ETag first to avoid createETag being called on non-string
res.setHeader('etag', 'custom-etag')
// Send a number which is not a string and not a buffer/object
send(req, res)(12345 as unknown as string)
})
await makeFetch(app)('/').expect('12345')
})
it('should send nothing on a HEAD request', async () => {
const app = runServer((req, res) => send(req, res)('Hello World'))
await makeFetch(app)('/', {
method: 'HEAD'
}).expectBody('')
})
it('should send nothing if body is empty', async () => {
const app = runServer((req, res) => send(req, res)(null))
await makeFetch(app)('/').expectBody('')
})
it('should remove some headers for 204 status', async () => {
const app = runServer((req, res) => {
res.statusCode = 204
send(req, res)('Hello World')
})
await makeFetch(app)('/')
.expectHeader('Content-Length', null)
.expectHeader('Content-Type', null)
.expectHeader('Transfer-Encoding', null)
})
it('should remove some headers for 304 status', async () => {
const app = runServer((req, res) => {
res.statusCode = 304
send(req, res)('Hello World')
})
await makeFetch(app)('/')
.expectHeader('Content-Length', null)
.expectHeader('Content-Type', null)
.expectHeader('Transfer-Encoding', null)
})
it("should set Content-Type to application/octet-stream for buffers if the header hasn't been set before", async () => {
const app = runServer((req, res) => send(req, res)(Buffer.from('Hello World', 'utf-8')).end())
await makeFetch(app)('/').expectHeader('Content-Type', 'application/octet-stream')
})
it('should set 304 status for fresh requests', async () => {
const etag = 'abc'
const app = new App()
const server = app.listen()
app.use((_req, res) => {
const str = Array(1000).join('-')
res.set('ETag', etag).send(str)
})
await makeFetch(server)('/', {
headers: {
'If-None-Match': etag,
'Cache-Control': 'max-age=3600'
}
}).expectStatus(304)
})
})
describe('status(status)', () => {
it('sets response status', async () => {
const app = runServer((_, res) => status(res)(418).end())
await makeFetch(app)('/').expectStatus(418)
})
it('supports nesting', async () => {
const app = runServer((_, res) => {
const r = status(res)(418)
expect(r).toBe(res)
r.end()
})
await makeFetch(app)('/').expectStatus(418)
})
})
describe('sendStatus(status)', () => {
it(`should send "I'm a teapot" when argument is 418`, async () => {
const app = runServer((req, res) => sendStatus(req, res)(418).end())
await makeFetch(app)('/').expect("I'm a Teapot")
})
it('should send stringified status code for unsupported status codes', async () => {
const app = runServer((req, res) => sendStatus(req, res)(999).end())
await makeFetch(app)('/').expect(999, '999')
})
})
describe('sendFile(path)', () => {
const testFilePath = path.resolve(__dirname, 'test.txt')
beforeAll(() => {
fs.writeFileSync(testFilePath, 'Hello World')
})
afterAll(() => {
fs.unlinkSync(testFilePath)
})
it('should send the file', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, {}))
await makeFetch(app)('/').expect('Hello World')
})
it('should throw if path is not absolute', async () => {
const app = runServer(async (req, res) => {
try {
sendFile(req, res)('../relative/path', {})
} catch (err) {
expect(err.message).toMatch(/absolute/)
res.end()
return
}
throw new Error('Did not throw an error')
})
await makeFetch(app)('/')
})
it('should set the Content-Type header based on the filename', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, {}))
await makeFetch(app)('/').expectHeader('Content-Type', 'text/plain; charset=utf-8')
})
it('should inherit the previously set Content-Type header', async () => {
const app = runServer((req, res) => {
res.setHeader('Content-Type', 'text/markdown')
sendFile(req, res)(testFilePath, {})
})
await makeFetch(app)('/').expectHeader('Content-Type', 'text/markdown')
})
it('should allow custom headers through the options param', async () => {
const HEADER_NAME = 'Test-Header'
const HEADER_VALUE = 'Hello World'
const app = runServer((req, res) => sendFile(req, res)(testFilePath, { headers: { [HEADER_NAME]: HEADER_VALUE } }))
await makeFetch(app)('/').expectHeader(HEADER_NAME, HEADER_VALUE)
})
it('should support Range header', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath))
await makeFetch(app)('/', {
headers: {
Range: 'bytes=0-4'
}
})
.expectStatus(206)
.expect('Content-Length', '5')
.expect('Accept-Ranges', 'bytes')
.expect('Hello')
})
it('should send 419 if out of range', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath))
await makeFetch(app)('/', {
headers: {
Range: 'bytes=0-666'
}
})
.expectStatus(416)
.expectHeader('Content-Range', 'bytes */11')
})
it('should set default encoding to UTF-8', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath))
await makeFetch(app)('/').expectStatus(200).expectHeader('Content-Encoding', 'utf-8')
})
it('should inherit the previously set status code', async () => {
const app = runServer((req, res) => {
res.statusCode = 418
sendFile(req, res)(testFilePath)
})
await makeFetch(app)('/').expectStatus(418)
})
it('should default to status 200 when statusCode is falsy', async () => {
const app = runServer((req, res) => {
res.statusCode = 0
sendFile(req, res)(testFilePath)
})
await makeFetch(app)('/').expectStatus(200)
})
it('should enable cache headers', async () => {
const app = runServer((req, res) =>
sendFile(req, res)(testFilePath, { caching: { maxAge: 4000, immutable: true } })
)
await makeFetch(app)('/').expectHeader('Cache-Control', 'public,max-age=4000,immutable')
})
it('should mark cache is "must-revalidate" if maxAge is 0', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, { caching: { maxAge: 0 } }))
await makeFetch(app)('/').expectHeader('Cache-Control', 'public,max-age=0,must-revalidate')
})
it('should set cache control with maxAge=0 and immutable=false', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, { caching: { maxAge: 0, immutable: false } }))
await makeFetch(app)('/').expectHeader('Cache-Control', 'public,max-age=0,must-revalidate')
})
it('should handle Range header with only start position', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath))
// Range with only start, no end (bytes=5-)
await makeFetch(app)('/', {
headers: {
Range: 'bytes=5-'
}
})
.expectStatus(206)
.expect('Content-Range', 'bytes 5-10/11')
.expect(' World')
})
it('should set cache header with just maxAge when not immutable and not 0', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, { caching: { maxAge: 3600 } }))
await makeFetch(app)('/').expectHeader('Cache-Control', 'public,max-age=3600')
})
it('should not set Cache-Control header when no caching options provided', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, {}))
const response = await makeFetch(app)('/')
expect(response.headers.get('Cache-Control')).toBeNull()
})
it('should not set Cache-Control header when caching is empty object', async () => {
const app = runServer((req, res) => sendFile(req, res)(testFilePath, { caching: {} }))
const response = await makeFetch(app)('/')
expect(response.headers.get('Cache-Control')).toBeNull()
})
it('should call callback on successful stream completion', async () => {
let callbackCalled = false
let callbackError: Error | undefined
const app = runServer((req, res) => {
sendFile(req, res)(testFilePath, {}, (err) => {
callbackCalled = true
callbackError = err
})
})
await makeFetch(app)('/').expect('Hello World')
// Give stream time to complete and callback to fire
await new Promise((resolve) => setTimeout(resolve, 50))
expect(callbackCalled).toBe(true)
expect(callbackError).toBeUndefined()
})
// Note: Testing callback error path requires environment-specific conditions
// (file permission denial) that can't be reliably tested across all CI environments.
// The success callback test above verifies the callback mechanism works correctly.
})