weifuwu 0.2.2 → 0.2.4
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/README.md +90 -5
- package/dist/compress.d.ts +6 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1486 -0
- package/dist/middleware.d.ts +21 -0
- package/dist/rate-limit.d.ts +8 -0
- package/dist/router.d.ts +55 -0
- package/dist/serve.d.ts +19 -0
- package/dist/static.d.ts +7 -0
- package/dist/tsx.d.ts +17 -0
- package/dist/types.d.ts +9 -0
- package/dist/upload.d.ts +14 -0
- package/dist/validate.d.ts +9 -0
- package/package.json +14 -2
- package/AGENTS.md +0 -105
- package/compress.ts +0 -69
- package/cookie.ts +0 -58
- package/index.ts +0 -21
- package/middleware.ts +0 -178
- package/rate-limit.ts +0 -68
- package/router.ts +0 -701
- package/serve.ts +0 -126
- package/static.ts +0 -113
- package/test/compress.test.ts +0 -106
- package/test/cookie.test.ts +0 -79
- package/test/fixtures/pages/about/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/load.ts +0 -3
- package/test/fixtures/pages/blog/[slug]/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/route.ts +0 -7
- package/test/fixtures/pages/blog/layout.tsx +0 -3
- package/test/fixtures/pages/layout.tsx +0 -12
- package/test/fixtures/pages/page.tsx +0 -3
- package/test/middleware.test.ts +0 -407
- package/test/rate-limit.test.ts +0 -94
- package/test/static.test.ts +0 -93
- package/test/tsx.test.ts +0 -285
- package/test/unode.test.ts +0 -401
- package/test/upload.test.ts +0 -130
- package/test/validate.test.ts +0 -133
- package/tsconfig.json +0 -13
- package/tsx.ts +0 -374
- package/types.ts +0 -23
- package/upload.ts +0 -101
- package/validate.ts +0 -88
package/test/middleware.test.ts
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import { describe, it, mock, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { Router } from '../router.ts'
|
|
4
|
-
import { serve } from '../serve.ts'
|
|
5
|
-
import { auth, cors, logger } from '../middleware.ts'
|
|
6
|
-
|
|
7
|
-
function handler(text = 'ok') {
|
|
8
|
-
return () => new Response(text)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// ── Logger ────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
describe('logger', () => {
|
|
14
|
-
it('logs method, path and status', async () => {
|
|
15
|
-
const logs: string[] = []
|
|
16
|
-
mock.method(console, 'log', (msg: string) => { logs.push(msg) })
|
|
17
|
-
|
|
18
|
-
const r = new Router()
|
|
19
|
-
.use(logger())
|
|
20
|
-
.get('/hello', handler())
|
|
21
|
-
|
|
22
|
-
await r.handler()(new Request('http://localhost/hello'), { params: {}, query: {} })
|
|
23
|
-
|
|
24
|
-
assert.equal(logs.length, 1)
|
|
25
|
-
assert.ok(logs[0]!.includes('GET'))
|
|
26
|
-
assert.ok(logs[0]!.includes('/hello'))
|
|
27
|
-
assert.ok(logs[0]!.includes('200'))
|
|
28
|
-
|
|
29
|
-
mock.restoreAll()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('combined format includes search params', async () => {
|
|
33
|
-
const logs: string[] = []
|
|
34
|
-
mock.method(console, 'log', (msg: string) => { logs.push(msg) })
|
|
35
|
-
|
|
36
|
-
const r = new Router()
|
|
37
|
-
.use(logger({ format: 'combined' }))
|
|
38
|
-
.get('/search', handler())
|
|
39
|
-
|
|
40
|
-
await r.handler()(new Request('http://localhost/search?q=test'), { params: {}, query: {} })
|
|
41
|
-
|
|
42
|
-
assert.ok(logs[0]!.includes('?q=test'))
|
|
43
|
-
|
|
44
|
-
mock.restoreAll()
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// ── CORS ───────────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe('cors', () => {
|
|
51
|
-
it('adds Access-Control-Allow-Origin: * by default', async () => {
|
|
52
|
-
const r = new Router()
|
|
53
|
-
.use(cors())
|
|
54
|
-
.get('/data', handler())
|
|
55
|
-
|
|
56
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
57
|
-
assert.equal(res.headers.get('Access-Control-Allow-Origin'), '*')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('reflects request origin when in allowed list', async () => {
|
|
61
|
-
const r = new Router()
|
|
62
|
-
.use(cors({ origin: ['https://example.com', 'https://app.com'] }))
|
|
63
|
-
.get('/data', handler())
|
|
64
|
-
|
|
65
|
-
const res = await r.handler()(
|
|
66
|
-
new Request('http://localhost/data', { headers: { origin: 'https://example.com' } }),
|
|
67
|
-
{ params: {}, query: {} },
|
|
68
|
-
)
|
|
69
|
-
assert.equal(res.headers.get('Access-Control-Allow-Origin'), 'https://example.com')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('omits CORS headers for disallowed origin', async () => {
|
|
73
|
-
const r = new Router()
|
|
74
|
-
.use(cors({ origin: ['https://example.com'] }))
|
|
75
|
-
.get('/data', handler())
|
|
76
|
-
|
|
77
|
-
const res = await r.handler()(
|
|
78
|
-
new Request('http://localhost/data', { headers: { origin: 'https://evil.com' } }),
|
|
79
|
-
{ params: {}, query: {} },
|
|
80
|
-
)
|
|
81
|
-
assert.equal(res.headers.get('Access-Control-Allow-Origin'), null)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('handles OPTIONS preflight', async () => {
|
|
85
|
-
const r = new Router()
|
|
86
|
-
.use(cors({ origin: 'https://example.com', methods: ['GET', 'POST'], allowedHeaders: ['X-Custom'], maxAge: 3600 }))
|
|
87
|
-
.get('/data', handler())
|
|
88
|
-
|
|
89
|
-
const res = await r.handler()(
|
|
90
|
-
new Request('http://localhost/data', { method: 'OPTIONS', headers: { origin: 'https://example.com' } }),
|
|
91
|
-
{ params: {}, query: {} },
|
|
92
|
-
)
|
|
93
|
-
assert.equal(res.status, 204)
|
|
94
|
-
assert.equal(res.headers.get('Access-Control-Allow-Origin'), 'https://example.com')
|
|
95
|
-
assert.equal(res.headers.get('Access-Control-Allow-Methods'), 'GET, POST')
|
|
96
|
-
assert.equal(res.headers.get('Access-Control-Allow-Headers'), 'X-Custom')
|
|
97
|
-
assert.equal(res.headers.get('Access-Control-Max-Age'), '3600')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('sets Access-Control-Allow-Credentials', async () => {
|
|
101
|
-
const r = new Router()
|
|
102
|
-
.use(cors({ origin: 'https://example.com', credentials: true }))
|
|
103
|
-
.get('/data', handler())
|
|
104
|
-
|
|
105
|
-
const res = await r.handler()(
|
|
106
|
-
new Request('http://localhost/data', { headers: { origin: 'https://example.com' } }),
|
|
107
|
-
{ params: {}, query: {} },
|
|
108
|
-
)
|
|
109
|
-
assert.equal(res.headers.get('Access-Control-Allow-Credentials'), 'true')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('sets Access-Control-Expose-Headers', async () => {
|
|
113
|
-
const r = new Router()
|
|
114
|
-
.use(cors({ origin: '*', exposedHeaders: ['X-Total-Count', 'X-Page'] }))
|
|
115
|
-
.get('/data', handler())
|
|
116
|
-
|
|
117
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
118
|
-
assert.equal(res.headers.get('Access-Control-Expose-Headers'), 'X-Total-Count, X-Page')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('sets Vary: Origin', async () => {
|
|
122
|
-
const r = new Router()
|
|
123
|
-
.use(cors({ origin: 'https://example.com' }))
|
|
124
|
-
.get('/data', handler())
|
|
125
|
-
|
|
126
|
-
const res = await r.handler()(
|
|
127
|
-
new Request('http://localhost/data', { headers: { origin: 'https://example.com' } }),
|
|
128
|
-
{ params: {}, query: {} },
|
|
129
|
-
)
|
|
130
|
-
assert.equal(res.headers.get('Vary'), 'Origin')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('uses dynamic origin function', async () => {
|
|
134
|
-
const r = new Router()
|
|
135
|
-
.use(cors({
|
|
136
|
-
origin: (origin) => origin.endsWith('.trusted.com') ? origin : false,
|
|
137
|
-
}))
|
|
138
|
-
.get('/data', handler())
|
|
139
|
-
|
|
140
|
-
const res1 = await r.handler()(
|
|
141
|
-
new Request('http://localhost/data', { headers: { origin: 'https://app.trusted.com' } }),
|
|
142
|
-
{ params: {}, query: {} },
|
|
143
|
-
)
|
|
144
|
-
assert.equal(res1.headers.get('Access-Control-Allow-Origin'), 'https://app.trusted.com')
|
|
145
|
-
|
|
146
|
-
const res2 = await r.handler()(
|
|
147
|
-
new Request('http://localhost/data', { headers: { origin: 'https://evil.com' } }),
|
|
148
|
-
{ params: {}, query: {} },
|
|
149
|
-
)
|
|
150
|
-
assert.equal(res2.headers.get('Access-Control-Allow-Origin'), null)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('returns 204 for OPTIONS without matching route', async () => {
|
|
154
|
-
const r = new Router()
|
|
155
|
-
.use(cors({ origin: '*' }))
|
|
156
|
-
.get('/data', handler())
|
|
157
|
-
|
|
158
|
-
const res = await r.handler()(
|
|
159
|
-
new Request('http://localhost/other', { method: 'OPTIONS', headers: { origin: 'https://example.com' } }),
|
|
160
|
-
{ params: {}, query: {} },
|
|
161
|
-
)
|
|
162
|
-
assert.equal(res.status, 204)
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// ── Auth ───────────────────────────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
function authHandler() {
|
|
169
|
-
return (req: Request, ctx: { user?: unknown }) =>
|
|
170
|
-
Response.json({ user: ctx.user })
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
describe('auth', () => {
|
|
174
|
-
it('rejects missing Authorization header with 401', async () => {
|
|
175
|
-
const r = new Router()
|
|
176
|
-
.use(auth({ token: 'secret' }))
|
|
177
|
-
.get('/data', handler())
|
|
178
|
-
|
|
179
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
180
|
-
assert.equal(res.status, 401)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('sets WWW-Authenticate header on 401', async () => {
|
|
184
|
-
const r = new Router()
|
|
185
|
-
.use(auth({ token: 'secret' }))
|
|
186
|
-
.get('/data', handler())
|
|
187
|
-
|
|
188
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
189
|
-
assert.equal(res.headers.get('WWW-Authenticate'), 'Bearer')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('accepts valid Bearer token', async () => {
|
|
193
|
-
const r = new Router()
|
|
194
|
-
.use(auth({ token: 'secret' }))
|
|
195
|
-
.get('/data', handler())
|
|
196
|
-
|
|
197
|
-
const res = await r.handler()(
|
|
198
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer secret' } }),
|
|
199
|
-
{ params: {}, query: {} },
|
|
200
|
-
)
|
|
201
|
-
assert.equal(res.status, 200)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('rejects invalid Bearer token with 403', async () => {
|
|
205
|
-
const r = new Router()
|
|
206
|
-
.use(auth({ token: 'secret' }))
|
|
207
|
-
.get('/data', handler())
|
|
208
|
-
|
|
209
|
-
const res = await r.handler()(
|
|
210
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer wrong' } }),
|
|
211
|
-
{ params: {}, query: {} },
|
|
212
|
-
)
|
|
213
|
-
assert.equal(res.status, 403)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('supports custom header name (X-API-Key)', async () => {
|
|
217
|
-
const r = new Router()
|
|
218
|
-
.use(auth({ token: 'my-key', header: 'X-API-Key' }))
|
|
219
|
-
.get('/data', handler())
|
|
220
|
-
|
|
221
|
-
const res1 = await r.handler()(
|
|
222
|
-
new Request('http://localhost/data', { headers: { 'X-API-Key': 'my-key' } }),
|
|
223
|
-
{ params: {}, query: {} },
|
|
224
|
-
)
|
|
225
|
-
assert.equal(res1.status, 200)
|
|
226
|
-
|
|
227
|
-
const res2 = await r.handler()(
|
|
228
|
-
new Request('http://localhost/data', { headers: { 'X-API-Key': 'wrong' } }),
|
|
229
|
-
{ params: {}, query: {} },
|
|
230
|
-
)
|
|
231
|
-
assert.equal(res2.status, 403)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('does not set WWW-Authenticate for custom header', async () => {
|
|
235
|
-
const r = new Router()
|
|
236
|
-
.use(auth({ token: 'my-key', header: 'X-API-Key' }))
|
|
237
|
-
.get('/data', handler())
|
|
238
|
-
|
|
239
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
240
|
-
assert.equal(res.headers.get('WWW-Authenticate'), null)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('verify returning boolean true passes', async () => {
|
|
244
|
-
const r = new Router()
|
|
245
|
-
.use(auth({
|
|
246
|
-
verify: () => true,
|
|
247
|
-
}))
|
|
248
|
-
.get('/data', handler())
|
|
249
|
-
|
|
250
|
-
const res = await r.handler()(
|
|
251
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer any' } }),
|
|
252
|
-
{ params: {}, query: {} },
|
|
253
|
-
)
|
|
254
|
-
assert.equal(res.status, 200)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('verify returning boolean false rejects with 403', async () => {
|
|
258
|
-
const r = new Router()
|
|
259
|
-
.use(auth({
|
|
260
|
-
verify: () => false,
|
|
261
|
-
}))
|
|
262
|
-
.get('/data', handler())
|
|
263
|
-
|
|
264
|
-
const res = await r.handler()(
|
|
265
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer any' } }),
|
|
266
|
-
{ params: {}, query: {} },
|
|
267
|
-
)
|
|
268
|
-
assert.equal(res.status, 403)
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('verify returning object sets ctx.user', async () => {
|
|
272
|
-
const r = new Router()
|
|
273
|
-
.use(auth({
|
|
274
|
-
verify: () => ({ sub: 'user-1', role: 'admin' }),
|
|
275
|
-
}))
|
|
276
|
-
.get('/admin', authHandler())
|
|
277
|
-
|
|
278
|
-
const res = await r.handler()(
|
|
279
|
-
new Request('http://localhost/admin', { headers: { Authorization: 'Bearer token' } }),
|
|
280
|
-
{ params: {}, query: {} },
|
|
281
|
-
)
|
|
282
|
-
const data = await res.json() as Record<string, unknown>
|
|
283
|
-
assert.deepEqual(data.user, { sub: 'user-1', role: 'admin' })
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
it('verify returning null rejects with 403', async () => {
|
|
287
|
-
const r = new Router()
|
|
288
|
-
.use(auth({
|
|
289
|
-
verify: () => null,
|
|
290
|
-
}))
|
|
291
|
-
.get('/data', handler())
|
|
292
|
-
|
|
293
|
-
const res = await r.handler()(
|
|
294
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer any' } }),
|
|
295
|
-
{ params: {}, query: {} },
|
|
296
|
-
)
|
|
297
|
-
assert.equal(res.status, 403)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
it('works as route-level middleware', async () => {
|
|
301
|
-
const mw = auth({ verify: () => ({ sub: 'u1' }) })
|
|
302
|
-
|
|
303
|
-
const r = new Router()
|
|
304
|
-
.get('/admin', mw, (req, ctx) =>
|
|
305
|
-
Response.json({ user: ctx.user }),
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
const res = await r.handler()(
|
|
309
|
-
new Request('http://localhost/admin', { headers: { Authorization: 'Bearer token' } }),
|
|
310
|
-
{ params: {}, query: {} },
|
|
311
|
-
)
|
|
312
|
-
assert.equal(res.status, 200)
|
|
313
|
-
const data = await res.json() as Record<string, unknown>
|
|
314
|
-
assert.deepEqual(data.user, { sub: 'u1' })
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('route-level auth rejects without token', async () => {
|
|
318
|
-
const mw = auth({ verify: () => ({ sub: 'u1' }) })
|
|
319
|
-
|
|
320
|
-
const r = new Router()
|
|
321
|
-
.get('/admin', mw, (req, ctx) => Response.json({ user: ctx.user }))
|
|
322
|
-
|
|
323
|
-
const res = await r.handler()(
|
|
324
|
-
new Request('http://localhost/admin'),
|
|
325
|
-
{ params: {}, query: {} },
|
|
326
|
-
)
|
|
327
|
-
assert.equal(res.status, 401)
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
// ── Proxy mode ──────────────────────────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
it('proxy: 2xx response passes auth', async () => {
|
|
333
|
-
const proxy = serve(() => new Response(JSON.stringify({ sub: 'u1' }), {
|
|
334
|
-
headers: { 'content-type': 'application/json' },
|
|
335
|
-
}), { port: 0 })
|
|
336
|
-
await proxy.ready
|
|
337
|
-
const proxyUrl = `http://localhost:${proxy.port}/validate`
|
|
338
|
-
|
|
339
|
-
const r = new Router()
|
|
340
|
-
.get('/data', auth({ proxy: proxyUrl }), authHandler())
|
|
341
|
-
|
|
342
|
-
const res = await r.handler()(
|
|
343
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer valid' } }),
|
|
344
|
-
{ params: {}, query: {} },
|
|
345
|
-
)
|
|
346
|
-
assert.equal(res.status, 200)
|
|
347
|
-
const data = await res.json() as Record<string, unknown>
|
|
348
|
-
assert.deepEqual(data.user, { sub: 'u1' })
|
|
349
|
-
proxy.stop()
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
it('proxy: 4xx response rejects auth', async () => {
|
|
353
|
-
const proxy = serve(() => new Response('Unauthorized', { status: 401 }), { port: 0 })
|
|
354
|
-
await proxy.ready
|
|
355
|
-
const proxyUrl = `http://localhost:${proxy.port}/validate`
|
|
356
|
-
|
|
357
|
-
const r = new Router()
|
|
358
|
-
.get('/data', auth({ proxy: proxyUrl }), handler())
|
|
359
|
-
|
|
360
|
-
const res = await r.handler()(
|
|
361
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer bad' } }),
|
|
362
|
-
{ params: {}, query: {} },
|
|
363
|
-
)
|
|
364
|
-
assert.equal(res.status, 401)
|
|
365
|
-
proxy.stop()
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
it('proxy forwards Authorization header for Bearer tokens', async () => {
|
|
369
|
-
let receivedAuth: string | null = null
|
|
370
|
-
const proxy = serve((req) => {
|
|
371
|
-
receivedAuth = req.headers.get('Authorization')
|
|
372
|
-
return new Response('ok')
|
|
373
|
-
}, { port: 0 })
|
|
374
|
-
await proxy.ready
|
|
375
|
-
const proxyUrl = `http://localhost:${proxy.port}/validate`
|
|
376
|
-
|
|
377
|
-
const r = new Router()
|
|
378
|
-
.get('/data', auth({ proxy: proxyUrl }), handler())
|
|
379
|
-
|
|
380
|
-
await r.handler()(
|
|
381
|
-
new Request('http://localhost/data', { headers: { Authorization: 'Bearer mytoken' } }),
|
|
382
|
-
{ params: {}, query: {} },
|
|
383
|
-
)
|
|
384
|
-
assert.equal(receivedAuth, 'Bearer mytoken')
|
|
385
|
-
proxy.stop()
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
it('proxy sends access_token query param for custom header', async () => {
|
|
389
|
-
let receivedQuery = ''
|
|
390
|
-
const proxy = serve((req) => {
|
|
391
|
-
receivedQuery = new URL(req.url).search
|
|
392
|
-
return new Response('ok')
|
|
393
|
-
}, { port: 0 })
|
|
394
|
-
await proxy.ready
|
|
395
|
-
const proxyUrl = `http://localhost:${proxy.port}/validate`
|
|
396
|
-
|
|
397
|
-
const r = new Router()
|
|
398
|
-
.get('/data', auth({ proxy: proxyUrl, header: 'X-API-Key' }), handler())
|
|
399
|
-
|
|
400
|
-
await r.handler()(
|
|
401
|
-
new Request('http://localhost/data', { headers: { 'X-API-Key': 'my-key' } }),
|
|
402
|
-
{ params: {}, query: {} },
|
|
403
|
-
)
|
|
404
|
-
assert.ok(receivedQuery.includes('access_token=my-key'))
|
|
405
|
-
proxy.stop()
|
|
406
|
-
})
|
|
407
|
-
})
|
package/test/rate-limit.test.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { Router } from '../router.ts'
|
|
4
|
-
import { rateLimit } from '../rate-limit.ts'
|
|
5
|
-
|
|
6
|
-
describe('rateLimit', () => {
|
|
7
|
-
it('allows requests under the limit', async () => {
|
|
8
|
-
const r = new Router()
|
|
9
|
-
.use(rateLimit({ max: 5, window: 60_000 }))
|
|
10
|
-
.get('/data', () => new Response('ok'))
|
|
11
|
-
|
|
12
|
-
for (let i = 0; i < 5; i++) {
|
|
13
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
14
|
-
assert.equal(res.status, 200)
|
|
15
|
-
}
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('blocks requests exceeding the limit', async () => {
|
|
19
|
-
const r = new Router()
|
|
20
|
-
.use(rateLimit({ max: 3, window: 60_000 }))
|
|
21
|
-
.get('/data', () => new Response('ok'))
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < 3; i++) {
|
|
24
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
25
|
-
assert.equal(res.status, 200)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
29
|
-
assert.equal(res.status, 429)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('returns rate limit headers', async () => {
|
|
33
|
-
const r = new Router()
|
|
34
|
-
.use(rateLimit({ max: 2, window: 60_000 }))
|
|
35
|
-
.get('/data', () => new Response('ok'))
|
|
36
|
-
|
|
37
|
-
let res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
38
|
-
assert.equal(res.headers.get('X-RateLimit-Remaining'), '1')
|
|
39
|
-
|
|
40
|
-
res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
41
|
-
assert.equal(res.headers.get('X-RateLimit-Remaining'), '0')
|
|
42
|
-
|
|
43
|
-
res = await r.handler()(new Request('http://localhost/data'), { params: {}, query: {} })
|
|
44
|
-
assert.equal(res.status, 429)
|
|
45
|
-
assert.equal(res.headers.get('X-RateLimit-Limit'), '2')
|
|
46
|
-
assert.equal(res.headers.get('X-RateLimit-Remaining'), '0')
|
|
47
|
-
assert.ok(res.headers.get('X-RateLimit-Reset'))
|
|
48
|
-
assert.ok(res.headers.get('Retry-After'))
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('uses custom key function', async () => {
|
|
52
|
-
const keys: string[] = []
|
|
53
|
-
const r = new Router()
|
|
54
|
-
.use(rateLimit({
|
|
55
|
-
max: 1,
|
|
56
|
-
window: 60_000,
|
|
57
|
-
key: (req) => {
|
|
58
|
-
const key = req.headers.get('x-api-key') ?? 'anonymous'
|
|
59
|
-
keys.push(key)
|
|
60
|
-
return key
|
|
61
|
-
},
|
|
62
|
-
}))
|
|
63
|
-
.get('/data', () => new Response('ok'))
|
|
64
|
-
|
|
65
|
-
const req1 = new Request('http://localhost/data', { headers: { 'x-api-key': 'alice' } })
|
|
66
|
-
const res1 = await r.handler()(req1, { params: {}, query: {} })
|
|
67
|
-
assert.equal(res1.status, 200)
|
|
68
|
-
|
|
69
|
-
const res2 = await r.handler()(req1, { params: {}, query: {} })
|
|
70
|
-
assert.equal(res2.status, 429)
|
|
71
|
-
|
|
72
|
-
const req2 = new Request('http://localhost/data', { headers: { 'x-api-key': 'bob' } })
|
|
73
|
-
const res3 = await r.handler()(req2, { params: {}, query: {} })
|
|
74
|
-
assert.equal(res3.status, 200)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('resets after window expires', { timeout: 2000 }, async () => {
|
|
78
|
-
const r = new Router()
|
|
79
|
-
.use(rateLimit({ max: 1, window: 100 }))
|
|
80
|
-
.get('/data', () => new Response('ok'))
|
|
81
|
-
|
|
82
|
-
const req = new Request('http://localhost/data')
|
|
83
|
-
const res1 = await r.handler()(req, { params: {}, query: {} })
|
|
84
|
-
assert.equal(res1.status, 200)
|
|
85
|
-
|
|
86
|
-
const res2 = await r.handler()(req, { params: {}, query: {} })
|
|
87
|
-
assert.equal(res2.status, 429)
|
|
88
|
-
|
|
89
|
-
await new Promise((r) => setTimeout(r, 150))
|
|
90
|
-
|
|
91
|
-
const res3 = await r.handler()(req, { params: {}, query: {} })
|
|
92
|
-
assert.equal(res3.status, 200)
|
|
93
|
-
})
|
|
94
|
-
})
|
package/test/static.test.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { writeFile, mkdir, rm } from 'node:fs/promises'
|
|
4
|
-
import { resolve } from 'node:path'
|
|
5
|
-
import { Router } from '../router.ts'
|
|
6
|
-
import { serve } from '../serve.ts'
|
|
7
|
-
import { serveStatic } from '../static.ts'
|
|
8
|
-
|
|
9
|
-
const tmpDir = resolve(import.meta.dirname, '../.test-static')
|
|
10
|
-
|
|
11
|
-
before(async () => {
|
|
12
|
-
await mkdir(tmpDir, { recursive: true })
|
|
13
|
-
await writeFile(resolve(tmpDir, 'hello.txt'), 'Hello, World!')
|
|
14
|
-
await writeFile(resolve(tmpDir, 'index.html'), '<h1>Index</h1>')
|
|
15
|
-
await writeFile(resolve(tmpDir, 'script.js'), 'console.log(1)')
|
|
16
|
-
await mkdir(resolve(tmpDir, 'sub'), { recursive: true })
|
|
17
|
-
await writeFile(resolve(tmpDir, 'sub', 'deep.txt'), 'deep')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
after(async () => {
|
|
21
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('serveStatic', () => {
|
|
25
|
-
it('serves a file', async () => {
|
|
26
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
27
|
-
const res = await r.handler()(new Request('http://localhost/files/hello.txt'), { params: {}, query: {} })
|
|
28
|
-
assert.equal(res.status, 200)
|
|
29
|
-
assert.equal(await res.text(), 'Hello, World!')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('sets Content-Type based on extension', async () => {
|
|
33
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
34
|
-
const res = await r.handler()(new Request('http://localhost/files/script.js'), { params: {}, query: {} })
|
|
35
|
-
assert.ok(res.headers.get('Content-Type')?.includes('javascript'))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('sets ETag header', async () => {
|
|
39
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
40
|
-
const res = await r.handler()(new Request('http://localhost/files/hello.txt'), { params: {}, query: {} })
|
|
41
|
-
assert.ok(res.headers.get('ETag'))
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns 304 on matching ETag', async () => {
|
|
45
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
46
|
-
const res1 = await r.handler()(new Request('http://localhost/files/hello.txt'), { params: {}, query: {} })
|
|
47
|
-
const etag = res1.headers.get('ETag')
|
|
48
|
-
const res2 = await r.handler()(
|
|
49
|
-
new Request('http://localhost/files/hello.txt', { headers: { 'if-none-match': etag! } }),
|
|
50
|
-
{ params: {}, query: {} },
|
|
51
|
-
)
|
|
52
|
-
assert.equal(res2.status, 304)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('serves index.html for directory', async () => {
|
|
56
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
57
|
-
const res = await r.handler()(new Request('http://localhost/files/'), { params: {}, query: {} })
|
|
58
|
-
assert.equal(res.status, 200)
|
|
59
|
-
assert.equal(await res.text(), '<h1>Index</h1>')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('returns 404 for missing file', async () => {
|
|
63
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
64
|
-
const res = await r.handler()(new Request('http://localhost/files/nope.txt'), { params: {}, query: {} })
|
|
65
|
-
assert.equal(res.status, 404)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('blocks directory traversal via wildcard param', async () => {
|
|
69
|
-
const handler = serveStatic(tmpDir)
|
|
70
|
-
const res = await handler(
|
|
71
|
-
new Request('http://localhost/ignored'),
|
|
72
|
-
{ params: { '*': '../../package.json' }, query: {} },
|
|
73
|
-
)
|
|
74
|
-
assert.equal(res.status, 403)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('supports nested paths', async () => {
|
|
78
|
-
const r = new Router().get('/files/*', serveStatic(tmpDir))
|
|
79
|
-
const res = await r.handler()(new Request('http://localhost/files/sub/deep.txt'), { params: {}, query: {} })
|
|
80
|
-
assert.equal(res.status, 200)
|
|
81
|
-
assert.equal(await res.text(), 'deep')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('works with serve() end-to-end', async () => {
|
|
85
|
-
const r = new Router().get('/static/*', serveStatic(tmpDir))
|
|
86
|
-
const server = serve(r.handler(), { port: 0 })
|
|
87
|
-
await server.ready
|
|
88
|
-
const res = await fetch(`http://localhost:${server.port}/static/hello.txt`)
|
|
89
|
-
assert.equal(res.status, 200)
|
|
90
|
-
assert.equal(await res.text(), 'Hello, World!')
|
|
91
|
-
server.stop()
|
|
92
|
-
})
|
|
93
|
-
})
|