@thinkbun/middleware 1.0.0
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 +290 -0
- package/package.json +19 -0
- package/src/index.ts +32 -0
- package/src/middlewares/cors.ts +182 -0
- package/src/middlewares/errorHandler.ts +88 -0
- package/src/middlewares/logger.ts +143 -0
- package/src/middlewares/static.ts +236 -0
- package/src/middlewares/validator.ts +187 -0
- package/src/test/index.ts +1 -0
- package/src/test/middlewares.test.ts +265 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { cors, errorHandler, logger, staticServe, validator } from '../index';
|
|
3
|
+
import type { Context } from '@thinkbun/core';
|
|
4
|
+
|
|
5
|
+
// 创建模拟上下文
|
|
6
|
+
function createMockContext(req: Request): Context {
|
|
7
|
+
const mockApp: any = {
|
|
8
|
+
server: {
|
|
9
|
+
requestIP: () => '127.0.0.1'
|
|
10
|
+
},
|
|
11
|
+
logger: console
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ctx: any = {
|
|
15
|
+
req,
|
|
16
|
+
app: mockApp,
|
|
17
|
+
responseHeaders: new Headers(),
|
|
18
|
+
_status: 200,
|
|
19
|
+
state: {},
|
|
20
|
+
get status() {
|
|
21
|
+
return this._status;
|
|
22
|
+
},
|
|
23
|
+
set status(code: number) {
|
|
24
|
+
this._status = code;
|
|
25
|
+
},
|
|
26
|
+
get method() {
|
|
27
|
+
return this.req.method;
|
|
28
|
+
},
|
|
29
|
+
get url() {
|
|
30
|
+
return new URL(this.req.url);
|
|
31
|
+
},
|
|
32
|
+
get path() {
|
|
33
|
+
return this.url.pathname;
|
|
34
|
+
},
|
|
35
|
+
get headers() {
|
|
36
|
+
return this.req.headers;
|
|
37
|
+
},
|
|
38
|
+
header(name: string): string | null {
|
|
39
|
+
return this.req.headers.get(name);
|
|
40
|
+
},
|
|
41
|
+
get ip() {
|
|
42
|
+
return this.app.server.requestIP(this.req);
|
|
43
|
+
},
|
|
44
|
+
get userAgent() {
|
|
45
|
+
return this.header('User-Agent');
|
|
46
|
+
},
|
|
47
|
+
get cookies() {
|
|
48
|
+
return {};
|
|
49
|
+
},
|
|
50
|
+
cookie: () => ctx,
|
|
51
|
+
clearCookie: () => ctx,
|
|
52
|
+
body: async () => ({}),
|
|
53
|
+
text: async () => '',
|
|
54
|
+
json: (data: any, options: any = {}) => new Response(JSON.stringify(data), {
|
|
55
|
+
status: options.status || ctx.status,
|
|
56
|
+
headers: new Headers([...ctx.responseHeaders.entries(), ...(options.headers ? Object.entries(options.headers) : [])])
|
|
57
|
+
}),
|
|
58
|
+
ok: () => new Response(null, { status: 200, headers: ctx.responseHeaders }),
|
|
59
|
+
success: (data: any) => ctx.json({ errno: 0, data }),
|
|
60
|
+
fail: (errno: number, msg: string) => ctx.json({ errno, msg })
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return ctx;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('Middleware Tests', () => {
|
|
67
|
+
// 测试CORS中间件
|
|
68
|
+
describe('CORS Middleware', () => {
|
|
69
|
+
it('should add CORS headers to response', async () => {
|
|
70
|
+
const req = new Request('http://test:5300/', {
|
|
71
|
+
headers: {
|
|
72
|
+
Origin: 'http://example.com'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const ctx = createMockContext(req);
|
|
76
|
+
const corsMiddleware = cors();
|
|
77
|
+
|
|
78
|
+
await corsMiddleware(ctx, async () => {});
|
|
79
|
+
|
|
80
|
+
expect(ctx.responseHeaders.get('Access-Control-Allow-Origin')).toBe('*'); // 默认配置下使用*
|
|
81
|
+
expect(ctx.responseHeaders.get('Access-Control-Allow-Methods')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
82
|
+
expect(ctx.responseHeaders.get('Access-Control-Allow-Headers')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
83
|
+
expect(ctx.responseHeaders.get('Access-Control-Max-Age')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle OPTIONS requests', async () => {
|
|
87
|
+
const req = new Request('http://test:5300/', {
|
|
88
|
+
method: 'OPTIONS',
|
|
89
|
+
headers: {
|
|
90
|
+
Origin: 'http://example.com',
|
|
91
|
+
'Access-Control-Request-Methods': 'GET, POST',
|
|
92
|
+
'Access-Control-Request-Headers': 'Content-Type'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const ctx = createMockContext(req);
|
|
96
|
+
const corsMiddleware = cors();
|
|
97
|
+
|
|
98
|
+
const result = await corsMiddleware(ctx, async () => {});
|
|
99
|
+
|
|
100
|
+
expect(result).toBeDefined();
|
|
101
|
+
expect((result as Response).status).toBe(204);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 测试Logger中间件
|
|
106
|
+
describe('Logger Middleware', () => {
|
|
107
|
+
it('should log request and response information', async () => {
|
|
108
|
+
const req = new Request('http://test:5300/');
|
|
109
|
+
const ctx = createMockContext(req);
|
|
110
|
+
|
|
111
|
+
let logCalled = false;
|
|
112
|
+
const loggerMiddleware = logger({
|
|
113
|
+
logger: () => logCalled = true
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await loggerMiddleware(ctx, async () => {
|
|
117
|
+
return new Response('OK', { status: 200 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(logCalled).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 测试ErrorHandler中间件
|
|
125
|
+
describe('ErrorHandler Middleware', () => {
|
|
126
|
+
it('should handle errors and return JSON response', async () => {
|
|
127
|
+
const req = new Request('http://test:5300/');
|
|
128
|
+
const ctx = createMockContext(req);
|
|
129
|
+
const errorHandlerMiddleware = errorHandler();
|
|
130
|
+
|
|
131
|
+
const result = await errorHandlerMiddleware(ctx, async () => {
|
|
132
|
+
throw new Error('Test error');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result).toBeDefined();
|
|
136
|
+
const response = result as Response;
|
|
137
|
+
expect(response.status).toBe(500);
|
|
138
|
+
const responseData = await response.json();
|
|
139
|
+
expect(responseData.error).toBe('Internal Server Error');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle HTTP errors with specific status codes', async () => {
|
|
143
|
+
const req = new Request('http://test:5300/');
|
|
144
|
+
const ctx = createMockContext(req);
|
|
145
|
+
const errorHandlerMiddleware = errorHandler();
|
|
146
|
+
|
|
147
|
+
// 使用与errorHandler中间件匹配的错误类型
|
|
148
|
+
const notFoundError = new Error('Not Found');
|
|
149
|
+
notFoundError.name = 'NotFoundError';
|
|
150
|
+
|
|
151
|
+
const result = await errorHandlerMiddleware(ctx, async () => {
|
|
152
|
+
throw notFoundError;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result).toBeDefined();
|
|
156
|
+
const response = result as Response;
|
|
157
|
+
expect(response.status).toBe(404);
|
|
158
|
+
const responseData = await response.json();
|
|
159
|
+
expect(responseData.error).toBe('Not Found');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 测试Validator中间件
|
|
164
|
+
describe('Validator Middleware', () => {
|
|
165
|
+
it('should validate request body against schema', async () => {
|
|
166
|
+
const req = new Request('http://test:5300/', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify({ name: 'test', age: 25 })
|
|
170
|
+
});
|
|
171
|
+
const ctx = createMockContext(req);
|
|
172
|
+
|
|
173
|
+
// Mock the body method
|
|
174
|
+
ctx.body = async () => ({ name: 'test', age: 25 });
|
|
175
|
+
|
|
176
|
+
// 设置params属性
|
|
177
|
+
ctx.params = {};
|
|
178
|
+
|
|
179
|
+
const validatorMiddleware = validator({
|
|
180
|
+
rules: [
|
|
181
|
+
{
|
|
182
|
+
path: '/',
|
|
183
|
+
method: 'POST',
|
|
184
|
+
schema: {
|
|
185
|
+
body: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
name: { type: 'string' },
|
|
189
|
+
age: { type: 'number' }
|
|
190
|
+
},
|
|
191
|
+
required: ['name', 'age']
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let nextCalled = false;
|
|
199
|
+
await validatorMiddleware(ctx, async () => {
|
|
200
|
+
nextCalled = true;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(nextCalled).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should throw error for invalid request body', async () => {
|
|
207
|
+
const req = new Request('http://test:5300/', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ name: 'test' })
|
|
211
|
+
});
|
|
212
|
+
const ctx = createMockContext(req);
|
|
213
|
+
|
|
214
|
+
// Mock the body method
|
|
215
|
+
ctx.body = async () => ({ name: 'test' });
|
|
216
|
+
|
|
217
|
+
// 设置params属性
|
|
218
|
+
ctx.params = {};
|
|
219
|
+
|
|
220
|
+
const validatorMiddleware = validator({
|
|
221
|
+
rules: [
|
|
222
|
+
{
|
|
223
|
+
path: '/',
|
|
224
|
+
method: 'POST',
|
|
225
|
+
schema: {
|
|
226
|
+
body: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
name: { type: 'string' },
|
|
230
|
+
age: { type: 'number' }
|
|
231
|
+
},
|
|
232
|
+
required: ['name', 'age']
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await expect(validatorMiddleware(ctx, async () => {})).rejects.toThrow();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// 测试StaticServe中间件
|
|
244
|
+
describe('StaticServe Middleware', () => {
|
|
245
|
+
it('should serve static files', async () => {
|
|
246
|
+
const req = new Request('http://test:5300/test.txt');
|
|
247
|
+
const ctx = createMockContext(req);
|
|
248
|
+
|
|
249
|
+
// 这个测试会实际查找文件,所以我们只测试中间件是否能正常执行
|
|
250
|
+
const staticMiddleware = staticServe({ root: './public' });
|
|
251
|
+
|
|
252
|
+
let nextCalled = false;
|
|
253
|
+
try {
|
|
254
|
+
await staticMiddleware(ctx, async () => {
|
|
255
|
+
nextCalled = true;
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// 忽略文件不存在的错误
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 因为文件可能不存在,所以无论如何都应该调用next
|
|
262
|
+
expect(nextCalled).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
package/tsconfig.json
ADDED