Files
dsh_shenxian/test/overlay-device-endpoint.test.mjs
T

285 lines
13 KiB
JavaScript
Raw Normal View History

/**
* 手机接入线 · **设备级取址**单测(S2)—— 纯函数,不连网、不起进程。
* 运行:`node --test test/overlay-device-endpoint.test.mjs`(已含进 `npm test` / `npm verify`)。
*
* ## 这个测试的职责
* S2 的验收标准有两条,都必须**机器可判**:
* 1. **解析口径与改造前逐条等价** —— `indexRelayStatus` 的容忍面(丢非法行、丢 `localPort<=0`、
* `network` 缺失按 `ops`)此前活在 `web/server.ts` 的内联循环里,抽出来后不得有语义漂移;
* 2. 🔴 **失败必须关闭** —— 查不到 / 离线 / 多端口,**⛔ 不许**返回任何"看起来像地址"的东西
* (回落 = 把请求打到控制面本机同号端口上,与 `reachability.ts` P0-3 同族的静默写坏)。
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
deviceWebDecision,
indexRelayStatus,
listDeviceEndpoints,
relayEndpointKey,
resolveDeviceReachability,
deviceWebSwitchOn,
} from '../lib/net/relay-endpoints.js'
import { parseDevicePath } from '../lib/web/routes/device-web.js'
import { VIA_RELAY } from '../lib/net/reachability.js'
/** 造一份 relay `/status` 响应体(只带本测试用得到的字段)。 */
function status(endpoints) {
return { endpoints }
}
test('indexRelayStatus:丢非法行、丢 localPort<=0、network 缺失按 ops', () => {
const index = indexRelayStatus(
status([
{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true },
{ hostId: 'd-b', network: 'u:5', port: 20101, localPort: 0, online: true }, // 纯流转发 / 未绑定
{ hostId: 'd-c', port: 20102, localPort: 42002, online: true }, // 无 network ⇒ ops
{ hostId: 42, network: 'u:5', port: 20103, localPort: 42003, online: true }, // hostId 非法
{ hostId: 'd-d', network: 'u:5', port: '20104', localPort: 42004, online: true }, // port 非法
]),
)
assert.deepEqual([...index.keys()].sort(), [relayEndpointKey('ops', 'd-c', 20102), relayEndpointKey('u:5', 'd-a', 20100)])
assert.deepEqual(index.get(relayEndpointKey('u:5', 'd-a', 20100)), { localPort: 42000, online: true })
})
test('indexRelayStatus:body 形状异常 ⇒ 空索引(不抛)', () => {
assert.equal(indexRelayStatus(undefined).size, 0)
assert.equal(indexRelayStatus({}).size, 0)
assert.equal(indexRelayStatus({ endpoints: 'nope' }).size, 0)
})
test('listDeviceEndpoints:只列本设备、按端口升序、不误命中前缀更长的 hostId', () => {
const index = indexRelayStatus(
status([
{ hostId: 'd-1', network: 'u:5', port: 20101, localPort: 42001, online: true },
{ hostId: 'd-1', network: 'u:5', port: 20100, localPort: 42000, online: true },
{ hostId: 'd-10', network: 'u:5', port: 20100, localPort: 43000, online: true }, // ⛔ 不是 d-1 的
{ hostId: 'd-1', network: 'u:9', port: 20100, localPort: 44000, online: true }, // ⛔ 别的租户网
]),
)
const got = listDeviceEndpoints(index, 'u:5', 'd-1')
assert.deepEqual(got.map((e) => [e.port, e.localPort]), [
[20100, 42000],
[20101, 42001],
])
})
test('resolveDeviceReachability:单端口在线 ⇒ via=relay 的落点可达性', () => {
const index = indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true }]))
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a' })
assert.equal(res.ok, true)
assert.equal(res.port, 20100)
assert.equal(res.localPort, 42000)
assert.deepEqual(res.reachability, {
hostId: 'd-a',
networkId: 'u:5',
via: VIA_RELAY,
address: '127.0.0.1:42000',
scheme: 'http',
})
})
test('跨租户结构上不可达:查 u:9 命中不到 u:5 的设备', () => {
const index = indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true }]))
const res = resolveDeviceReachability(index, { network: 'u:9', hostId: 'd-a' })
assert.equal(res.ok, false)
assert.equal(res.reason, 'device-no-endpoints')
assert.equal('reachability' in res, false) // 🔴 失败关闭:不许带地址出门
})
test('设备一个端口都没声明 ⇒ device-no-endpoints(且不带地址)', () => {
const res = resolveDeviceReachability(indexRelayStatus(status([])), { network: 'u:5', hostId: 'd-a' })
assert.equal(res.ok, false)
assert.equal(res.reason, 'device-no-endpoints')
assert.equal('reachability' in res, false)
})
test('声明的端口全部离线 ⇒ device-offline', () => {
const index = indexRelayStatus(
status([
{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: false },
{ hostId: 'd-a', network: 'u:5', port: 20101, localPort: 42001, online: false },
]),
)
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a' })
assert.equal(res.ok, false)
assert.equal(res.reason, 'device-offline')
assert.match(res.detail, /20100,20101/u)
})
test('多个在线端口且未指定 ⇒ ambiguous-device-ports(⛔ 不猜)', () => {
const index = indexRelayStatus(
status([
{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true },
{ hostId: 'd-a', network: 'u:5', port: 20101, localPort: 42001, online: true },
]),
)
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a' })
assert.equal(res.ok, false)
assert.equal(res.reason, 'ambiguous-device-ports')
})
test('指定端口:存在且在线的那个能被选中(多端口下唯一可用形态)', () => {
const index = indexRelayStatus(
status([
{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: false },
{ hostId: 'd-a', network: 'u:5', port: 20101, localPort: 42001, online: true },
]),
)
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a', preferredPort: 20101 })
assert.equal(res.ok, true)
assert.equal(res.reachability.address, '127.0.0.1:42001')
})
test('指定端口:设备没声明过 ⇒ port-not-declared(列出已声明端口)', () => {
const index = indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true }]))
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a', preferredPort: 20999 })
assert.equal(res.ok, false)
assert.equal(res.reason, 'port-not-declared')
assert.match(res.detail, /20100/u)
})
test('指定端口:存在但离线 ⇒ device-offline(⛔ 不因为"端口在"就放行)', () => {
const index = indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: false }]))
const res = resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a', preferredPort: 20100 })
assert.equal(res.ok, false)
assert.equal(res.reason, 'device-offline')
})
// ── S3:设备 Web 入口的准入判据(纯函数)──────────────────────────────────────
/**
* ⚠️ **2026-09-27(S5)对本节用例的改动**:S5 在平台闸之后加了**用户自己的开关**那一道
* (`userEnabled`,缺省 = 关)。下面 ②~⑤ 与"全过"这几条测的是**台账 / 取址**那几道闸,
* 因此必须**显式**把用户开关写成 `true` —— 否则它们会在第一道闸就返回 `user-switch-off`
* (这正是"漏传不许默认放行"的失败方向,⛔ 不是 bug)。
* 🔴 用户开关自己的判据 ⇒ `test/device-web-switch.test.mjs`(含先红后绿夹具 + 独立进程急停腿)。
*/
/** 造一个"设备在线且已声明端口"的取址结果。 */
function reachableDevice() {
const index = indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: true }]))
return resolveDeviceReachability(index, { network: 'u:5', hostId: 'd-a' })
}
test('deviceWebDecision:① 开关未开 ⇒ switch-off(默认关闭的第一道闸,且不带地址)', () => {
const d = deviceWebDecision({
enabled: false,
ledger: { status: 'active', kind: 'desktop' },
hostId: 'd-a',
resolved: reachableDevice(),
})
assert.equal(d.ok, false)
assert.equal(d.reason, 'switch-off')
assert.equal('address' in d, false)
})
test('deviceWebDecision:② 台账查无此设备 ⇒ device-unknown', () => {
const d = deviceWebDecision({ enabled: true,
userEnabled: true, ledger: undefined, hostId: 'd-a', resolved: reachableDevice() })
assert.equal(d.ok, false)
assert.equal(d.reason, 'device-unknown')
})
test('deviceWebDecision:③ 停发(status=revoked)⇒ device-revoked,⛔ 与"到不了"分开', () => {
const d = deviceWebDecision({
enabled: true,
userEnabled: true,
ledger: { status: 'revoked', kind: 'desktop' },
hostId: 'd-a',
resolved: reachableDevice(),
})
assert.equal(d.ok, false)
assert.equal(d.reason, 'device-revoked')
})
test('deviceWebDecision:④ kind=instance ⇒ device-not-desktop(平台代签的实例条目不走这条路)', () => {
const d = deviceWebDecision({
enabled: true,
userEnabled: true,
ledger: { status: 'active', kind: 'instance' },
hostId: 'd-a',
resolved: reachableDevice(),
})
assert.equal(d.ok, false)
assert.equal(d.reason, 'device-not-desktop')
})
test('deviceWebDecision:⑤ 台账通过但取址失败 ⇒ device-unreachable(带具名 detail,⛔ 不带地址)', () => {
const offline = resolveDeviceReachability(
indexRelayStatus(status([{ hostId: 'd-a', network: 'u:5', port: 20100, localPort: 42000, online: false }])),
{ network: 'u:5', hostId: 'd-a' },
)
const d = deviceWebDecision({
enabled: true,
userEnabled: true,
ledger: { status: 'active', kind: 'desktop' },
hostId: 'd-a',
resolved: offline,
})
assert.equal(d.ok, false)
assert.equal(d.reason, 'device-unreachable')
assert.match(d.detail, /离线/u)
assert.equal('address' in d, false)
})
test('deviceWebDecision:四闸全过 ⇒ ok,地址 = 解析出的落点', () => {
const d = deviceWebDecision({
enabled: true,
userEnabled: true,
ledger: { status: 'active', kind: 'desktop' },
hostId: 'd-a',
resolved: reachableDevice(),
})
assert.equal(d.ok, true)
assert.equal(d.address, '127.0.0.1:42000')
assert.equal(d.port, 42000) // 落点口(连的就是它)
assert.equal(d.declaredPort, 20100) // 设备声明的口(只作诊断)
})
// ── S3:开关读法 与 路径解析(纯函数)────────────────────────────────────────
test('deviceWebSwitchOn:语义已改为「平台没强制关」(S5)—— 只有字面量 0 才是关', () => {
// 🔴 2026-09-27 改口径:原语义「只有 1 才算开,其余当关」已作废。
// 现在 = 运维急停读法:显式 0 ⇒ false(强制关所有人);**其余一切**(含未设)⇒ true
// (平台不拦,交给用户自己的开关 —— 那一道的判据在 device-web-switch.test.mjs)。
assert.equal(deviceWebSwitchOn({ DSHS_DEVICE_WEB: '0' }), false)
assert.equal(deviceWebSwitchOn({ DSHS_DEVICE_WEB: ' 0 ' }), false)
for (const v of ['1', 'true', 'yes', 'on', '', undefined]) {
assert.equal(deviceWebSwitchOn({ DSHS_DEVICE_WEB: v }), true, String(v))
}
assert.equal(deviceWebSwitchOn({}), true)
})
test('deviceWebDecision:ok 形态带 host(= relay 回环主机名)与 port,供代理腿直接用', () => {
const d = deviceWebDecision({
enabled: true,
userEnabled: true,
ledger: { status: 'active', kind: 'desktop' },
hostId: 'd-a',
resolved: reachableDevice(),
})
assert.equal(d.ok, true)
assert.equal(d.host, '127.0.0.1')
assert.equal(d.port, 42000) // 落点口(连的就是它)
assert.equal(d.declaredPort, 20100) // 设备声明的口(只作诊断)
})
test('parseDevicePath:命中 /u/<uid>/desk/<hostId>/… ⇒ 拆出 rest(含 query)与前缀', () => {
const m = parseDevicePath('/u/u1/desk/d-1-abc/assets/app.js?rev=2')
assert.deepEqual(m, {
userId: 'u1',
hostId: 'd-1-abc',
prefix: '/u/u1/desk/d-1-abc',
rest: '/assets/app.js?rev=2',
})
})
test('parseDevicePath:根路径与无子路径两种边界都要能转发(rest 回落 /)', () => {
assert.equal(parseDevicePath('/u/u1/desk/d-1')?.rest, '/')
assert.equal(parseDevicePath('/u/u1/desk/d-1/')?.rest, '/')
})
test('parseDevicePath:⛔ 非设备入口一律 undefined(这是"关着时零影响"的判据)', () => {
for (const url of ['/', '/login.html', '/u/u1/dsh/x', '/api/overlay/my-devices', '/u/u1/desk/', '/u//desk/d-1']) {
assert.equal(parseDevicePath(url), undefined, url)
}
})