@wutiange/log-listener-plugin 2.0.2-alpha.2 → 2.0.2-alpha.3
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/dist/index.cjs.js +1447 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1447 -4
- package/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/HTTPInterceptor.ts +9 -13
- package/src/__tests__/HTTPInterceptor.test.ts +96 -56
- package/src/__tests__/utils.test.ts +197 -75
- package/src/logPlugin.ts +1 -4
- package/src/utils.ts +6 -69
@@ -1,114 +1,236 @@
|
|
1
|
-
import
|
2
|
-
import { createClassWithErrorHandling, hasPort } from '../utils';
|
1
|
+
import { hasPort, formDataToString, sleep, typeReplacer } from '../utils';
|
3
2
|
|
4
|
-
describe('
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
describe('hasPort function', () => {
|
4
|
+
it('should return true for URLs with explicit ports', () => {
|
5
|
+
expect(hasPort('http://example.com:8080')).toBe(true);
|
6
|
+
expect(hasPort('ftp://example.com:210')).toBe(true);
|
7
|
+
});
|
8
|
+
|
9
|
+
it('should return false for URLs without explicit ports', () => {
|
10
|
+
expect(hasPort('http://example.com')).toBe(false);
|
11
|
+
expect(hasPort('https://example.com')).toBe(false);
|
12
|
+
expect(hasPort('ftp://example.com')).toBe(false);
|
13
|
+
});
|
14
|
+
|
15
|
+
it('should return false for invalid URLs', () => {
|
16
|
+
expect(hasPort('not a url')).toBe(false);
|
17
|
+
expect(hasPort('http:/example.com')).toBe(false);
|
18
|
+
expect(hasPort('example.com:8080')).toBe(false);
|
19
|
+
});
|
20
|
+
|
21
|
+
it('should return false for empty input', () => {
|
22
|
+
expect(hasPort('')).toBe(false);
|
23
|
+
});
|
9
24
|
|
10
|
-
|
11
|
-
|
12
|
-
|
25
|
+
it('should return false for non-string input', () => {
|
26
|
+
expect(hasPort(null as any)).toBe(false);
|
27
|
+
expect(hasPort(undefined as any)).toBe(false);
|
28
|
+
expect(hasPort(123 as any)).toBe(false);
|
29
|
+
expect(hasPort({} as any)).toBe(false);
|
30
|
+
});
|
13
31
|
|
14
|
-
|
15
|
-
|
16
|
-
|
32
|
+
it('should handle URLs with default ports correctly', () => {
|
33
|
+
expect(hasPort('http://example.com:80')).toBe(false);
|
34
|
+
expect(hasPort('https://example.com:443')).toBe(false);
|
35
|
+
});
|
17
36
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
}
|
37
|
+
it('should handle URLs with IPv6 addresses', () => {
|
38
|
+
expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
|
39
|
+
expect(hasPort('https://[2001:db8::1]')).toBe(false);
|
40
|
+
});
|
41
|
+
|
42
|
+
it('should handle URLs with userinfo', () => {
|
43
|
+
expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
|
44
|
+
expect(hasPort('http://user:pass@example.com')).toBe(false);
|
45
|
+
});
|
46
|
+
});
|
22
47
|
|
23
|
-
|
48
|
+
describe('formDataToString', () => {
|
49
|
+
let mockFormData: FormData;
|
24
50
|
|
25
51
|
beforeEach(() => {
|
26
|
-
|
52
|
+
// 创建一个模拟的 FormData 对象
|
53
|
+
mockFormData = new FormData();
|
54
|
+
// 模拟 getParts 方法
|
55
|
+
(mockFormData as any).getParts = jest.fn();
|
27
56
|
});
|
28
57
|
|
29
|
-
|
30
|
-
|
58
|
+
it('should convert form data with text fields to string', () => {
|
59
|
+
// 模拟 getParts 返回包含文本字段的数据
|
60
|
+
(mockFormData as any).getParts.mockReturnValue([
|
61
|
+
{
|
62
|
+
headers: {
|
63
|
+
'content-disposition': 'form-data; name="field1"',
|
64
|
+
},
|
65
|
+
string: 'value1',
|
66
|
+
},
|
67
|
+
]);
|
68
|
+
|
69
|
+
const result = formDataToString(mockFormData);
|
70
|
+
|
71
|
+
// 验证基本结构
|
72
|
+
expect(result).toMatch(/^------WebKitFormBoundary.*\r\n/);
|
73
|
+
expect(result).toMatch(/Content-Disposition: form-data; name="field1"\r\n/);
|
74
|
+
expect(result).toMatch(/Content-Length: 6\r\n/);
|
75
|
+
expect(result).toMatch(/value1\r\n/);
|
76
|
+
expect(result).toMatch(/----WebKitFormBoundary.*--\r\n$/);
|
31
77
|
});
|
32
78
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
79
|
+
it('should handle form data with content-type header', () => {
|
80
|
+
// 模拟 getParts 返回包含 content-type 的数据
|
81
|
+
(mockFormData as any).getParts.mockReturnValue([
|
82
|
+
{
|
83
|
+
headers: {
|
84
|
+
'content-disposition': 'form-data; name="file"; filename="test.txt"',
|
85
|
+
'content-type': 'text/plain',
|
86
|
+
},
|
87
|
+
string: 'file content',
|
88
|
+
},
|
89
|
+
]);
|
90
|
+
|
91
|
+
const result = formDataToString(mockFormData);
|
92
|
+
|
93
|
+
expect(result).toMatch(
|
94
|
+
/Content-Disposition: form-data; name="file"; filename="test.txt"\r\n/,
|
95
|
+
);
|
96
|
+
expect(result).toMatch(/Content-Type: text\/plain\r\n/);
|
97
|
+
expect(result).toMatch(/Content-Length: 12\r\n/);
|
98
|
+
expect(result).toMatch(/file content\r\n/);
|
37
99
|
});
|
38
100
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
101
|
+
it('should handle multiple form fields', () => {
|
102
|
+
// 模拟 getParts 返回多个字段
|
103
|
+
(mockFormData as any).getParts.mockReturnValue([
|
104
|
+
{
|
105
|
+
headers: {
|
106
|
+
'content-disposition': 'form-data; name="field1"',
|
107
|
+
},
|
108
|
+
string: 'value1',
|
109
|
+
},
|
110
|
+
{
|
111
|
+
headers: {
|
112
|
+
'content-disposition': 'form-data; name="field2"',
|
113
|
+
},
|
114
|
+
string: 'value2',
|
115
|
+
},
|
116
|
+
]);
|
117
|
+
|
118
|
+
const result = formDataToString(mockFormData);
|
119
|
+
|
120
|
+
expect(result).toMatch(/field1.*value1.*field2.*value2/s);
|
121
|
+
expect((result.match(/----WebKitFormBoundary/g) || []).length).toBe(3); // 开始、中间、结束
|
44
122
|
});
|
45
123
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
124
|
+
it('should handle URI parts', () => {
|
125
|
+
// 模拟 getParts 返回包含 URI 的数据
|
126
|
+
(mockFormData as any).getParts.mockReturnValue([
|
127
|
+
{
|
128
|
+
headers: {
|
129
|
+
'content-disposition': 'form-data; name="file"',
|
130
|
+
'content-type': 'image/jpeg',
|
131
|
+
},
|
132
|
+
uri: 'file:///path/to/image.jpg',
|
133
|
+
},
|
134
|
+
]);
|
135
|
+
|
136
|
+
const result = formDataToString(mockFormData);
|
137
|
+
|
138
|
+
expect(result).toMatch(/Content-Type: image\/jpeg\r\n/);
|
139
|
+
expect(result).toMatch(/file:\/\/\/path\/to\/image.jpg\r\n/);
|
50
140
|
});
|
141
|
+
});
|
51
142
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
143
|
+
describe('sleep function', () => {
|
144
|
+
// 测试正常延迟情况
|
145
|
+
it('should resolve after specified delay', async () => {
|
146
|
+
const startTime = Date.now();
|
147
|
+
const delay = 100;
|
148
|
+
|
149
|
+
await sleep(delay);
|
150
|
+
const endTime = Date.now();
|
151
|
+
const actualDelay = endTime - startTime;
|
152
|
+
|
153
|
+
// 由于 JavaScript 定时器的不精确性,我们允许一个小的误差范围
|
154
|
+
expect(actualDelay).toBeGreaterThanOrEqual(delay);
|
155
|
+
expect(actualDelay).toBeLessThan(delay + 50); // 允许 50ms 的误差
|
57
156
|
});
|
58
157
|
|
59
|
-
|
60
|
-
|
61
|
-
const
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
158
|
+
// 测试超时拒绝情况
|
159
|
+
it('should reject with timeout error when isReject is true', async () => {
|
160
|
+
const delay = 100;
|
161
|
+
|
162
|
+
await expect(sleep(delay, true)).rejects.toEqual({
|
163
|
+
code: 11001,
|
164
|
+
key: '@wutiange/log-listener-plugin%%timeout',
|
165
|
+
message: 'Timeout',
|
166
|
+
});
|
67
167
|
});
|
68
168
|
});
|
69
169
|
|
170
|
+
describe('typeReplacer', () => {
|
171
|
+
// 测试 Error 类型转换
|
172
|
+
it('should convert Error to string', () => {
|
173
|
+
const error = new Error('test error');
|
174
|
+
expect(typeReplacer('error', error)).toBe('Error: test error');
|
175
|
+
});
|
70
176
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
177
|
+
// 测试 Function 类型转换
|
178
|
+
it('should convert Function to string', () => {
|
179
|
+
const fn = function test() {
|
180
|
+
return 'hello';
|
181
|
+
};
|
182
|
+
const result = typeReplacer('fn', fn);
|
183
|
+
expect(result).toContain('function test()');
|
75
184
|
});
|
76
185
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
expect(
|
186
|
+
// 测试 Symbol 类型转换
|
187
|
+
it('should convert Symbol to string', () => {
|
188
|
+
const sym = Symbol('test');
|
189
|
+
expect(typeReplacer('symbol', sym)).toBe('Symbol(test)');
|
81
190
|
});
|
82
191
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
expect(
|
192
|
+
// 测试 BigInt 类型转换
|
193
|
+
it('should convert BigInt to string', () => {
|
194
|
+
const big = BigInt(9007199254740991);
|
195
|
+
expect(typeReplacer('bigint', big)).toBe('9007199254740991');
|
87
196
|
});
|
88
197
|
|
89
|
-
|
90
|
-
|
198
|
+
// 测试 RegExp 类型转换
|
199
|
+
it('should convert RegExp to string', () => {
|
200
|
+
const regex = /test/g;
|
201
|
+
expect(typeReplacer('regex', regex)).toBe('/test/g');
|
91
202
|
});
|
92
203
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
expect(
|
97
|
-
expect(hasPort({} as any)).toBe(false);
|
204
|
+
// 测试 Set 类型转换
|
205
|
+
it('should convert Set to array', () => {
|
206
|
+
const set = new Set([1, 2, 3]);
|
207
|
+
expect(typeReplacer('set', set)).toEqual([1, 2, 3]);
|
98
208
|
});
|
99
209
|
|
100
|
-
|
101
|
-
|
102
|
-
|
210
|
+
// 测试 Map 类型转换
|
211
|
+
it('should convert Map to object', () => {
|
212
|
+
const map = new Map([
|
213
|
+
['key1', 'value1'],
|
214
|
+
['key2', 'value2'],
|
215
|
+
]);
|
216
|
+
expect(typeReplacer('map', map)).toEqual({
|
217
|
+
key1: 'value1',
|
218
|
+
key2: 'value2',
|
219
|
+
});
|
103
220
|
});
|
104
221
|
|
105
|
-
|
106
|
-
|
107
|
-
expect(
|
222
|
+
// 测试普通值不变
|
223
|
+
it('should return primitive values as is', () => {
|
224
|
+
expect(typeReplacer('string', 'test')).toBe('test');
|
225
|
+
expect(typeReplacer('number', 42)).toBe(42);
|
226
|
+
expect(typeReplacer('boolean', true)).toBe(true);
|
227
|
+
expect(typeReplacer('null', null)).toBe(null);
|
228
|
+
expect(typeReplacer('undefined', undefined)).toBe(undefined);
|
108
229
|
});
|
109
230
|
|
110
|
-
|
111
|
-
|
112
|
-
|
231
|
+
// 测试普通对象不变
|
232
|
+
it('should return objects as is', () => {
|
233
|
+
const obj = { name: 'test', age: 25 };
|
234
|
+
expect(typeReplacer('object', obj)).toEqual(obj);
|
113
235
|
});
|
114
236
|
});
|
package/src/logPlugin.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import Server from './Server';
|
2
|
-
import { createClassWithErrorHandling } from './utils';
|
3
2
|
import { httpInterceptor } from './HTTPInterceptor';
|
4
3
|
import {
|
5
4
|
DEFAULT_TIMEOUT,
|
@@ -232,7 +231,5 @@ class LogPlugin {
|
|
232
231
|
this._log(Level.ERROR, Tag.DEFAULT, ...data);
|
233
232
|
};
|
234
233
|
}
|
235
|
-
const
|
236
|
-
const logPlugin = new SafeLogPlugin();
|
237
|
-
export { SafeLogPlugin };
|
234
|
+
const logPlugin = new LogPlugin();
|
238
235
|
export default logPlugin;
|
package/src/utils.ts
CHANGED
@@ -24,75 +24,12 @@ export function hasPort(url: string) {
|
|
24
24
|
return false;
|
25
25
|
}
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
const parsedUrl = new URL(url);
|
27
|
+
// 使用 URL 构造函数解析 URL
|
28
|
+
const parsedUrl = new URL(url);
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
} catch (error) {
|
35
|
-
logger.error(error);
|
36
|
-
// 如果 URL 无效,捕获错误并返回 false
|
37
|
-
return false;
|
38
|
-
}
|
39
|
-
}
|
40
|
-
|
41
|
-
type Constructor<T = {}> = new (...args: any[]) => T;
|
42
|
-
|
43
|
-
export function createClassWithErrorHandling<T extends Constructor>(
|
44
|
-
BaseClass: T,
|
45
|
-
): T {
|
46
|
-
return new Proxy(BaseClass, {
|
47
|
-
construct(target: T, args: any[]): object {
|
48
|
-
const instance = new target(...args);
|
49
|
-
return new Proxy(instance, {
|
50
|
-
get(target: any, prop: string | symbol): any {
|
51
|
-
const value = target[prop];
|
52
|
-
if (typeof value === 'function') {
|
53
|
-
return function (this: any, ...args: any[]): any {
|
54
|
-
try {
|
55
|
-
const result = value.apply(this, args);
|
56
|
-
if (result instanceof Promise) {
|
57
|
-
return result.catch((error: Error) => {
|
58
|
-
logger.error(`Error in ${String(prop)}:`, error);
|
59
|
-
throw error; // 重新抛出错误,以便调用者可以捕获它
|
60
|
-
});
|
61
|
-
}
|
62
|
-
return result;
|
63
|
-
} catch (error) {
|
64
|
-
logger.error(`Error in ${String(prop)}:`, error);
|
65
|
-
throw error; // 重新抛出错误,以便调用者可以捕获它
|
66
|
-
}
|
67
|
-
};
|
68
|
-
}
|
69
|
-
return value;
|
70
|
-
},
|
71
|
-
set(target: any, prop: string | symbol, value: any): boolean {
|
72
|
-
if (typeof value === 'function') {
|
73
|
-
target[prop] = function (this: any, ...args: any[]): any {
|
74
|
-
try {
|
75
|
-
const result = value.apply(this, args);
|
76
|
-
if (result instanceof Promise) {
|
77
|
-
return result.catch((error: Error) => {
|
78
|
-
logger.error(`Error in ${String(prop)}:`, error);
|
79
|
-
throw error;
|
80
|
-
});
|
81
|
-
}
|
82
|
-
return result;
|
83
|
-
} catch (error) {
|
84
|
-
logger.error(`Error in ${String(prop)}:`, error);
|
85
|
-
throw error;
|
86
|
-
}
|
87
|
-
};
|
88
|
-
} else {
|
89
|
-
target[prop] = value;
|
90
|
-
}
|
91
|
-
return true;
|
92
|
-
},
|
93
|
-
});
|
94
|
-
},
|
95
|
-
});
|
30
|
+
// 检查 port 属性是否为空
|
31
|
+
// 注意:如果使用默认端口(如 HTTP 的 80 或 HTTPS 的 443),port 会是空字符串
|
32
|
+
return parsedUrl.port !== '';
|
96
33
|
}
|
97
34
|
|
98
35
|
export function formDataToString(formData: FormData): string {
|
@@ -120,7 +57,7 @@ export function typeReplacer(key: string, val: any) {
|
|
120
57
|
return val.toString();
|
121
58
|
} else if (val instanceof Function) {
|
122
59
|
return Function.prototype.toString.call(val);
|
123
|
-
} else if (val
|
60
|
+
} else if (typeof val === 'symbol') {
|
124
61
|
return val.toString();
|
125
62
|
} else if (typeof val === 'bigint') {
|
126
63
|
return val.toString();
|