@mzzsfy/dsh-turn-notify 0.10.0 → 0.11.1
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 +15 -8
- package/package.json +1 -1
- package/src/client.js +175 -6
- package/src/core.mjs +20 -20
- package/src/index.js +22 -22
- package/test/client-fold.test.mjs +405 -0
- package/test/core.test.mjs +3 -0
- package/test/flow.test.mjs +15 -5
- package/test/host-notify.test.mjs +96 -113
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 宿主桌面通知 BDD:core 纯函数(
|
|
1
|
+
// 宿主桌面通知 BDD:core 纯函数(派发决策 / 平台命令 / spawn 执行 / 配置键)
|
|
2
2
|
// 与 host 接线集成(stub ctx 装载真实 src/index.js,stub spawn 观测派发)。
|
|
3
3
|
import test from 'node:test'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
@@ -10,53 +10,32 @@ import {
|
|
|
10
10
|
HOST_NOTIFY_TIMEOUT_MS,
|
|
11
11
|
hostNotifyCommand,
|
|
12
12
|
hostNotifyWanted,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
isLoopbackAddress,
|
|
13
|
+
isBrowserPoll,
|
|
14
|
+
isBrowserPresent,
|
|
16
15
|
publicConfig,
|
|
17
16
|
resolvedConfig,
|
|
18
17
|
sendHostNotify,
|
|
19
18
|
validateConfigPatch,
|
|
20
19
|
} from '../src/core.mjs'
|
|
21
20
|
|
|
22
|
-
// ----
|
|
21
|
+
// ---- isBrowserPoll:在场记账形态收紧,防伪造在场压制宿主通知 ----
|
|
23
22
|
|
|
24
|
-
test('Given
|
|
25
|
-
assert.equal(
|
|
26
|
-
assert.equal(
|
|
27
|
-
assert.equal(isLoopbackAddress('127.8.8.8'), true)
|
|
28
|
-
assert.equal(isLoopbackAddress('::ffff:127.0.0.1'), true)
|
|
23
|
+
test('Given 合格轮询形态(任意来源加 cursor 加 fetch 形态或无 Sec-Fetch 头)When 判定 Then 为真', () => {
|
|
24
|
+
assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: 'cors' }), true)
|
|
25
|
+
assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: undefined }), true)
|
|
29
26
|
})
|
|
30
27
|
|
|
31
|
-
test('Given
|
|
32
|
-
assert.equal(
|
|
33
|
-
assert.equal(
|
|
34
|
-
assert.equal(isLoopbackAddress('::2'), false)
|
|
35
|
-
assert.equal(isLoopbackAddress(''), false)
|
|
36
|
-
assert.equal(isLoopbackAddress(undefined), false)
|
|
37
|
-
assert.equal(isLoopbackAddress(null), false)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// ---- isLocalBrowserPoll:在场记账形态收紧,防伪造在场压制宿主通知 ----
|
|
41
|
-
|
|
42
|
-
test('Given 回环加 cursor 加 fetch 形态或无 Sec-Fetch 头 When 判定 Then 为真', () => {
|
|
43
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: true, secFetchMode: 'cors' }), true)
|
|
44
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: '::1', hasCursor: true, secFetchMode: undefined }), true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('Given 非回环或无 cursor 或 no-cors 形态 When 判定 Then 为假', () => {
|
|
48
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: '192.168.1.5', hasCursor: true, secFetchMode: 'cors' }), false)
|
|
49
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: false, secFetchMode: 'cors' }), false)
|
|
50
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: true, secFetchMode: 'no-cors' }), false)
|
|
51
|
-
assert.equal(isLocalBrowserPoll({ remoteAddress: undefined, hasCursor: true, secFetchMode: 'cors' }), false)
|
|
28
|
+
test('Given 无 cursor 或 no-cors 形态 When 判定 Then 为假', () => {
|
|
29
|
+
assert.equal(isBrowserPoll({ hasCursor: false, secFetchMode: 'cors' }), false)
|
|
30
|
+
assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: 'no-cors' }), false)
|
|
52
31
|
})
|
|
53
32
|
|
|
54
33
|
test('Given Origin 缺席或与 Host 一致 When 判定 Then 为真;跨源或畸形 Origin Then 为假', () => {
|
|
55
|
-
const base = {
|
|
56
|
-
assert.equal(
|
|
57
|
-
assert.equal(
|
|
58
|
-
assert.equal(
|
|
59
|
-
assert.equal(
|
|
34
|
+
const base = { hasCursor: true, secFetchMode: 'cors' }
|
|
35
|
+
assert.equal(isBrowserPoll({ ...base, origin: undefined, host: '127.0.0.1:3080' }), true, '同源 GET fetch 不带 Origin')
|
|
36
|
+
assert.equal(isBrowserPoll({ ...base, origin: 'http://127.0.0.1:3080', host: '127.0.0.1:3080' }), true)
|
|
37
|
+
assert.equal(isBrowserPoll({ ...base, origin: 'https://evil.example', host: '127.0.0.1:3080' }), false, '跨源 fetch 必带且与 Host 不符')
|
|
38
|
+
assert.equal(isBrowserPoll({ ...base, origin: 'not a url', host: '127.0.0.1:3080' }), false, '畸形 Origin 不放行')
|
|
60
39
|
})
|
|
61
40
|
|
|
62
41
|
// ---- hostNotifyWanted:浏览器优先,在场让位、离场补位;重复只发生在边界情况 ----
|
|
@@ -64,36 +43,36 @@ test('Given Origin 缺席或与 Host 一致 When 判定 Then 为真;跨源或畸
|
|
|
64
43
|
const NOW = 1000 * 1000
|
|
65
44
|
const WINDOW_MS = 2 * 1000
|
|
66
45
|
|
|
67
|
-
test('Given
|
|
68
|
-
assert.equal(hostNotifyWanted({ hostNotify: true,
|
|
69
|
-
assert.equal(hostNotifyWanted({ hostNotifyFallback: true,
|
|
70
|
-
assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true,
|
|
46
|
+
test('Given 任一开关开且浏览器不在场 When 决策 Then 弹', () => {
|
|
47
|
+
assert.equal(hostNotifyWanted({ hostNotify: true, browserPresent: false }), true)
|
|
48
|
+
assert.equal(hostNotifyWanted({ hostNotifyFallback: true, browserPresent: false }), true)
|
|
49
|
+
assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, browserPresent: false }), true)
|
|
71
50
|
})
|
|
72
51
|
|
|
73
|
-
test('Given
|
|
74
|
-
assert.equal(hostNotifyWanted({ hostNotify: true,
|
|
75
|
-
assert.equal(hostNotifyWanted({ hostNotifyFallback: true,
|
|
76
|
-
assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true,
|
|
52
|
+
test('Given 任一开关开且浏览器在场 When 决策 Then 让位不弹(浏览器优先,正常路径零重复)', () => {
|
|
53
|
+
assert.equal(hostNotifyWanted({ hostNotify: true, browserPresent: true }), false)
|
|
54
|
+
assert.equal(hostNotifyWanted({ hostNotifyFallback: true, browserPresent: true }), false)
|
|
55
|
+
assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, browserPresent: true }), false)
|
|
77
56
|
})
|
|
78
57
|
|
|
79
58
|
test('Given 两开关均关 When 决策 Then 恒不弹', () => {
|
|
80
|
-
assert.equal(hostNotifyWanted({
|
|
81
|
-
assert.equal(hostNotifyWanted({ hostNotify: false, hostNotifyFallback: false,
|
|
59
|
+
assert.equal(hostNotifyWanted({ browserPresent: false }), false)
|
|
60
|
+
assert.equal(hostNotifyWanted({ hostNotify: false, hostNotifyFallback: false, browserPresent: false }), false)
|
|
82
61
|
})
|
|
83
62
|
|
|
84
|
-
// ----
|
|
63
|
+
// ---- isBrowserPresent:在场 = 有在途长轮询,或宽限窗口内有活动 ----
|
|
85
64
|
|
|
86
65
|
test('Given 在途长轮询存在 When 判定在场 Then 为真(时间戳过期不影响)', () => {
|
|
87
|
-
assert.equal(
|
|
88
|
-
assert.equal(
|
|
66
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 1, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), true)
|
|
67
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 3, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), true)
|
|
89
68
|
})
|
|
90
69
|
|
|
91
70
|
test('Given 无在途 When 判定在场 Then 最近活动在窗口内为真,超窗或从未为假', () => {
|
|
92
|
-
assert.equal(
|
|
93
|
-
assert.equal(
|
|
94
|
-
assert.equal(
|
|
95
|
-
assert.equal(
|
|
96
|
-
assert.equal(
|
|
71
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - 1, now: NOW, windowMs: WINDOW_MS }), true)
|
|
72
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS, now: NOW, windowMs: WINDOW_MS }), true)
|
|
73
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), false)
|
|
74
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), false)
|
|
75
|
+
assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: undefined, now: NOW, windowMs: WINDOW_MS }), false)
|
|
97
76
|
})
|
|
98
77
|
|
|
99
78
|
// ---- hostNotifyCommand:平台命令构造与文本转义 ----
|
|
@@ -209,7 +188,7 @@ test('Given 缺省配置 When 解析 Then 两键回 false;CHANNELS 含 host', ()
|
|
|
209
188
|
assert.ok(CHANNELS.indexOf('host') >= 0, 'host 应为可路由通道')
|
|
210
189
|
})
|
|
211
190
|
|
|
212
|
-
// ---- host 接线集成:stub spawn 观测派发,presence
|
|
191
|
+
// ---- host 接线集成:stub spawn 观测派发,presence 经投影路由的请求形态注入 ----
|
|
213
192
|
|
|
214
193
|
// res 桩带事件能力:投影处理器挂 close 事件感知断连,桩须支持 once/emit;
|
|
215
194
|
// end 不自动触发 close,断连时机由测试显式 emit 模拟
|
|
@@ -272,30 +251,31 @@ async function postConfig(routes, patch) {
|
|
|
272
251
|
return res
|
|
273
252
|
}
|
|
274
253
|
|
|
275
|
-
async function pollProjection(routes
|
|
254
|
+
async function pollProjection(routes) {
|
|
276
255
|
const req = makeReq('GET')
|
|
277
256
|
req.url = '/api/turn-notify/projection'
|
|
278
|
-
if (remoteAddress !== undefined) req.socket = { remoteAddress }
|
|
279
257
|
const res = makeRes()
|
|
280
258
|
await routes.get('/api/turn-notify/projection')(req, res)
|
|
281
259
|
assert.equal(res.status, 200)
|
|
282
260
|
return res
|
|
283
261
|
}
|
|
284
262
|
|
|
285
|
-
//
|
|
286
|
-
// cursor
|
|
263
|
+
// 浏览器长轮询记账形态:任意来源 + 带续传 cursor + fetch 形态头;
|
|
264
|
+
// cursor 取超前值立即返回,不挂起。跨机地址为主场景(真实部署拓扑:
|
|
265
|
+
// 浏览器在局域网另一台机器访问 dsh),回环为同机特例
|
|
287
266
|
const FUTURE_CURSOR = 2 ** 40
|
|
288
|
-
|
|
267
|
+
const REMOTE_BROWSER = '10.123.90.14'
|
|
268
|
+
async function browserPoll(routes) {
|
|
289
269
|
const req = makeReq('GET', undefined, { 'sec-fetch-mode': 'cors' })
|
|
290
270
|
req.url = '/api/turn-notify/projection?cursor=' + FUTURE_CURSOR
|
|
291
|
-
req.socket = { remoteAddress }
|
|
271
|
+
req.socket = { remoteAddress: REMOTE_BROWSER }
|
|
292
272
|
const res = makeRes()
|
|
293
273
|
await routes.get('/api/turn-notify/projection')(req, res)
|
|
294
274
|
assert.equal(res.status, 200)
|
|
295
275
|
return res
|
|
296
276
|
}
|
|
297
277
|
|
|
298
|
-
//
|
|
278
|
+
// 伪形态轮询:缺 cursor / no-cors / 跨源 Origin,记账须拒绝(与来源地址无关)
|
|
299
279
|
async function rawShapePoll(routes, remoteAddress, secFetchMode, withCursor, origin) {
|
|
300
280
|
const headers = secFetchMode === undefined ? {} : { 'sec-fetch-mode': secFetchMode }
|
|
301
281
|
if (origin !== undefined) headers.origin = origin
|
|
@@ -330,9 +310,11 @@ async function emitCompletedTurn(handlers) {
|
|
|
330
310
|
}
|
|
331
311
|
|
|
332
312
|
// 挂起中的长轮询:cursor 等于当前版本(配置 POST 后为 1),服务端挂起等待,
|
|
333
|
-
// 模拟真实客户端的在途请求;返回 pending 承诺与 res 桩(供断连模拟与唤醒等待)
|
|
334
|
-
|
|
335
|
-
|
|
313
|
+
// 模拟真实客户端的在途请求;返回 pending 承诺与 res 桩(供断连模拟与唤醒等待);
|
|
314
|
+
// secFetchMode 缺省 cors,传 undefined 模拟不发 Sec-Fetch 头的旧浏览器
|
|
315
|
+
function hangingPoll(routes, remoteAddress, secFetchMode) {
|
|
316
|
+
const headers = secFetchMode === undefined ? {} : { 'sec-fetch-mode': secFetchMode }
|
|
317
|
+
const req = makeReq('GET', undefined, headers)
|
|
336
318
|
req.url = '/api/turn-notify/projection?cursor=1'
|
|
337
319
|
req.socket = { remoteAddress }
|
|
338
320
|
const res = makeRes()
|
|
@@ -340,39 +322,53 @@ function hangingPoll(routes, remoteAddress) {
|
|
|
340
322
|
return { pending, res }
|
|
341
323
|
}
|
|
342
324
|
|
|
343
|
-
test('Given
|
|
325
|
+
test('Given 旧浏览器形态(无 Sec-Fetch 头)跨机长轮询在途 When 回合完成 Then 宿主让位', async () => {
|
|
326
|
+
await withStubSpawn(async (spawns) => {
|
|
327
|
+
const { ctx, routes, handlers } = makeCtx()
|
|
328
|
+
apply(ctx)
|
|
329
|
+
await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
|
|
330
|
+
const { pending } = hangingPoll(routes, REMOTE_BROWSER, undefined)
|
|
331
|
+
await flushMicrotasks()
|
|
332
|
+
await emitCompletedTurn(handlers)
|
|
333
|
+
assert.equal(spawns.length, 0, '无 Sec-Fetch 头的同源续传轮询同样入账,宿主让位')
|
|
334
|
+
await pending
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('Given 总开关开且跨机长轮询在途 When 回合完成 Then 宿主让位;离场超窗后接管', async () => {
|
|
344
339
|
await withStubSpawn(async (spawns) => {
|
|
345
340
|
const { ctx, routes, handlers } = makeCtx()
|
|
346
341
|
apply(ctx)
|
|
347
342
|
const saved = await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
|
|
348
343
|
assert.equal(saved.body.hostNotify, true)
|
|
349
|
-
const { pending, res } = hangingPoll(routes,
|
|
344
|
+
const { pending, res } = hangingPoll(routes, REMOTE_BROWSER)
|
|
350
345
|
await flushMicrotasks()
|
|
351
|
-
//
|
|
346
|
+
// 在途请求即在场信号:浏览器优先,宿主让位,正常路径零重复;
|
|
347
|
+
// 浏览器在局域网另一台机器访问时同样让位
|
|
352
348
|
await emitCompletedTurn(handlers)
|
|
353
|
-
assert.equal(spawns.length, 0, '
|
|
349
|
+
assert.equal(spawns.length, 0, '跨机浏览器长轮询在途,宿主让位')
|
|
354
350
|
// 在途请求被回合事件唤醒完成,宽限窗口内仍算在场
|
|
355
351
|
await pending
|
|
356
352
|
await emitCompletedTurn(handlers)
|
|
357
353
|
assert.equal(spawns.length, 0, '长轮询刚完成,宽限窗口内仍由浏览器呈现')
|
|
358
|
-
//
|
|
354
|
+
// 推进时钟越过宽限窗口:浏览器视为离场,宿主接管(宁可重复不可漏)
|
|
359
355
|
const realNow = Date.now
|
|
360
356
|
Date.now = () => realNow() + CLIENT_PRESENCE_WINDOW_MS + 1
|
|
361
357
|
try {
|
|
362
358
|
await emitCompletedTurn(handlers)
|
|
363
359
|
} finally { Date.now = realNow }
|
|
364
360
|
assert.equal(spawns.length, 1, '离场超窗,宿主接管')
|
|
365
|
-
const projection = await pollProjection(routes
|
|
361
|
+
const projection = await pollProjection(routes)
|
|
366
362
|
assert.equal(projection.body.units.length, 3, '浏览器呈现通道不受让位影响')
|
|
367
363
|
})
|
|
368
364
|
})
|
|
369
365
|
|
|
370
|
-
test('Given
|
|
366
|
+
test('Given 总开关开且长轮询断连 When 回合完成 Then 认可窗口内让位,超窗接管', async () => {
|
|
371
367
|
await withStubSpawn(async (spawns) => {
|
|
372
368
|
const { ctx, routes, handlers } = makeCtx()
|
|
373
369
|
apply(ctx)
|
|
374
370
|
await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
|
|
375
|
-
const { pending, res } = hangingPoll(routes,
|
|
371
|
+
const { pending, res } = hangingPoll(routes, REMOTE_BROWSER)
|
|
376
372
|
await flushMicrotasks()
|
|
377
373
|
// 浏览器关闭中止请求:close 事件即时出账,最近活动定格于断连时刻
|
|
378
374
|
res.emit('close')
|
|
@@ -401,7 +397,7 @@ test('Given 记账轮询正常完成后再触发 close When 时钟推进越过
|
|
|
401
397
|
let fake = 1000 * 1000
|
|
402
398
|
Date.now = () => fake
|
|
403
399
|
try {
|
|
404
|
-
const first = await browserPoll(routes
|
|
400
|
+
const first = await browserPoll(routes)
|
|
405
401
|
// 生产正常完成路径:finally 已出账,响应完成后的 close 再触发须被幂等消化;
|
|
406
402
|
// 若双出账致计数走负,时钟推进后本应让位的在途轮询将误弹
|
|
407
403
|
first.emit('close')
|
|
@@ -427,8 +423,8 @@ test('Given 双 tab 并发挂起其一断连 When 时钟推进越过认可窗 Th
|
|
|
427
423
|
let fake = 1000 * 1000
|
|
428
424
|
Date.now = () => fake
|
|
429
425
|
try {
|
|
430
|
-
const tabA = hangingPoll(routes,
|
|
431
|
-
const tabB = hangingPoll(routes, '
|
|
426
|
+
const tabA = hangingPoll(routes, REMOTE_BROWSER)
|
|
427
|
+
const tabB = hangingPoll(routes, '127.0.0.1')
|
|
432
428
|
await flushMicrotasks()
|
|
433
429
|
// tabA 断连即时出账,tabB 独力承载在途信号
|
|
434
430
|
tabA.res.emit('close')
|
|
@@ -445,15 +441,23 @@ test('Given 双 tab 并发挂起其一断连 When 时钟推进越过认可窗 Th
|
|
|
445
441
|
})
|
|
446
442
|
})
|
|
447
443
|
|
|
448
|
-
test('Given
|
|
444
|
+
test('Given 回退开关开 When 跨机长轮询在途 Then 不弹;无浏览器在场 Then 弹', async () => {
|
|
449
445
|
await withStubSpawn(async (spawns) => {
|
|
450
|
-
const
|
|
451
|
-
apply(ctx)
|
|
452
|
-
await postConfig(routes, {
|
|
453
|
-
|
|
454
|
-
await
|
|
455
|
-
|
|
456
|
-
assert.
|
|
446
|
+
const local = makeCtx()
|
|
447
|
+
apply(local.ctx)
|
|
448
|
+
await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
449
|
+
const localPoll = hangingPoll(local.routes, REMOTE_BROWSER)
|
|
450
|
+
await flushMicrotasks()
|
|
451
|
+
await emitCompletedTurn(local.handlers)
|
|
452
|
+
assert.equal(spawns.length, 0, '跨机浏览器在途,回退不触发')
|
|
453
|
+
await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
454
|
+
await localPoll.pending
|
|
455
|
+
// 独立实例:从未有浏览器轮询
|
|
456
|
+
const remote = makeCtx()
|
|
457
|
+
apply(remote.ctx)
|
|
458
|
+
await postConfig(remote.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
459
|
+
await emitCompletedTurn(remote.handlers)
|
|
460
|
+
assert.equal(spawns.length, 1, '无浏览器在场,回退触发')
|
|
457
461
|
})
|
|
458
462
|
})
|
|
459
463
|
|
|
@@ -477,40 +481,19 @@ test('Given kindRoutes 名单不含 host When 总开关开 Then 不弹', async (
|
|
|
477
481
|
})
|
|
478
482
|
})
|
|
479
483
|
|
|
480
|
-
test('Given 回退开关开 When 本机长轮询在途 Then 不弹;仅远程在线 Then 弹', async () => {
|
|
481
|
-
await withStubSpawn(async (spawns) => {
|
|
482
|
-
const local = makeCtx()
|
|
483
|
-
apply(local.ctx)
|
|
484
|
-
await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
485
|
-
const localPoll = hangingPoll(local.routes, '::1')
|
|
486
|
-
await flushMicrotasks()
|
|
487
|
-
await emitCompletedTurn(local.handlers)
|
|
488
|
-
assert.equal(spawns.length, 0, '本机浏览器在途,回退不触发')
|
|
489
|
-
await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
490
|
-
await localPoll.pending
|
|
491
|
-
// 独立实例:本机从未有浏览器,仅远程浏览器轮询过
|
|
492
|
-
const remote = makeCtx()
|
|
493
|
-
apply(remote.ctx)
|
|
494
|
-
await postConfig(remote.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
495
|
-
await browserPoll(remote.routes, '10.0.0.8')
|
|
496
|
-
await emitCompletedTurn(remote.handlers)
|
|
497
|
-
assert.equal(spawns.length, 1, '本机无浏览器,回退触发')
|
|
498
|
-
})
|
|
499
|
-
})
|
|
500
|
-
|
|
501
484
|
test('Given 裸 GET 形态(无 cursor 或 no-cors 或跨源)When 回合完成 Then 回退不触发(伪造在场不入账)', async () => {
|
|
502
485
|
await withStubSpawn(async (spawns) => {
|
|
503
486
|
const { ctx, routes, handlers } = makeCtx()
|
|
504
487
|
apply(ctx)
|
|
505
488
|
await postConfig(routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
|
|
506
|
-
//
|
|
507
|
-
await rawShapePoll(routes,
|
|
508
|
-
//
|
|
509
|
-
await rawShapePoll(routes,
|
|
510
|
-
//
|
|
511
|
-
await rawShapePoll(routes,
|
|
489
|
+
// 浏览器来源但缺 cursor:手工探测形态
|
|
490
|
+
await rawShapePoll(routes, REMOTE_BROWSER, 'cors', false)
|
|
491
|
+
// 浏览器来源带 cursor 但 no-cors:dsh 同源网页的 img 探活形态
|
|
492
|
+
await rawShapePoll(routes, REMOTE_BROWSER, 'no-cors', true)
|
|
493
|
+
// 浏览器来源带 cursor 且 cors 但跨源 Origin:任意机器上恶意网页的标准 fetch 形态
|
|
494
|
+
await rawShapePoll(routes, REMOTE_BROWSER, 'cors', true, 'https://evil.example')
|
|
512
495
|
await emitCompletedTurn(handlers)
|
|
513
|
-
assert.equal(spawns.length, 1, '
|
|
496
|
+
assert.equal(spawns.length, 1, '伪造在场不入账,无真实浏览器时回退照常触发')
|
|
514
497
|
})
|
|
515
498
|
})
|
|
516
499
|
|
|
@@ -529,7 +512,7 @@ test('Given spawn 失败 When 回合完成 Then 无未处理拒绝且投影照
|
|
|
529
512
|
await flushMicrotasks()
|
|
530
513
|
await flushMicrotasks()
|
|
531
514
|
assert.equal(rejections.length, 0)
|
|
532
|
-
const projection = await pollProjection(routes
|
|
515
|
+
const projection = await pollProjection(routes)
|
|
533
516
|
assert.equal(projection.body.units.length, 1)
|
|
534
517
|
} finally {
|
|
535
518
|
process.off('unhandledRejection', onUnhandled)
|