ike-weblca-html 2.2.20 → 2.2.21

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.
@@ -0,0 +1,489 @@
1
+ const assert = require('assert')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const babel = require('@babel/core')
5
+
6
+ function loadSourceModule(relativePath) {
7
+ const filename = path.resolve(__dirname, '..', relativePath)
8
+ const source = fs.readFileSync(filename, 'utf8')
9
+ const transformed = babel.transformSync(source, {
10
+ babelrc: false,
11
+ configFile: false,
12
+ filename,
13
+ presets: [
14
+ [
15
+ require.resolve('@babel/preset-env'),
16
+ { targets: { node: '14.20.1' }, modules: 'commonjs' },
17
+ ],
18
+ ],
19
+ })
20
+ const loadedModule = { exports: {} }
21
+ const evaluate = new Function('module', 'exports', transformed.code)
22
+ evaluate(loadedModule, loadedModule.exports)
23
+ return loadedModule.exports
24
+ }
25
+
26
+ function createWindow({ origin, parentOrigin }) {
27
+ const listeners = new Map()
28
+ const parentMessages = []
29
+ const frames = []
30
+ const windowObject = {
31
+ location: { origin },
32
+ document: {
33
+ referrer: parentOrigin ? `${parentOrigin}/database` : '',
34
+ querySelectorAll(selector) {
35
+ if (selector !== 'iframe') return []
36
+ return frames
37
+ },
38
+ },
39
+ addEventListener(type, listener) {
40
+ const typeListeners = listeners.get(type) || []
41
+ typeListeners.push(listener)
42
+ listeners.set(type, typeListeners)
43
+ },
44
+ removeEventListener(type, listener) {
45
+ const typeListeners = listeners.get(type) || []
46
+ listeners.set(
47
+ type,
48
+ typeListeners.filter((item) => item !== listener)
49
+ )
50
+ },
51
+ async emit(type, event) {
52
+ const typeListeners = listeners.get(type) || []
53
+ await Promise.all(typeListeners.map((listener) => listener(event)))
54
+ },
55
+ }
56
+
57
+ if (parentOrigin) {
58
+ windowObject.parent = {
59
+ postMessage(message, targetOrigin) {
60
+ parentMessages.push({ message, targetOrigin })
61
+ },
62
+ }
63
+ windowObject.top = windowObject.parent
64
+ } else {
65
+ windowObject.parent = windowObject
66
+ windowObject.top = windowObject
67
+ }
68
+
69
+ return {
70
+ windowObject,
71
+ parentMessages,
72
+ addFrameSource(source, src) {
73
+ frames.push({ contentWindow: source, src })
74
+ },
75
+ }
76
+ }
77
+
78
+ const tests = []
79
+
80
+ function test(name, run) {
81
+ tests.push({ name, run })
82
+ }
83
+
84
+ const bridge = loadSourceModule('src/js/HeaderAuthBridge.js')
85
+
86
+ test('父页面只向受信任 iframe 返回不含 token 的认证快照', async () => {
87
+ const { windowObject, addFrameSource } = createWindow({
88
+ origin: 'https://www.weblca.net',
89
+ })
90
+ const trustedMessages = []
91
+ const dynamicProjectMessages = []
92
+ const untrustedMessages = []
93
+ const spoofedMessages = []
94
+ const dispose = bridge.installParentAuthBridge({
95
+ windowObject,
96
+ getAuthState: async () => ({
97
+ loggedIn: true,
98
+ token: 'Bearer secret-must-not-leak',
99
+ userSummary: { userName: '测试用户', userId: 'user-1' },
100
+ messageCount: 3,
101
+ }),
102
+ })
103
+
104
+ await windowObject.emit('message', {
105
+ origin: 'https://evil.example',
106
+ source: {
107
+ postMessage(message, targetOrigin) {
108
+ untrustedMessages.push({ message, targetOrigin })
109
+ },
110
+ },
111
+ data: { type: bridge.AUTH_REQUEST, requestId: 'untrusted-request' },
112
+ })
113
+
114
+ await windowObject.emit('message', {
115
+ origin: 'https://www.efootprint.net',
116
+ source: {
117
+ postMessage(message, targetOrigin) {
118
+ spoofedMessages.push({ message, targetOrigin })
119
+ },
120
+ },
121
+ data: { type: bridge.AUTH_REQUEST, requestId: 'spoofed-request' },
122
+ })
123
+
124
+ const trustedSource = {
125
+ postMessage(message, targetOrigin) {
126
+ trustedMessages.push({ message, targetOrigin })
127
+ },
128
+ }
129
+ addFrameSource(trustedSource, 'https://www.efootprint.net/database')
130
+ await windowObject.emit('message', {
131
+ origin: 'https://www.efootprint.net',
132
+ source: trustedSource,
133
+ data: { type: bridge.AUTH_REQUEST, requestId: 'trusted-request' },
134
+ })
135
+
136
+ const dynamicProjectSource = {
137
+ postMessage(message, targetOrigin) {
138
+ dynamicProjectMessages.push({ message, targetOrigin })
139
+ },
140
+ }
141
+ addFrameSource(
142
+ dynamicProjectSource,
143
+ 'https://unlisted-product.customer-example.org/embedded-page'
144
+ )
145
+ await windowObject.emit('message', {
146
+ origin: 'https://unlisted-product.customer-example.org',
147
+ source: dynamicProjectSource,
148
+ data: { type: bridge.AUTH_REQUEST, requestId: 'dynamic-request' },
149
+ })
150
+
151
+ assert.strictEqual(untrustedMessages.length, 0)
152
+ assert.strictEqual(spoofedMessages.length, 0)
153
+ assert.strictEqual(trustedMessages.length, 1)
154
+ assert.strictEqual(dynamicProjectMessages.length, 1)
155
+ assert.strictEqual(
156
+ dynamicProjectMessages[0].targetOrigin,
157
+ 'https://unlisted-product.customer-example.org'
158
+ )
159
+ assert.strictEqual(trustedMessages[0].targetOrigin, 'https://www.efootprint.net')
160
+ assert.deepStrictEqual(trustedMessages[0].message, {
161
+ type: bridge.AUTH_RESPONSE,
162
+ requestId: 'trusted-request',
163
+ authState: {
164
+ loggedIn: true,
165
+ userSummary: { userName: '测试用户', userId: 'user-1' },
166
+ messageCount: 3,
167
+ },
168
+ })
169
+
170
+ dispose()
171
+ })
172
+
173
+ test('iframe Header 只接受真实父页面返回的对应认证响应', async () => {
174
+ const { windowObject, parentMessages } = createWindow({
175
+ origin: 'https://www.efootprint.net',
176
+ parentOrigin: 'https://www.weblca.net',
177
+ })
178
+ const receivedStates = []
179
+ const connection = bridge.connectIframeHeaderAuth({
180
+ windowObject,
181
+ onAuthState: (authState) => receivedStates.push(authState),
182
+ })
183
+
184
+ assert.strictEqual(parentMessages.length, 1)
185
+ assert.strictEqual(parentMessages[0].targetOrigin, 'https://www.weblca.net')
186
+ assert.strictEqual(parentMessages[0].message.type, bridge.AUTH_REQUEST)
187
+ const requestId = parentMessages[0].message.requestId
188
+
189
+ await windowObject.emit('message', {
190
+ origin: 'https://fake.weblca.net',
191
+ source: windowObject.parent,
192
+ data: {
193
+ type: bridge.AUTH_RESPONSE,
194
+ requestId,
195
+ authState: { loggedIn: true },
196
+ },
197
+ })
198
+ await windowObject.emit('message', {
199
+ origin: 'https://www.weblca.net',
200
+ source: {},
201
+ data: {
202
+ type: bridge.AUTH_RESPONSE,
203
+ requestId,
204
+ authState: { loggedIn: true },
205
+ },
206
+ })
207
+ await windowObject.emit('message', {
208
+ origin: 'https://www.weblca.net',
209
+ source: windowObject.parent,
210
+ data: {
211
+ type: bridge.AUTH_RESPONSE,
212
+ requestId,
213
+ authState: {
214
+ loggedIn: true,
215
+ userSummary: { userName: '父页面用户' },
216
+ messageCount: 5,
217
+ },
218
+ },
219
+ })
220
+
221
+ assert.deepStrictEqual(receivedStates, [
222
+ {
223
+ loggedIn: true,
224
+ userSummary: { userName: '父页面用户' },
225
+ messageCount: 5,
226
+ },
227
+ ])
228
+
229
+ connection.dispose()
230
+ })
231
+
232
+ test('未预先列出的父项目也使用 referrer 的精确 origin 完成认证握手', async () => {
233
+ const { windowObject, parentMessages } = createWindow({
234
+ origin: 'https://embedded.customer-example.org',
235
+ parentOrigin: 'https://portal.customer-example.org',
236
+ })
237
+ const receivedStates = []
238
+ const connection = bridge.connectIframeHeaderAuth({
239
+ windowObject,
240
+ onAuthState: (authState) => receivedStates.push(authState),
241
+ })
242
+
243
+ assert.strictEqual(connection.connected, false)
244
+ assert.strictEqual(parentMessages.length, 1)
245
+ assert.strictEqual(
246
+ parentMessages[0].targetOrigin,
247
+ 'https://portal.customer-example.org'
248
+ )
249
+ const requestId = parentMessages[0].message.requestId
250
+
251
+ await windowObject.emit('message', {
252
+ origin: 'https://portal.customer-example.org',
253
+ source: windowObject.parent,
254
+ data: {
255
+ type: bridge.AUTH_RESPONSE,
256
+ requestId,
257
+ authState: { loggedIn: true, userSummary: { userName: '动态项目用户' } },
258
+ },
259
+ })
260
+
261
+ assert.strictEqual(receivedStates.length, 1)
262
+ assert.strictEqual(receivedStates[0].userSummary.userName, '动态项目用户')
263
+ assert.strictEqual(connection.connected, true)
264
+ connection.dispose()
265
+ })
266
+
267
+ test('iframe Header 的导航和退出动作只发送给已验证父页面', async () => {
268
+ const { windowObject, parentMessages } = createWindow({
269
+ origin: 'https://www.efootprint.net',
270
+ parentOrigin: 'https://www.weblca.net',
271
+ })
272
+ const connection = bridge.connectIframeHeaderAuth({
273
+ windowObject,
274
+ onAuthState() {},
275
+ })
276
+
277
+ const requestId = parentMessages[0].message.requestId
278
+ await windowObject.emit('message', {
279
+ origin: 'https://www.weblca.net',
280
+ source: windowObject.parent,
281
+ data: {
282
+ type: bridge.AUTH_RESPONSE,
283
+ requestId,
284
+ authState: { loggedIn: true },
285
+ },
286
+ })
287
+
288
+ connection.navigate('https://www.weblca.net/content/selfCenter')
289
+ connection.logout()
290
+
291
+ assert.strictEqual(parentMessages.length, 3)
292
+ assert.deepStrictEqual(parentMessages[1], {
293
+ message: {
294
+ type: bridge.NAVIGATE_REQUEST,
295
+ url: 'https://www.weblca.net/content/selfCenter',
296
+ },
297
+ targetOrigin: 'https://www.weblca.net',
298
+ })
299
+ assert.deepStrictEqual(parentMessages[2], {
300
+ message: { type: bridge.LOGOUT_REQUEST },
301
+ targetOrigin: 'https://www.weblca.net',
302
+ })
303
+
304
+ connection.dispose()
305
+ })
306
+
307
+ test('父页面只执行受信任 iframe 发出的平台内导航和退出动作', async () => {
308
+ const { windowObject, addFrameSource } = createWindow({
309
+ origin: 'https://www.weblca.net',
310
+ })
311
+ const navigations = []
312
+ let logoutCount = 0
313
+ const dispose = bridge.installParentAuthBridge({
314
+ windowObject,
315
+ getAuthState: () => ({ loggedIn: true }),
316
+ onNavigate: (url) => navigations.push(url),
317
+ onLogout: () => {
318
+ logoutCount += 1
319
+ },
320
+ })
321
+ const source = { postMessage() {} }
322
+ addFrameSource(source, 'https://www.efootprint.net/database')
323
+
324
+ await windowObject.emit('message', {
325
+ origin: 'https://evil.example',
326
+ source,
327
+ data: {
328
+ type: bridge.NAVIGATE_REQUEST,
329
+ url: 'https://www.weblca.net/content/selfCenter',
330
+ },
331
+ })
332
+ await windowObject.emit('message', {
333
+ origin: 'https://www.efootprint.net',
334
+ source,
335
+ data: {
336
+ type: bridge.NAVIGATE_REQUEST,
337
+ url: 'https://evil.example/phishing',
338
+ },
339
+ })
340
+ await windowObject.emit('message', {
341
+ origin: 'https://www.efootprint.net',
342
+ source,
343
+ data: {
344
+ type: bridge.NAVIGATE_REQUEST,
345
+ url: 'https://www.weblca.net/content/selfCenter',
346
+ },
347
+ })
348
+ await windowObject.emit('message', {
349
+ origin: 'https://www.efootprint.net',
350
+ source,
351
+ data: { type: bridge.LOGOUT_REQUEST },
352
+ })
353
+
354
+ assert.deepStrictEqual(navigations, [
355
+ 'https://www.weblca.net/content/selfCenter',
356
+ ])
357
+ assert.strictEqual(logoutCount, 1)
358
+
359
+ dispose()
360
+ })
361
+
362
+ test('没有认证响应的旧父项目仍保留 wildcard navigate 兼容消息', () => {
363
+ const { windowObject, parentMessages } = createWindow({
364
+ origin: 'https://legacy-child.unknown.example',
365
+ parentOrigin: 'https://legacy-parent.unknown.example',
366
+ })
367
+ const url = 'https://www.weblca.net/content/selfCenter'
368
+ const connection = bridge.connectIframeHeaderAuth({
369
+ windowObject,
370
+ onAuthState() {},
371
+ })
372
+
373
+ const mode = bridge.navigateWithParentFallback({
374
+ windowObject,
375
+ connection,
376
+ url,
377
+ })
378
+
379
+ assert.strictEqual(mode, 'legacy')
380
+ assert.strictEqual(windowObject.location.href, url)
381
+ assert.strictEqual(parentMessages.length, 2)
382
+ assert.strictEqual(parentMessages[0].message.type, bridge.AUTH_REQUEST)
383
+ assert.strictEqual(
384
+ parentMessages[0].targetOrigin,
385
+ 'https://legacy-parent.unknown.example'
386
+ )
387
+ assert.deepStrictEqual(parentMessages[1], {
388
+ message: { type: 'navigate', url },
389
+ targetOrigin: '*',
390
+ })
391
+ connection.dispose()
392
+ })
393
+
394
+ test('父页面从 Cookie 生成 Header 所需的最小认证快照', () => {
395
+ const cookieValues = {
396
+ token: 'Bearer parent-token',
397
+ userInfo: JSON.stringify({
398
+ userName: 'Cookie 用户',
399
+ relationId: 'relation-1',
400
+ profileImg: 'https://images.example/avatar.png',
401
+ accountCount: 2,
402
+ groupCount: 3,
403
+ datasetCount: 4,
404
+ materialCount: 5,
405
+ messageCount: 6,
406
+ token: 'Bearer nested-token-must-not-leak',
407
+ roles: ['admin'],
408
+ }),
409
+ }
410
+ const authState = bridge.readCookieAuthState({
411
+ get(key) {
412
+ return cookieValues[key]
413
+ },
414
+ })
415
+
416
+ assert.deepStrictEqual(authState, {
417
+ loggedIn: true,
418
+ userSummary: {
419
+ userName: 'Cookie 用户',
420
+ relationId: 'relation-1',
421
+ profileImg: 'https://images.example/avatar.png',
422
+ accountCount: 2,
423
+ groupCount: 3,
424
+ datasetCount: 4,
425
+ materialCount: 5,
426
+ },
427
+ messageCount: 6,
428
+ })
429
+ })
430
+
431
+ test('iframe Header 收到认证快照后立即更新登录展示数据', () => {
432
+ let updateCount = 0
433
+ const header = {
434
+ isLoggedIn: false,
435
+ userSummary: null,
436
+ userName: '',
437
+ userId: '',
438
+ userAvatar: '',
439
+ userStats: {},
440
+ messageCount: 0,
441
+ requestUpdate() {
442
+ updateCount += 1
443
+ },
444
+ }
445
+
446
+ bridge.applyAuthStateToHeader(header, {
447
+ loggedIn: true,
448
+ userSummary: {
449
+ realName: '响应式用户',
450
+ userId: 'user-2',
451
+ avatar: 'https://images.example/user-2.png',
452
+ accountCount: 7,
453
+ groupCount: 8,
454
+ datasetCount: 9,
455
+ materialCount: 10,
456
+ },
457
+ messageCount: 11,
458
+ })
459
+
460
+ assert.strictEqual(header.isLoggedIn, true)
461
+ assert.strictEqual(header.userName, '响应式用户')
462
+ assert.strictEqual(header.userId, 'user-2')
463
+ assert.strictEqual(header.userAvatar, 'https://images.example/user-2.png')
464
+ assert.deepStrictEqual(header.userStats, {
465
+ account: 7,
466
+ group: 8,
467
+ database: 9,
468
+ databank: 10,
469
+ })
470
+ assert.strictEqual(header.messageCount, 11)
471
+ assert.strictEqual(updateCount, 1)
472
+ })
473
+
474
+ async function main() {
475
+ let failed = 0
476
+ for (const item of tests) {
477
+ try {
478
+ await item.run()
479
+ console.log(`PASS ${item.name}`)
480
+ } catch (error) {
481
+ failed += 1
482
+ console.error(`FAIL ${item.name}`)
483
+ console.error(error && error.stack ? error.stack : error)
484
+ }
485
+ }
486
+ if (failed > 0) process.exitCode = 1
487
+ }
488
+
489
+ main()