@mrrisega/dsh-remote 0.7.4-beta.1 → 0.7.5-beta.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/clients/dsh-remote/dsh-bridge.mjs +540 -49
- package/clients/dsh-remote/e2ee-client.mjs +251 -0
- package/clients/dsh-remote/mobile-adapter.mjs +261 -0
- package/clients/dsh-remote/test/bridge-control-priority.test.mjs +206 -0
- package/clients/dsh-remote/test/bridge-heartbeat-adaptive.test.mjs +110 -0
- package/clients/dsh-remote/test/bridge-upstream-refresh.test.mjs +26 -8
- package/clients/dsh-remote/test/bridge-ws-open-fast-ack.test.mjs +213 -0
- package/clients/dsh-remote/test/e2ee-session-store.test.mjs +254 -0
- package/clients/dsh-remote/test/mobile-adapter-single-page.test.mjs +328 -0
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +195 -0
- package/packages/dsh-remote-web/lib/index.js +145 -0
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/community-invite-proxy.test.mjs +343 -0
- package/packages/dsh-remote-web/test/community-invite-ui.test.mjs +375 -0
- package/packages/dsh-remote-web/test/purchase-agreement-proxy.test.mjs +198 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +323 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// 🎉 企微交流群引导(使用满 2 小时弹一次)—— **宿主半边**(lib/index.js)代理路由回归。
|
|
2
|
+
//
|
|
3
|
+
// 背景(业主 2026-10-23 需求):用户实际使用满 2 小时后要「加入企业微信群」引导,
|
|
4
|
+
// 每用户全局只弹一次;手机 Web 端已实现,桌面端插件面板复用**同一套企业端接口**:
|
|
5
|
+
// GET /api/community-invite/status → { ok, show, reason, used_seconds, required_seconds, qr_url }
|
|
6
|
+
// POST /api/community-invite/shown → { ok:true }(幂等)
|
|
7
|
+
//
|
|
8
|
+
// 桌面端浏览器半没有企业端用户 token 直连,所以本文件锁 node 半两条代理路由的契约:
|
|
9
|
+
// GET /dsh-remote/community-invite/status → 上游 GET {api}/api/community-invite/status
|
|
10
|
+
// POST /dsh-remote/community-invite/shown → 上游 POST {api}/api/community-invite/shown
|
|
11
|
+
//
|
|
12
|
+
// 最要紧的四件(都是"写错就出事故"的点):
|
|
13
|
+
// ① 身份只认本机 device-login token,**不透传浏览器带来的 Authorization**;
|
|
14
|
+
// ② 判定/记账全在服务端:show/reason/时长字段原样透传,node 半**不自己算**;
|
|
15
|
+
// ③ qr_url 归一化成企业端绝对地址(相对路径在 dsh web 源上会解析到 127.0.0.1),
|
|
16
|
+
// 非 http(s)/站内路径一律丢弃(前端降级),绝不把 data:/javascript: 交给 <img src>;
|
|
17
|
+
// ④ 一切失败软着陆:未登录/自建 → 200 + ok:false(面板静默);上游 4xx/5xx 透传;
|
|
18
|
+
// 不可达 → 502 JSON,**绝不抛回 webServer**(否则面板会看到 500 堆栈)。
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import test from "node:test";
|
|
25
|
+
import { apply } from "../lib/index.js";
|
|
26
|
+
|
|
27
|
+
/** 假 relay:device-login 发 token;community-invite 两个端点按 opts 返回(并记录收到的请求)。 */
|
|
28
|
+
async function startFakeRelay(opts = {}) {
|
|
29
|
+
const seen = [];
|
|
30
|
+
const srv = http.createServer((req, res) => {
|
|
31
|
+
let raw = "";
|
|
32
|
+
req.on("data", (c) => { raw += c; });
|
|
33
|
+
req.on("end", () => {
|
|
34
|
+
const entry = { method: req.method, url: req.url, headers: req.headers, body: raw };
|
|
35
|
+
seen.push(entry);
|
|
36
|
+
const send = (code, obj) => {
|
|
37
|
+
res.writeHead(code, { "content-type": "application/json" });
|
|
38
|
+
res.end(JSON.stringify(obj));
|
|
39
|
+
};
|
|
40
|
+
const url = new URL(req.url, "http://x");
|
|
41
|
+
// 模拟"token 拿到了、但企业端 community-invite 端点不可达":直接掐连接(fetch 端表现为 status 0)
|
|
42
|
+
if (opts.killCommunity && url.pathname.startsWith("/api/community-invite")) {
|
|
43
|
+
try { req.socket.destroy(); } catch { /* 忽略 */ }
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (req.method === "POST" && url.pathname === "/api/device-login") {
|
|
47
|
+
// device-login 失败场景:返回 401(插件 node 半应静默降级成 not_logged_in)
|
|
48
|
+
if (opts.loginStatus && opts.loginStatus !== 200) return send(opts.loginStatus, { error: { code: "bad_credentials", message: "密码错误" } });
|
|
49
|
+
return send(200, { token: "jwt-abc" });
|
|
50
|
+
}
|
|
51
|
+
if (url.pathname === "/api/community-invite/status") {
|
|
52
|
+
if (opts.statusCode && opts.statusCode !== 200) {
|
|
53
|
+
return send(opts.statusCode, { error: { code: "boom", message: "服务端炸了" } });
|
|
54
|
+
}
|
|
55
|
+
return send(200, opts.statusBody || {
|
|
56
|
+
ok: true,
|
|
57
|
+
show: true,
|
|
58
|
+
reason: "ok",
|
|
59
|
+
used_seconds: 7205,
|
|
60
|
+
required_seconds: 7200,
|
|
61
|
+
qr_url: "/qr/group-qr.png"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (req.method === "POST" && url.pathname === "/api/community-invite/shown") {
|
|
65
|
+
return send(200, { ok: true });
|
|
66
|
+
}
|
|
67
|
+
send(404, { error: { code: "not_found" } });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
|
|
71
|
+
return { srv, seen, base: `http://127.0.0.1:${srv.address().port}` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function boot(relayDir) {
|
|
75
|
+
const routes = new Map();
|
|
76
|
+
apply({
|
|
77
|
+
webServer: { register(rt) { routes.set(rt.path, rt.handler); return () => {}; } },
|
|
78
|
+
effect(reg) { return reg(); },
|
|
79
|
+
logger: { info() {}, warn() {} },
|
|
80
|
+
}, { relayDir });
|
|
81
|
+
const host = http.createServer((req, res) => {
|
|
82
|
+
const url = new URL(req.url, "http://x");
|
|
83
|
+
// 与 announcements-proxy.test.mjs 对齐:注册对象只有 {kind,path,handler},
|
|
84
|
+
// prefix 路由的 handler 自己判别子路径(没有 route.prefix 字段)。
|
|
85
|
+
const handler = routes.get(url.pathname) || routes.get("/dsh-remote/community-invite");
|
|
86
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
87
|
+
});
|
|
88
|
+
return host;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const writeCfg = (relayDir, apiUrl, extra = {}) => writeFile(path.join(relayDir, ".dsh-config.json"), JSON.stringify({
|
|
92
|
+
device_id: "dev-ci-1", api_url: apiUrl, phone: "13800000000", password: "pw", ...extra,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
/** 只挑 community-invite 的上游请求(插件装载时还会打 public-config / device-login 等旁路请求)。 */
|
|
96
|
+
const ciReqs = (up) => up.seen.filter((e) => String(e.url || "").startsWith("/api/community-invite"));
|
|
97
|
+
|
|
98
|
+
async function withPlugin(fn) {
|
|
99
|
+
const relayDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ci-"));
|
|
100
|
+
process.env.DSH_RELAY_SKIP_SERVICE = "1"; // 套件级隔离:绝不真的去后台装运行环境
|
|
101
|
+
let host = null;
|
|
102
|
+
try {
|
|
103
|
+
return await fn({ relayDir, startHost: () => { host = boot(relayDir); return host; } });
|
|
104
|
+
} finally {
|
|
105
|
+
try { if (host) host.close(); } catch { /* 忽略 */ }
|
|
106
|
+
process.env.DSH_RELAY_SKIP_SERVICE = "1"; // 恢复(不要 delete:套件本来就靠它隔离)
|
|
107
|
+
await rm(relayDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function listen(host) {
|
|
112
|
+
await new Promise((r) => host.listen(0, "127.0.0.1", r));
|
|
113
|
+
return `http://127.0.0.1:${host.address().port}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
test("GET status:device-login token 打企业端,show/qr_url 原样透传(qr_url 归一化成绝对地址)", async () => {
|
|
117
|
+
const up = await startFakeRelay();
|
|
118
|
+
try {
|
|
119
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
120
|
+
await writeCfg(relayDir, up.base);
|
|
121
|
+
const base = await listen(startHost());
|
|
122
|
+
const r = await (await fetch(`${base}/dsh-remote/community-invite/status`)).json();
|
|
123
|
+
assert.equal(r.ok, true);
|
|
124
|
+
assert.equal(r.show, true);
|
|
125
|
+
assert.equal(r.reason, "ok");
|
|
126
|
+
assert.equal(r.used_seconds, 7205, "时长口径由服务端算,node 半只透传");
|
|
127
|
+
assert.equal(r.required_seconds, 7200);
|
|
128
|
+
assert.equal(r.qr_url, `${up.base}/qr/group-qr.png`, "相对路径必须拼成企业端绝对地址(否则图片落在 127.0.0.1 上打不开)");
|
|
129
|
+
|
|
130
|
+
const got = ciReqs(up).find((e) => e.method === "GET" && e.url === "/api/community-invite/status");
|
|
131
|
+
assert.ok(got, "必须真的打到企业端 GET /api/community-invite/status");
|
|
132
|
+
assert.equal(got.headers.authorization, "Bearer jwt-abc", "必须用 device-login 换来的 token(sub = user)");
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
up.srv.close();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("GET status:show=false(未满 2h / 已弹过 / 没配二维码)原样透传,node 半绝不自己算", async () => {
|
|
140
|
+
const up = await startFakeRelay({
|
|
141
|
+
statusBody: { ok: true, show: false, reason: "already_shown", used_seconds: 9000, required_seconds: 7200, qr_url: "/qr/group-qr.png" }
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
145
|
+
await writeCfg(relayDir, up.base);
|
|
146
|
+
const base = await listen(startHost());
|
|
147
|
+
const r = await (await fetch(`${base}/dsh-remote/community-invite/status`)).json();
|
|
148
|
+
assert.equal(r.ok, true);
|
|
149
|
+
assert.equal(r.show, false, "服务端说不弹就不弹");
|
|
150
|
+
assert.equal(r.reason, "already_shown", "reason 要透传(面板/排障用)");
|
|
151
|
+
assert.equal(r.used_seconds, 9000);
|
|
152
|
+
});
|
|
153
|
+
} finally {
|
|
154
|
+
up.srv.close();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("GET status:qr_url 为空 / 非法协议(data:/javascript:/协议相对)→ 下发空串(前端降级)", async () => {
|
|
159
|
+
for (const bad of ["", " ", "data:image/png;base64,AAA", "javascript:alert(1)", "//evil.example/qr.png"]) {
|
|
160
|
+
const up = await startFakeRelay({
|
|
161
|
+
statusBody: { ok: true, show: true, reason: "ok", used_seconds: 7300, required_seconds: 7200, qr_url: bad }
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
165
|
+
await writeCfg(relayDir, up.base);
|
|
166
|
+
const base = await listen(startHost());
|
|
167
|
+
const r = await (await fetch(`${base}/dsh-remote/community-invite/status`)).json();
|
|
168
|
+
assert.equal(r.show, true, `show 仍然透传(${JSON.stringify(bad)})`);
|
|
169
|
+
assert.equal(r.qr_url, "", `非法/空 qr_url 必须下发空串,交给前端降级(实际 ${JSON.stringify(r.qr_url)})`);
|
|
170
|
+
});
|
|
171
|
+
} finally {
|
|
172
|
+
up.srv.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("POST shown:幂等上报打到企业端 POST /api/community-invite/shown(Bearer)", async () => {
|
|
178
|
+
const up = await startFakeRelay();
|
|
179
|
+
try {
|
|
180
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
181
|
+
await writeCfg(relayDir, up.base);
|
|
182
|
+
const base = await listen(startHost());
|
|
183
|
+
const res = await fetch(`${base}/dsh-remote/community-invite/shown`, { method: "POST" });
|
|
184
|
+
assert.equal(res.status, 200);
|
|
185
|
+
const r = await res.json();
|
|
186
|
+
assert.equal(r.ok, true);
|
|
187
|
+
const got = ciReqs(up).find((e) => e.method === "POST" && e.url === "/api/community-invite/shown");
|
|
188
|
+
assert.ok(got, "必须真的打到企业端 POST /api/community-invite/shown");
|
|
189
|
+
assert.equal(got.headers.authorization, "Bearer jwt-abc");
|
|
190
|
+
// 上报两次也必须都成功(幂等契约:展示时一次 + 关闭时一次)
|
|
191
|
+
const again = await (await fetch(`${base}/dsh-remote/community-invite/shown`, { method: "POST" })).json();
|
|
192
|
+
assert.equal(again.ok, true);
|
|
193
|
+
assert.equal(ciReqs(up).filter((e) => e.method === "POST").length, 2, "两次上报都要转发(服务端幂等)");
|
|
194
|
+
});
|
|
195
|
+
} finally {
|
|
196
|
+
up.srv.close();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("鉴权:没配账号 → 200 + ok:false/not_logged_in,且完全不碰企业端(不白跑 device-login)", async () => {
|
|
201
|
+
const up = await startFakeRelay();
|
|
202
|
+
try {
|
|
203
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
204
|
+
await writeCfg(relayDir, up.base, { phone: "", password: "" });
|
|
205
|
+
const base = await listen(startHost());
|
|
206
|
+
const r = await (await fetch(`${base}/dsh-remote/community-invite/status`)).json();
|
|
207
|
+
assert.equal(r.ok, false);
|
|
208
|
+
assert.equal(r.show, false, "未登录绝不能让面板弹引导");
|
|
209
|
+
assert.equal(r.reason, "not_logged_in");
|
|
210
|
+
assert.equal(ciReqs(up).length, 0, "不得向企业端发任何 community-invite 请求");
|
|
211
|
+
assert.equal(up.seen.filter((e) => e.url === "/api/device-login").length, 0, "没凭据就不该尝试 device-login");
|
|
212
|
+
});
|
|
213
|
+
} finally {
|
|
214
|
+
up.srv.close();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("鉴权:自建(local_key)没有企业端账号体系 → local_mode,不发请求", async () => {
|
|
219
|
+
const up = await startFakeRelay();
|
|
220
|
+
try {
|
|
221
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
222
|
+
await writeCfg(relayDir, up.base, { phone: "", password: "", local_key: "selfhost-key" });
|
|
223
|
+
const base = await listen(startHost());
|
|
224
|
+
const r = await (await fetch(`${base}/dsh-remote/community-invite/status`)).json();
|
|
225
|
+
assert.equal(r.ok, false);
|
|
226
|
+
assert.equal(r.show, false);
|
|
227
|
+
assert.equal(r.reason, "local_mode");
|
|
228
|
+
assert.equal(ciReqs(up).length, 0, "自建模式不得请求企业端");
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
up.srv.close();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("鉴权:device-login 换 token 失败 → 静默 not_logged_in(面板不弹、不报错)", async () => {
|
|
236
|
+
const up = await startFakeRelay({ loginStatus: 401 });
|
|
237
|
+
try {
|
|
238
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
239
|
+
await writeCfg(relayDir, up.base);
|
|
240
|
+
const base = await listen(startHost());
|
|
241
|
+
const res = await fetch(`${base}/dsh-remote/community-invite/status`);
|
|
242
|
+
assert.equal(res.status, 200, "对面板而言是静默降级,不是 500");
|
|
243
|
+
const r = await res.json();
|
|
244
|
+
assert.equal(r.ok, false);
|
|
245
|
+
assert.equal(r.show, false);
|
|
246
|
+
assert.equal(r.reason, "not_logged_in");
|
|
247
|
+
});
|
|
248
|
+
} finally {
|
|
249
|
+
up.srv.close();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("错误语义:企业端 401/500 → 状态码透传 + ok:false + 可读 error(面板依旧静默)", async () => {
|
|
254
|
+
for (const code of [401, 500]) {
|
|
255
|
+
const up = await startFakeRelay({ statusCode: code });
|
|
256
|
+
try {
|
|
257
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
258
|
+
await writeCfg(relayDir, up.base);
|
|
259
|
+
const base = await listen(startHost());
|
|
260
|
+
const res = await fetch(`${base}/dsh-remote/community-invite/status`);
|
|
261
|
+
assert.equal(res.status, code, "上游状态码透传(排障能看见,不伪装成 200)");
|
|
262
|
+
const r = await res.json();
|
|
263
|
+
assert.equal(r.ok, false);
|
|
264
|
+
assert.equal(r.show, false);
|
|
265
|
+
assert.equal(r.reason, "status_failed");
|
|
266
|
+
assert.ok(String(r.error || "").length > 0, "必须给可读错误");
|
|
267
|
+
});
|
|
268
|
+
} finally {
|
|
269
|
+
up.srv.close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("软着陆:整个企业端不可达(连 token 都拿不到)→ 200 + not_logged_in,面板不弹不报错", async () => {
|
|
275
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
276
|
+
// 指向一个一定没人监听的端口
|
|
277
|
+
await writeCfg(relayDir, "http://127.0.0.1:1");
|
|
278
|
+
const base = await listen(startHost());
|
|
279
|
+
const res = await fetch(`${base}/dsh-remote/community-invite/status`);
|
|
280
|
+
assert.equal(res.status, 200, "拿不到 token 属于'未登录',不是服务端错误(面板静默)");
|
|
281
|
+
const r = await res.json();
|
|
282
|
+
assert.equal(r.ok, false);
|
|
283
|
+
assert.equal(r.show, false);
|
|
284
|
+
assert.equal(r.reason, "not_logged_in");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("错误语义:token 拿到了但企业端 community-invite 端点不可达 → 502 JSON(路由自身绝不抛)", async () => {
|
|
289
|
+
const up = await startFakeRelay({ killCommunity: true });
|
|
290
|
+
try {
|
|
291
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
292
|
+
await writeCfg(relayDir, up.base);
|
|
293
|
+
const base = await listen(startHost());
|
|
294
|
+
const res = await fetch(`${base}/dsh-remote/community-invite/status`);
|
|
295
|
+
assert.equal(res.status, 502, "已确认登录、失败发生在上游请求 → 502(排障可见,不是静默 200)");
|
|
296
|
+
const r = await res.json();
|
|
297
|
+
assert.equal(r.ok, false);
|
|
298
|
+
assert.equal(r.show, false);
|
|
299
|
+
assert.equal(r.reason, "status_failed");
|
|
300
|
+
assert.ok(String(r.error || "").length > 0);
|
|
301
|
+
// 路由本身绝不能把异常抛回 webServer(那会变成 500 + 堆栈)
|
|
302
|
+
assert.notEqual(res.status, 500);
|
|
303
|
+
});
|
|
304
|
+
} finally {
|
|
305
|
+
up.srv.close();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("未知子路径 / 错误方法 → 404,不代理到奇怪的上游路径", async () => {
|
|
310
|
+
const up = await startFakeRelay();
|
|
311
|
+
try {
|
|
312
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
313
|
+
await writeCfg(relayDir, up.base);
|
|
314
|
+
const base = await listen(startHost());
|
|
315
|
+
for (const [p, method] of [["/dsh-remote/community-invite/other", "GET"], ["/dsh-remote/community-invite/status", "POST"], ["/dsh-remote/community-invite/shown", "GET"]]) {
|
|
316
|
+
const res = await fetch(`${base}${p}`, { method });
|
|
317
|
+
assert.equal(res.status, 404, `${method} ${p} 必须 404`);
|
|
318
|
+
const r = await res.json();
|
|
319
|
+
assert.equal(r.reason, "unknown_route");
|
|
320
|
+
}
|
|
321
|
+
assert.equal(ciReqs(up).length, 0, "未知路由绝不请求企业端");
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
up.srv.close();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("身份纪律:浏览器带来的 Authorization **不透传**(用户级接口只认本机 node 半 token)", async () => {
|
|
329
|
+
const up = await startFakeRelay();
|
|
330
|
+
try {
|
|
331
|
+
await withPlugin(async ({ relayDir, startHost }) => {
|
|
332
|
+
await writeCfg(relayDir, up.base);
|
|
333
|
+
const base = await listen(startHost());
|
|
334
|
+
await fetch(`${base}/dsh-remote/community-invite/status`, { headers: { authorization: "Bearer browser-forged" } });
|
|
335
|
+
const got = ciReqs(up).find((e) => e.method === "GET");
|
|
336
|
+
assert.ok(got);
|
|
337
|
+
assert.equal(got.headers.authorization, "Bearer jwt-abc",
|
|
338
|
+
"身份必须是本机 device-login 换来的 token;浏览器带的 token 不能替代(否则'谁的身份'就含糊了)");
|
|
339
|
+
});
|
|
340
|
+
} finally {
|
|
341
|
+
up.srv.close();
|
|
342
|
+
}
|
|
343
|
+
});
|