@wutiange/log-listener-plugin 2.0.2-alpha.2 → 2.0.2-alpha.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/package.json +5 -5
- package/src/HTTPInterceptor.ts +17 -20
- package/src/Server.ts +51 -40
- 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
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@wutiange/log-listener-plugin",
|
3
|
-
"version": "2.0.2-alpha.
|
3
|
+
"version": "2.0.2-alpha.4",
|
4
4
|
"description": "log-record 客户端对应的的插件\r\nLog-record client corresponding plugin",
|
5
5
|
"source": "index.ts",
|
6
6
|
"react-native": "index.ts",
|
@@ -22,10 +22,10 @@
|
|
22
22
|
"scripts": {
|
23
23
|
"publish-alpha": "npm publish --access public --tag alpha",
|
24
24
|
"prepublishOnly": "npm run build",
|
25
|
-
"test": "jest",
|
26
|
-
"build": "yarn clean && yarn test && rollup -c",
|
27
|
-
"dev": "rollup -c -w",
|
28
|
-
"clean": "rimraf dist"
|
25
|
+
"test": "yarn dlx jest",
|
26
|
+
"build": "yarn clean && yarn test && yarn dlx rollup -c",
|
27
|
+
"dev": "yarn dlx rollup -c -w",
|
28
|
+
"clean": "yarn dlx rimraf dist"
|
29
29
|
},
|
30
30
|
"devDependencies": {
|
31
31
|
"@types/react-native-zeroconf": "^0.13.1",
|
package/src/HTTPInterceptor.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
|
2
2
|
import BlobFileReader from 'react-native/Libraries/Blob/FileReader';
|
3
|
-
import {Blob} from 'buffer';
|
4
|
-
import {
|
3
|
+
import { Blob } from 'buffer';
|
4
|
+
import { formDataToString } from './utils';
|
5
5
|
import logger from './logger';
|
6
6
|
|
7
7
|
type StartNetworkLoggingOptions = {
|
@@ -87,7 +87,7 @@ const getResponseBody = async (responseType: string, response: any) => {
|
|
87
87
|
}
|
88
88
|
return response ?? null;
|
89
89
|
} catch (error) {
|
90
|
-
logger.warn(
|
90
|
+
logger.warn('getResponseBody---error---', error);
|
91
91
|
return null;
|
92
92
|
}
|
93
93
|
};
|
@@ -153,7 +153,7 @@ class HTTPInterceptor {
|
|
153
153
|
await listener(data);
|
154
154
|
}
|
155
155
|
} catch (error: any) {
|
156
|
-
console.warn(`eventName=${eventName}, error=${error?.message}`)
|
156
|
+
console.warn(`eventName=${eventName}, error=${error?.message}`);
|
157
157
|
}
|
158
158
|
});
|
159
159
|
};
|
@@ -171,7 +171,7 @@ class HTTPInterceptor {
|
|
171
171
|
|
172
172
|
if (this.ignoredPatterns) {
|
173
173
|
if (
|
174
|
-
this.ignoredPatterns.some(pattern => pattern.test(`${method} ${url}`))
|
174
|
+
this.ignoredPatterns.some((pattern) => pattern.test(`${method} ${url}`))
|
175
175
|
) {
|
176
176
|
return;
|
177
177
|
}
|
@@ -257,19 +257,17 @@ class HTTPInterceptor {
|
|
257
257
|
};
|
258
258
|
|
259
259
|
setIgnoredUrls = (ignoredUrls: string[]) => {
|
260
|
-
if (
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
return;
|
269
|
-
}
|
270
|
-
this.ignoredUrls = new Set(ignoredUrls);
|
260
|
+
if (
|
261
|
+
!Array.isArray(ignoredUrls) ||
|
262
|
+
(ignoredUrls[0] && typeof ignoredUrls[0] !== 'string')
|
263
|
+
) {
|
264
|
+
console.warn(
|
265
|
+
'ignoredUrls must be an array of strings. The logger has not been started.',
|
266
|
+
);
|
267
|
+
return;
|
271
268
|
}
|
272
|
-
|
269
|
+
this.ignoredUrls = new Set(ignoredUrls);
|
270
|
+
};
|
273
271
|
|
274
272
|
enable = (options?: StartNetworkLoggingOptions) => {
|
275
273
|
try {
|
@@ -301,7 +299,7 @@ class HTTPInterceptor {
|
|
301
299
|
if (options?.ignoredPatterns) {
|
302
300
|
this.ignoredPatterns = options.ignoredPatterns;
|
303
301
|
}
|
304
|
-
this.setIgnoredUrls(options?.ignoredUrls ?? [])
|
302
|
+
this.setIgnoredUrls(options?.ignoredUrls ?? []);
|
305
303
|
XHRInterceptor.setOpenCallback(this.openHandle);
|
306
304
|
XHRInterceptor.setRequestHeaderCallback(this.requestHeaderHandle);
|
307
305
|
XHRInterceptor.setHeaderReceivedCallback(this.headerReceivedHandle);
|
@@ -330,8 +328,7 @@ class HTTPInterceptor {
|
|
330
328
|
};
|
331
329
|
}
|
332
330
|
|
333
|
-
const
|
334
|
-
const httpInterceptor = new SafeHTTPInterceptor();
|
331
|
+
const httpInterceptor = new HTTPInterceptor();
|
335
332
|
export {
|
336
333
|
type StartNetworkLoggingOptions,
|
337
334
|
httpInterceptor,
|
package/src/Server.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
import { hasPort, sleep, typeReplacer } from
|
2
|
-
import { getBaseData, LOG_KEY } from
|
3
|
-
import logger from
|
1
|
+
import { hasPort, sleep, typeReplacer } from './utils';
|
2
|
+
import { getBaseData, LOG_KEY } from './common';
|
3
|
+
import logger from './logger';
|
4
4
|
|
5
5
|
const DEFAULT_PORT = 27751;
|
6
6
|
class Server {
|
@@ -12,7 +12,7 @@ class Server {
|
|
12
12
|
private innerBaseData: Record<string, string> = {};
|
13
13
|
|
14
14
|
constructor(url?: string | Set<string>, timeout: number = 30000) {
|
15
|
-
if (typeof url ===
|
15
|
+
if (typeof url === 'string') {
|
16
16
|
this.updateUrl(url);
|
17
17
|
} else {
|
18
18
|
this.setBaseUrlArr(url ?? new Set());
|
@@ -22,21 +22,21 @@ class Server {
|
|
22
22
|
this.handleZeroConf();
|
23
23
|
}
|
24
24
|
|
25
|
-
addUrlsListener = (
|
26
|
-
onNewUrlCallback: (urls: Set<string>) => void
|
27
|
-
) => {
|
25
|
+
addUrlsListener = (onNewUrlCallback: (urls: Set<string>) => void) => {
|
28
26
|
this.urlsListener = onNewUrlCallback;
|
29
27
|
};
|
30
28
|
|
31
29
|
private requestJoin = async (url: string, token: string) => {
|
32
30
|
const response = await fetch(url, {
|
33
|
-
method:
|
31
|
+
method: 'POST',
|
34
32
|
headers: {
|
35
|
-
|
33
|
+
'Content-Type': 'application/json;charset=utf-8',
|
36
34
|
},
|
37
35
|
body: JSON.stringify({
|
38
36
|
token,
|
39
|
-
model:
|
37
|
+
model:
|
38
|
+
this.innerBaseData.Model ??
|
39
|
+
`${this.innerBaseData.systemName}v${this.innerBaseData.osVersion}`,
|
40
40
|
}),
|
41
41
|
});
|
42
42
|
if (response.status !== 200) {
|
@@ -46,17 +46,17 @@ class Server {
|
|
46
46
|
if (json.code !== 0) {
|
47
47
|
return false;
|
48
48
|
}
|
49
|
-
return true
|
50
|
-
}
|
49
|
+
return true;
|
50
|
+
};
|
51
51
|
|
52
52
|
private async handleZeroConf() {
|
53
53
|
try {
|
54
|
-
const ZeroConf: any = require(
|
54
|
+
const ZeroConf: any = require('react-native-zeroconf')?.default;
|
55
55
|
if (!ZeroConf) {
|
56
56
|
return;
|
57
57
|
}
|
58
|
-
const zeroConf: import(
|
59
|
-
zeroConf.on(
|
58
|
+
const zeroConf: import('react-native-zeroconf').default = new ZeroConf();
|
59
|
+
zeroConf.on('resolved', async (service) => {
|
60
60
|
try {
|
61
61
|
const { path, token } = service.txt ?? {};
|
62
62
|
const url = `http://${service.host}:${service.port}`;
|
@@ -67,31 +67,35 @@ class Server {
|
|
67
67
|
return;
|
68
68
|
}
|
69
69
|
this.baseUrlArr.add(url);
|
70
|
-
this.urlsObj.set(service.name, url)
|
70
|
+
this.urlsObj.set(service.name, url);
|
71
71
|
if (this.urlsListener) {
|
72
72
|
this.urlsListener(this.baseUrlArr);
|
73
73
|
}
|
74
74
|
} catch (error) {
|
75
|
-
logger.warn(LOG_KEY,
|
75
|
+
logger.warn(LOG_KEY, '加入日志系统失败---', error);
|
76
76
|
}
|
77
77
|
});
|
78
|
-
zeroConf.on(
|
78
|
+
zeroConf.on('remove', (name: string) => {
|
79
79
|
const currentUrl = this.urlsObj.get(name);
|
80
80
|
if (currentUrl === undefined) {
|
81
81
|
return;
|
82
82
|
}
|
83
|
-
this.baseUrlArr.delete(currentUrl)
|
84
|
-
this.urlsObj.delete(name)
|
83
|
+
this.baseUrlArr.delete(currentUrl);
|
84
|
+
this.urlsObj.delete(name);
|
85
85
|
if (this.urlsListener) {
|
86
86
|
this.urlsListener(this.baseUrlArr);
|
87
87
|
}
|
88
88
|
});
|
89
|
-
zeroConf.on(
|
90
|
-
logger.warn(LOG_KEY,
|
91
|
-
})
|
92
|
-
zeroConf.scan(
|
89
|
+
zeroConf.on('error', (err: any) => {
|
90
|
+
logger.warn(LOG_KEY, 'zeroconf出现错误', err);
|
91
|
+
});
|
92
|
+
zeroConf.scan('http', 'tcp');
|
93
93
|
} catch (error: any) {
|
94
|
-
logger.warn(
|
94
|
+
logger.warn(
|
95
|
+
LOG_KEY,
|
96
|
+
'zeroconf扫描或处理相关逻辑失败或者您根本就没有安装 react-native-zeroconf ,如果您没有安装,那么您将无法使用发现功能',
|
97
|
+
error,
|
98
|
+
);
|
95
99
|
}
|
96
100
|
}
|
97
101
|
|
@@ -101,18 +105,18 @@ class Server {
|
|
101
105
|
|
102
106
|
private send = async (
|
103
107
|
path: string,
|
104
|
-
data: Record<string, any
|
108
|
+
data: Record<string, any>,
|
105
109
|
): Promise<void> => {
|
106
110
|
const request = async (url: string, _data: Record<string, any>) => {
|
107
111
|
await Promise.race([
|
108
112
|
fetch(`${url}/${path}`, {
|
109
|
-
method:
|
113
|
+
method: 'POST',
|
110
114
|
headers: {
|
111
|
-
|
115
|
+
'Content-Type': 'application/json;charset=utf-8',
|
112
116
|
},
|
113
117
|
body: JSON.stringify(
|
114
118
|
{ ...this.innerBaseData, ...this.baseData, ..._data },
|
115
|
-
typeReplacer
|
119
|
+
typeReplacer,
|
116
120
|
),
|
117
121
|
}),
|
118
122
|
sleep(this.timeout, true),
|
@@ -123,30 +127,37 @@ class Server {
|
|
123
127
|
}
|
124
128
|
this.baseUrlArr.forEach(async (e) => {
|
125
129
|
try {
|
126
|
-
await request(e, data)
|
130
|
+
await request(e, data);
|
127
131
|
} catch (error: any) {
|
128
|
-
if (
|
129
|
-
|
132
|
+
if (
|
133
|
+
error?.message?.includes('Network request failed') ||
|
134
|
+
error?.message?.includes('Timeout')
|
135
|
+
) {
|
136
|
+
return;
|
130
137
|
}
|
131
|
-
logger.warn(LOG_KEY,
|
138
|
+
logger.warn(LOG_KEY, '上报日志失败', error);
|
132
139
|
}
|
133
|
-
})
|
140
|
+
});
|
134
141
|
};
|
135
142
|
|
136
143
|
updateUrl(url: string = '') {
|
137
|
-
const tempUrl = url.includes(
|
144
|
+
const tempUrl = url.includes('http') ? url : `http://${url}`;
|
138
145
|
if (!url) {
|
139
|
-
const currentUrl = this.urlsObj.get(
|
146
|
+
const currentUrl = this.urlsObj.get('Default');
|
140
147
|
if (!currentUrl) {
|
141
148
|
return;
|
142
149
|
}
|
143
150
|
this.baseUrlArr.delete(currentUrl);
|
144
|
-
this.urlsObj.delete(
|
151
|
+
this.urlsObj.delete('Default');
|
145
152
|
} else if (!hasPort(tempUrl)) {
|
146
153
|
this.updateUrl(`${tempUrl}:${DEFAULT_PORT}`);
|
147
154
|
} else {
|
155
|
+
const defaultUrl = this.urlsObj.get('Default');
|
156
|
+
if (defaultUrl) {
|
157
|
+
this.baseUrlArr.delete(defaultUrl);
|
158
|
+
}
|
148
159
|
this.baseUrlArr.add(tempUrl);
|
149
|
-
this.urlsObj.set(
|
160
|
+
this.urlsObj.set('Default', tempUrl);
|
150
161
|
}
|
151
162
|
}
|
152
163
|
|
@@ -163,11 +174,11 @@ class Server {
|
|
163
174
|
}
|
164
175
|
|
165
176
|
log = async (data: Record<string, any>) => {
|
166
|
-
return this.send(
|
177
|
+
return this.send('log', data);
|
167
178
|
};
|
168
179
|
|
169
180
|
network = async (data: Record<string, any>) => {
|
170
|
-
return this.send(
|
181
|
+
return this.send('network', data);
|
171
182
|
};
|
172
183
|
}
|
173
184
|
|
@@ -7,7 +7,6 @@ jest.mock('buffer', () => ({
|
|
7
7
|
Blob: jest.fn(),
|
8
8
|
}));
|
9
9
|
jest.mock('../utils', () => ({
|
10
|
-
createClassWithErrorHandling: jest.fn(Class => Class),
|
11
10
|
formDataToString: jest.fn(),
|
12
11
|
}));
|
13
12
|
|
@@ -28,7 +27,7 @@ class MockFormData {
|
|
28
27
|
|
29
28
|
class MockBlob {
|
30
29
|
private content: string;
|
31
|
-
type: any
|
30
|
+
type: any;
|
32
31
|
constructor(parts: any, options: any = {}) {
|
33
32
|
this.content = parts ? parts.join('') : '';
|
34
33
|
this.type = options.type || '';
|
@@ -165,127 +164,166 @@ describe('HTTPInterceptor', () => {
|
|
165
164
|
|
166
165
|
beforeEach(() => {
|
167
166
|
httpInterceptor.enable();
|
168
|
-
openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
167
|
+
openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
168
|
+
.calls[0][0];
|
169
|
+
requestHeaderCallback = (
|
170
|
+
XHRInterceptor.setRequestHeaderCallback as jest.Mock
|
171
|
+
).mock.calls[0][0];
|
172
|
+
headerReceivedCallback = (
|
173
|
+
XHRInterceptor.setHeaderReceivedCallback as jest.Mock
|
174
|
+
).mock.calls[0][0];
|
175
|
+
sendCallback = (XHRInterceptor.setSendCallback as jest.Mock).mock
|
176
|
+
.calls[0][0];
|
177
|
+
responseCallback = (XHRInterceptor.setResponseCallback as jest.Mock).mock
|
178
|
+
.calls[0][0];
|
173
179
|
});
|
174
180
|
|
175
181
|
it('should handle open event', () => {
|
176
182
|
const listener = jest.fn();
|
177
183
|
httpInterceptor.addListener('open', listener);
|
178
|
-
const xhr = {}
|
184
|
+
const xhr = {};
|
179
185
|
openCallback('GET', 'https://example.com', xhr);
|
180
|
-
expect(listener).toHaveBeenCalledWith(
|
181
|
-
|
182
|
-
|
183
|
-
|
186
|
+
expect(listener).toHaveBeenCalledWith(
|
187
|
+
expect.objectContaining({
|
188
|
+
method: 'GET',
|
189
|
+
url: 'https://example.com',
|
190
|
+
}),
|
191
|
+
);
|
184
192
|
});
|
185
193
|
|
186
194
|
it('should handle request header event', () => {
|
187
195
|
const listener = jest.fn();
|
188
196
|
httpInterceptor.addListener('requestHeader', listener);
|
189
|
-
const xhr = {}
|
197
|
+
const xhr = {};
|
190
198
|
openCallback('GET', 'https://example.com', xhr);
|
191
199
|
requestHeaderCallback('Content-Type', 'application/json', xhr);
|
192
|
-
expect(listener).toHaveBeenCalledWith(
|
193
|
-
|
194
|
-
|
200
|
+
expect(listener).toHaveBeenCalledWith(
|
201
|
+
expect.objectContaining({
|
202
|
+
requestHeaders: { 'Content-Type': 'application/json' },
|
203
|
+
}),
|
204
|
+
);
|
195
205
|
});
|
196
206
|
|
197
207
|
it('should handle header received event', () => {
|
198
208
|
const listener = jest.fn();
|
199
209
|
httpInterceptor.addListener('headerReceived', listener);
|
200
|
-
const xhr: {[key in string]: any} = {}
|
210
|
+
const xhr: { [key in string]: any } = {};
|
201
211
|
openCallback('GET', 'https://example.com', xhr);
|
202
|
-
xhr.responseHeaders = { 'Content-Type': 'application/json' }
|
212
|
+
xhr.responseHeaders = { 'Content-Type': 'application/json' };
|
203
213
|
headerReceivedCallback('application/json', 100, {}, xhr);
|
204
|
-
expect(listener).toHaveBeenCalledWith(
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
214
|
+
expect(listener).toHaveBeenCalledWith(
|
215
|
+
expect.objectContaining({
|
216
|
+
responseContentType: 'application/json',
|
217
|
+
responseSize: 100,
|
218
|
+
responseHeaders: { 'Content-Type': 'application/json' },
|
219
|
+
}),
|
220
|
+
);
|
209
221
|
});
|
210
222
|
|
211
223
|
it('should handle send event with JSON data', () => {
|
212
224
|
const listener = jest.fn();
|
213
225
|
httpInterceptor.addListener('send', listener);
|
214
|
-
const xhr = {}
|
226
|
+
const xhr = {};
|
215
227
|
openCallback('POST', 'https://example.com', xhr);
|
216
228
|
sendCallback(JSON.stringify({ key: 'value' }), xhr);
|
217
|
-
expect(listener).toHaveBeenCalledWith(
|
218
|
-
|
219
|
-
|
229
|
+
expect(listener).toHaveBeenCalledWith(
|
230
|
+
expect.objectContaining({
|
231
|
+
requestData: { key: 'value' },
|
232
|
+
}),
|
233
|
+
);
|
220
234
|
});
|
221
235
|
|
222
236
|
it('should handle send event with FormData', () => {
|
223
237
|
const listener = jest.fn();
|
224
238
|
httpInterceptor.addListener('send', listener);
|
225
|
-
const xhr = {}
|
239
|
+
const xhr = {};
|
226
240
|
openCallback('POST', 'https://example.com', xhr);
|
227
241
|
const formData = new FormData();
|
228
242
|
formData.append('key', 'value');
|
229
243
|
(formDataToString as jest.Mock).mockReturnValue('key=value');
|
230
244
|
sendCallback(formData, xhr);
|
231
|
-
expect(listener).toHaveBeenCalledWith(
|
232
|
-
|
233
|
-
|
245
|
+
expect(listener).toHaveBeenCalledWith(
|
246
|
+
expect.objectContaining({
|
247
|
+
requestData: 'key=value',
|
248
|
+
}),
|
249
|
+
);
|
234
250
|
});
|
235
251
|
|
236
252
|
it('should handle response event', async () => {
|
237
253
|
const listener = jest.fn();
|
238
254
|
httpInterceptor.addListener('response', listener);
|
239
|
-
const xhr = {}
|
255
|
+
const xhr = {};
|
240
256
|
openCallback('GET', 'https://example.com', xhr);
|
241
|
-
await responseCallback(
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
257
|
+
await responseCallback(
|
258
|
+
200,
|
259
|
+
1000,
|
260
|
+
{ data: 'response' },
|
261
|
+
'https://example.com',
|
262
|
+
'json',
|
263
|
+
xhr,
|
264
|
+
);
|
265
|
+
expect(listener).toHaveBeenCalledWith(
|
266
|
+
expect.objectContaining({
|
267
|
+
status: 200,
|
268
|
+
timeout: 1000,
|
269
|
+
responseData: { data: 'response' },
|
270
|
+
responseURL: 'https://example.com',
|
271
|
+
responseType: 'json',
|
272
|
+
}),
|
273
|
+
);
|
249
274
|
});
|
250
275
|
|
251
276
|
it('should handle response event with blob data', async () => {
|
252
277
|
const listener = jest.fn();
|
253
278
|
httpInterceptor.addListener('response', listener);
|
254
|
-
const xhr = {}
|
279
|
+
const xhr = {};
|
255
280
|
openCallback('GET', 'https://example.com', xhr);
|
256
281
|
const mockBlob = new MockBlob(['blob content']);
|
257
|
-
await responseCallback(
|
258
|
-
|
259
|
-
|
260
|
-
|
282
|
+
await responseCallback(
|
283
|
+
200,
|
284
|
+
1000,
|
285
|
+
mockBlob,
|
286
|
+
'https://example.com',
|
287
|
+
'blob',
|
288
|
+
xhr,
|
289
|
+
);
|
290
|
+
expect(listener).toHaveBeenCalledWith(
|
291
|
+
expect.objectContaining({
|
292
|
+
responseData: 'blob content',
|
293
|
+
}),
|
294
|
+
);
|
261
295
|
});
|
262
296
|
});
|
263
297
|
|
264
298
|
describe('error handling', () => {
|
265
299
|
beforeEach(() => {
|
266
300
|
httpInterceptor.enable();
|
267
|
-
})
|
301
|
+
});
|
268
302
|
it('should handle errors in listeners', async () => {
|
269
303
|
const errorListener = jest.fn(() => {
|
270
304
|
throw new Error('Listener error');
|
271
305
|
});
|
272
306
|
httpInterceptor.addListener('open', errorListener);
|
273
307
|
console.warn = jest.fn();
|
274
|
-
const xhr = {}
|
275
|
-
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
308
|
+
const xhr = {};
|
309
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
310
|
+
.calls[0][0];
|
276
311
|
openCallback('GET', 'https://example.com', xhr);
|
277
|
-
|
278
|
-
expect(console.warn).toHaveBeenCalledWith(
|
312
|
+
|
313
|
+
expect(console.warn).toHaveBeenCalledWith(
|
314
|
+
expect.stringContaining('Listener error'),
|
315
|
+
);
|
279
316
|
});
|
280
317
|
});
|
281
318
|
|
282
319
|
describe('ignored requests', () => {
|
283
320
|
it('should ignore requests to ignored hosts', () => {
|
284
321
|
httpInterceptor.enable({ ignoredHosts: ['ignored.com'] });
|
285
|
-
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
322
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
323
|
+
.calls[0][0];
|
286
324
|
const listener = jest.fn();
|
287
325
|
httpInterceptor.addListener('open', listener);
|
288
|
-
|
326
|
+
|
289
327
|
openCallback('GET', 'https://ignored.com', { uniqueId: '123' });
|
290
328
|
expect(listener).not.toHaveBeenCalled();
|
291
329
|
|
@@ -295,10 +333,11 @@ describe('HTTPInterceptor', () => {
|
|
295
333
|
|
296
334
|
it('should ignore requests to ignored URLs', () => {
|
297
335
|
httpInterceptor.enable({ ignoredUrls: ['https://example.com/ignored'] });
|
298
|
-
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
336
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
337
|
+
.calls[0][0];
|
299
338
|
const listener = jest.fn();
|
300
339
|
httpInterceptor.addListener('open', listener);
|
301
|
-
|
340
|
+
|
302
341
|
openCallback('GET', 'https://example.com/ignored', { uniqueId: '123' });
|
303
342
|
expect(listener).not.toHaveBeenCalled();
|
304
343
|
|
@@ -308,10 +347,11 @@ describe('HTTPInterceptor', () => {
|
|
308
347
|
|
309
348
|
it('should ignore requests matching ignored patterns', () => {
|
310
349
|
httpInterceptor.enable({ ignoredPatterns: [/^GET https:\/\/test\.com/] });
|
311
|
-
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
350
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
|
351
|
+
.calls[0][0];
|
312
352
|
const listener = jest.fn();
|
313
353
|
httpInterceptor.addListener('open', listener);
|
314
|
-
|
354
|
+
|
315
355
|
openCallback('GET', 'https://test.com/api', { uniqueId: '123' });
|
316
356
|
expect(listener).not.toHaveBeenCalled();
|
317
357
|
|
@@ -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();
|