@csntgao/uni-base 0.1.0
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/.prettierignore +4 -0
- package/.prettierrc.json +6 -0
- package/AGENTS.md +519 -0
- package/CLAUDE.md +512 -0
- package/README.md +734 -0
- package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
- package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
- package/eslint.config.js +30 -0
- package/examples/vanilla/index.html +33 -0
- package/examples/vanilla/main.js +247 -0
- package/examples/vanilla/package.json +15 -0
- package/examples/vanilla/public/runtime-config.js +10 -0
- package/examples/vanilla/style.css +99 -0
- package/package.json +31 -0
- package/packages/core/package.json +30 -0
- package/packages/core/src/client.ts +1132 -0
- package/packages/core/src/customer-support-client.ts +825 -0
- package/packages/core/src/customer-support-types.ts +123 -0
- package/packages/core/src/index.ts +79 -0
- package/packages/core/src/notification-center-client.ts +462 -0
- package/packages/core/src/notification-center-types.ts +154 -0
- package/packages/core/src/protocol.ts +237 -0
- package/packages/core/src/types.ts +170 -0
- package/packages/core/tests/client.test.ts +760 -0
- package/packages/core/tests/customer-support-client.test.ts +576 -0
- package/packages/core/tests/fake-websocket.ts +55 -0
- package/packages/core/tests/notification-center-client.test.ts +467 -0
- package/packages/core/tests/protocol.test.ts +124 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/web-components/package.json +35 -0
- package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
- package/packages/web-components/src/account-context.ts +1540 -0
- package/packages/web-components/src/application-switcher-transport.ts +243 -0
- package/packages/web-components/src/application-switcher.ts +1103 -0
- package/packages/web-components/src/chat.ts +1754 -0
- package/packages/web-components/src/customer-support-chat.ts +862 -0
- package/packages/web-components/src/customer-support-transport.ts +269 -0
- package/packages/web-components/src/index.ts +256 -0
- package/packages/web-components/src/notification-center.ts +1066 -0
- package/packages/web-components/src/notification-popover.ts +615 -0
- package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
- package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
- package/packages/web-components/src/organization-onboarding.ts +1216 -0
- package/packages/web-components/src/organization-selector.ts +534 -0
- package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
- package/packages/web-components/src/prototype-icons.ts +242 -0
- package/packages/web-components/src/safe-image-url.ts +29 -0
- package/packages/web-components/src/safe-markdown.ts +192 -0
- package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
- package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
- package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
- package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
- package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
- package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
- package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
- package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
- package/packages/web-components/src/user-login-transport.ts +563 -0
- package/packages/web-components/src/user-login.ts +2027 -0
- package/packages/web-components/src/user-menu.ts +880 -0
- package/packages/web-components/tests/account-context.test.ts +798 -0
- package/packages/web-components/tests/application-switcher.test.ts +562 -0
- package/packages/web-components/tests/chat.test.ts +1130 -0
- package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
- package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
- package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
- package/packages/web-components/tests/notification-center.test.ts +429 -0
- package/packages/web-components/tests/notification-popover.test.ts +283 -0
- package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
- package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
- package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
- package/packages/web-components/tests/organization-selector.test.ts +127 -0
- package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
- package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
- package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
- package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
- package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
- package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
- package/packages/web-components/tests/user-login-transport.test.ts +443 -0
- package/packages/web-components/tests/user-login.test.ts +626 -0
- package/packages/web-components/tests/user-menu.test.ts +271 -0
- package/packages/web-components/tsconfig.json +8 -0
- package/packages/web-components/tsup.config.ts +12 -0
- package/packages/web-components/tsup.runtime.config.ts +37 -0
- package/pnpm-workspace.yaml +6 -0
- package/tsconfig.base.json +21 -0
- package/vitest.config.ts +20 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { createUniplatBaseApplicationSwitcherTransport } from '../src'
|
|
5
|
+
|
|
6
|
+
const serviceBaseUrl = 'https://api.example.com/api/general/project/uniplat_base/service'
|
|
7
|
+
const listEndpoint = `${serviceBaseUrl}/application.identity/list_available_applications`
|
|
8
|
+
const organizationsEndpoint = `${serviceBaseUrl}/application.identity/list_available_organizations`
|
|
9
|
+
const issueTicketEndpoint = `${serviceBaseUrl}/application.identity/issue_ticket`
|
|
10
|
+
const applicationTicket = 'ticket_for_application_navigation_12345'
|
|
11
|
+
const sourceSystem = 'ae-employee'
|
|
12
|
+
|
|
13
|
+
function applicationListResponse() {
|
|
14
|
+
return {
|
|
15
|
+
rescode: 0,
|
|
16
|
+
data: {
|
|
17
|
+
current_identity: {
|
|
18
|
+
xid: '21',
|
|
19
|
+
oid: '11',
|
|
20
|
+
eid: '145',
|
|
21
|
+
mid: '1231',
|
|
22
|
+
application_name: 'hrSaasAdmin',
|
|
23
|
+
},
|
|
24
|
+
available_applications: [
|
|
25
|
+
{
|
|
26
|
+
xid: '21',
|
|
27
|
+
oid: '11',
|
|
28
|
+
application_name: 'hrSaasAdmin',
|
|
29
|
+
application_scope: 'organization',
|
|
30
|
+
application_title: '平台运营后台',
|
|
31
|
+
entry_url: '/platform-admin/',
|
|
32
|
+
icon_url: '/assets/platform-admin.svg',
|
|
33
|
+
description: '平台运营管理',
|
|
34
|
+
open_mode: 'self',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
xid: 22,
|
|
38
|
+
oid: 11,
|
|
39
|
+
application_name: 'HrSaas',
|
|
40
|
+
application_scope: 'organization',
|
|
41
|
+
application_title: '企业管理',
|
|
42
|
+
entry_url: 'https://apps.example.com/enterprise',
|
|
43
|
+
open_mode: 'blank',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
xid: '900',
|
|
47
|
+
oid: 11,
|
|
48
|
+
application_name: 'UniplatPortal',
|
|
49
|
+
application_scope: 'open',
|
|
50
|
+
application_title: '平台门户',
|
|
51
|
+
entry_url: '/portal/',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function organizationListResponse() {
|
|
59
|
+
return {
|
|
60
|
+
rescode: 0,
|
|
61
|
+
data: {
|
|
62
|
+
application_name: 'hrSaasAdmin',
|
|
63
|
+
application_scope: 'organization',
|
|
64
|
+
current_identity: {
|
|
65
|
+
xid: '21',
|
|
66
|
+
oid: '11',
|
|
67
|
+
eid: '145',
|
|
68
|
+
mid: '1231',
|
|
69
|
+
application_name: 'hrSaasAdmin',
|
|
70
|
+
application_scope: 'organization',
|
|
71
|
+
},
|
|
72
|
+
organizations: [
|
|
73
|
+
{ oid: '11', team_name: '金色华勤团队', customer_name: '金色华勤科技' },
|
|
74
|
+
{ oid: '50', team_name: '示例科技团队', customer_name: '示例科技有限公司' },
|
|
75
|
+
{ oid: '34', team_name: '', customer_name: '不能作为团队名称回退' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('createUniplatBaseApplicationSwitcherTransport()', () => {
|
|
82
|
+
it('maps the current organization applications together with open applications', async () => {
|
|
83
|
+
const token = 'uniplat-base.list.jwt'
|
|
84
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
85
|
+
void init
|
|
86
|
+
return new Response(
|
|
87
|
+
JSON.stringify(
|
|
88
|
+
String(input) === organizationsEndpoint
|
|
89
|
+
? organizationListResponse()
|
|
90
|
+
: applicationListResponse(),
|
|
91
|
+
),
|
|
92
|
+
{
|
|
93
|
+
status: 200,
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
99
|
+
serviceBaseUrl,
|
|
100
|
+
fetch: fetchMock as typeof fetch,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const [entries, organizations] = await Promise.all([
|
|
104
|
+
transport.listApplications(token),
|
|
105
|
+
transport.listOrganizations?.(token),
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
expect(entries).toEqual([
|
|
109
|
+
{
|
|
110
|
+
id: '21',
|
|
111
|
+
name: '平台运营后台',
|
|
112
|
+
url: '/platform-admin/',
|
|
113
|
+
iconUrl: '/assets/platform-admin.svg',
|
|
114
|
+
description: '平台运营管理',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: '22',
|
|
118
|
+
name: '企业管理',
|
|
119
|
+
url: 'https://apps.example.com/enterprise',
|
|
120
|
+
openMode: 'blank',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: '900',
|
|
124
|
+
name: '平台门户',
|
|
125
|
+
url: '/portal/',
|
|
126
|
+
},
|
|
127
|
+
])
|
|
128
|
+
expect(organizations).toEqual({
|
|
129
|
+
organizations: [
|
|
130
|
+
{ id: '11', name: '金色华勤团队' },
|
|
131
|
+
{ id: '50', name: '示例科技团队' },
|
|
132
|
+
{ id: '34', name: '组织 34' },
|
|
133
|
+
],
|
|
134
|
+
currentOrganizationId: '11',
|
|
135
|
+
})
|
|
136
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
137
|
+
const [url, init] = fetchMock.mock.calls.find(([input]) => String(input) === listEndpoint)!
|
|
138
|
+
expect(url).toBe(listEndpoint)
|
|
139
|
+
expect(init?.method).toBe('POST')
|
|
140
|
+
expect(init?.body).toBe('{}')
|
|
141
|
+
expect(String(url)).not.toContain(token)
|
|
142
|
+
expect(String(init?.body)).not.toContain(token)
|
|
143
|
+
expect(new Headers(init?.headers).get('authorization')).toBe(`Bearer ${token}`)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('keeps every entry exposed by one open application identity', async () => {
|
|
147
|
+
const payload = applicationListResponse() as {
|
|
148
|
+
rescode: number
|
|
149
|
+
data: {
|
|
150
|
+
current_identity: Record<string, unknown>
|
|
151
|
+
available_applications: Array<Record<string, unknown>>
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
payload.data.available_applications = [
|
|
155
|
+
{
|
|
156
|
+
xid: 0,
|
|
157
|
+
oid: 0,
|
|
158
|
+
application_name: 'OfficialWebsite',
|
|
159
|
+
application_scope: 'open',
|
|
160
|
+
application_title: '官方网站',
|
|
161
|
+
entry_name: 'main',
|
|
162
|
+
entry_url: 'https://website.example.com/',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
xid: 0,
|
|
166
|
+
oid: 0,
|
|
167
|
+
application_name: 'OfficialWebsite',
|
|
168
|
+
application_scope: 'open',
|
|
169
|
+
application_title: '个人中心',
|
|
170
|
+
entry_name: 'personal',
|
|
171
|
+
entry_url: 'https://website.example.com/personal',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
xid: 0,
|
|
175
|
+
oid: 0,
|
|
176
|
+
application_name: 'OfficialWebsite',
|
|
177
|
+
application_scope: 'open',
|
|
178
|
+
application_title: '组织管理',
|
|
179
|
+
entry_name: 'organizational',
|
|
180
|
+
entry_url: 'https://website.example.com/organizational',
|
|
181
|
+
},
|
|
182
|
+
payload.data.available_applications[1]!,
|
|
183
|
+
payload.data.available_applications[0]!,
|
|
184
|
+
]
|
|
185
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
186
|
+
return new Response(
|
|
187
|
+
JSON.stringify(
|
|
188
|
+
String(input) === organizationsEndpoint ? organizationListResponse() : payload,
|
|
189
|
+
),
|
|
190
|
+
{
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: { 'content-type': 'application/json' },
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
197
|
+
serviceBaseUrl,
|
|
198
|
+
fetch: fetchMock as typeof fetch,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
await expect(transport.listApplications('multiple-open-entries.jwt')).resolves.toMatchObject([
|
|
202
|
+
{ id: 'OfficialWebsite:0:main', name: '官方网站' },
|
|
203
|
+
{ id: 'OfficialWebsite:0:personal', name: '个人中心' },
|
|
204
|
+
{ id: 'OfficialWebsite:0:organizational', name: '组织管理' },
|
|
205
|
+
{ id: '22', name: '企业管理' },
|
|
206
|
+
{ id: '21', name: '平台运营后台' },
|
|
207
|
+
])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('does not infer applications for a locally selected organization without a new JWT', async () => {
|
|
211
|
+
const token = 'uniplat-base.organization.jwt'
|
|
212
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
213
|
+
return new Response(
|
|
214
|
+
JSON.stringify(
|
|
215
|
+
String(input) === organizationsEndpoint
|
|
216
|
+
? organizationListResponse()
|
|
217
|
+
: applicationListResponse(),
|
|
218
|
+
),
|
|
219
|
+
{
|
|
220
|
+
status: 200,
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
226
|
+
serviceBaseUrl,
|
|
227
|
+
fetch: fetchMock as typeof fetch,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
await transport.switchOrganization?.(token, '50')
|
|
231
|
+
const selectedEntries = await transport.listApplications(token)
|
|
232
|
+
const selectedOrganizations = await transport.listOrganizations?.(token)
|
|
233
|
+
|
|
234
|
+
expect(selectedEntries.map((entry) => entry.id)).toEqual(['900'])
|
|
235
|
+
expect(selectedOrganizations?.currentOrganizationId).toBe('50')
|
|
236
|
+
expect(fetchMock).toHaveBeenCalledTimes(4)
|
|
237
|
+
|
|
238
|
+
await transport.switchOrganization?.(token, '34')
|
|
239
|
+
expect((await transport.listApplications(token)).map((entry) => entry.id)).toEqual(['900'])
|
|
240
|
+
expect(fetchMock).toHaveBeenCalledTimes(6)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('resets the selected organization when the Host JWT changes', async () => {
|
|
244
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
245
|
+
return new Response(
|
|
246
|
+
JSON.stringify(
|
|
247
|
+
String(input) === organizationsEndpoint
|
|
248
|
+
? organizationListResponse()
|
|
249
|
+
: applicationListResponse(),
|
|
250
|
+
),
|
|
251
|
+
{
|
|
252
|
+
status: 200,
|
|
253
|
+
headers: { 'content-type': 'application/json' },
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
258
|
+
serviceBaseUrl,
|
|
259
|
+
fetch: fetchMock as typeof fetch,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
await transport.listApplications('organization-11.jwt')
|
|
263
|
+
await transport.switchOrganization?.('organization-11.jwt', '50')
|
|
264
|
+
expect(
|
|
265
|
+
(await transport.listApplications('organization-11.jwt')).map((entry) => entry.id),
|
|
266
|
+
).toEqual(['900'])
|
|
267
|
+
|
|
268
|
+
expect(
|
|
269
|
+
(await transport.listApplications('organization-11.refreshed.jwt')).map((entry) => entry.id),
|
|
270
|
+
).toEqual(['21', '22', '900'])
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('loads only applications when the internal workbench does not request organizations', async () => {
|
|
274
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
275
|
+
void input
|
|
276
|
+
return new Response(JSON.stringify(applicationListResponse()), {
|
|
277
|
+
status: 200,
|
|
278
|
+
headers: { 'content-type': 'application/json' },
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
282
|
+
serviceBaseUrl,
|
|
283
|
+
fetch: fetchMock as typeof fetch,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await transport.listApplications('internal-workbench.jwt')
|
|
287
|
+
|
|
288
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
289
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(listEndpoint)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('keeps the temporary oid=0 fallback when application_scope is absent', async () => {
|
|
293
|
+
const payload = applicationListResponse()
|
|
294
|
+
const legacyOpenApplication = payload.data.available_applications[2] as Record<string, unknown>
|
|
295
|
+
legacyOpenApplication.oid = 0
|
|
296
|
+
delete legacyOpenApplication.application_scope
|
|
297
|
+
const fetchMock = vi.fn(
|
|
298
|
+
async (input: RequestInfo | URL) =>
|
|
299
|
+
new Response(
|
|
300
|
+
JSON.stringify(
|
|
301
|
+
String(input) === organizationsEndpoint ? organizationListResponse() : payload,
|
|
302
|
+
),
|
|
303
|
+
{
|
|
304
|
+
status: 200,
|
|
305
|
+
headers: { 'content-type': 'application/json' },
|
|
306
|
+
},
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
310
|
+
serviceBaseUrl,
|
|
311
|
+
fetch: fetchMock as typeof fetch,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
await transport.switchOrganization?.('legacy-scope.test.jwt', '50')
|
|
315
|
+
|
|
316
|
+
expect(
|
|
317
|
+
(await transport.listApplications('legacy-scope.test.jwt')).map((entry) => entry.id),
|
|
318
|
+
).toEqual(['900'])
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('keeps all xid=0 open applications without selecting an organization implicitly', async () => {
|
|
322
|
+
const payload = applicationListResponse() as { rescode: number; data: Record<string, unknown> }
|
|
323
|
+
payload.data.current_identity = {
|
|
324
|
+
xid: 0,
|
|
325
|
+
oid: 0,
|
|
326
|
+
eid: 0,
|
|
327
|
+
mid: 0,
|
|
328
|
+
application_name: 'OfficialWebsite',
|
|
329
|
+
}
|
|
330
|
+
payload.data.available_applications = [
|
|
331
|
+
{
|
|
332
|
+
xid: 0,
|
|
333
|
+
oid: 0,
|
|
334
|
+
application_name: 'OfficialWebsite',
|
|
335
|
+
application_scope: 'open',
|
|
336
|
+
application_title: '官方网站',
|
|
337
|
+
entry_url: 'https://website.example.com/',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
xid: 0,
|
|
341
|
+
oid: 0,
|
|
342
|
+
application_name: 'PersonalSettings',
|
|
343
|
+
application_scope: 'open',
|
|
344
|
+
application_title: '个人设置',
|
|
345
|
+
entry_url: 'https://settings.example.com/',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
xid: 21,
|
|
349
|
+
oid: 11,
|
|
350
|
+
application_name: 'HrSaas',
|
|
351
|
+
application_scope: 'organization',
|
|
352
|
+
application_title: '数字人事部',
|
|
353
|
+
entry_url: 'https://hr.example.com/',
|
|
354
|
+
},
|
|
355
|
+
]
|
|
356
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
357
|
+
void init
|
|
358
|
+
const response =
|
|
359
|
+
String(input) === issueTicketEndpoint
|
|
360
|
+
? {
|
|
361
|
+
rescode: 0,
|
|
362
|
+
data: { ticket: applicationTicket, expires_in: 15, source_system: sourceSystem },
|
|
363
|
+
}
|
|
364
|
+
: String(input) === organizationsEndpoint
|
|
365
|
+
? organizationListResponse()
|
|
366
|
+
: payload
|
|
367
|
+
return new Response(JSON.stringify(response), {
|
|
368
|
+
status: 200,
|
|
369
|
+
headers: { 'content-type': 'application/json' },
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
373
|
+
serviceBaseUrl,
|
|
374
|
+
fetch: fetchMock as typeof fetch,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
await expect(transport.listApplications('open-application.jwt')).resolves.toEqual([
|
|
378
|
+
{
|
|
379
|
+
id: 'OfficialWebsite:0',
|
|
380
|
+
name: '官方网站',
|
|
381
|
+
url: 'https://website.example.com/',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: 'PersonalSettings:0',
|
|
385
|
+
name: '个人设置',
|
|
386
|
+
url: 'https://settings.example.com/',
|
|
387
|
+
},
|
|
388
|
+
])
|
|
389
|
+
await expect(transport.listOrganizations?.('open-application.jwt')).resolves.toMatchObject({
|
|
390
|
+
currentOrganizationId: '',
|
|
391
|
+
})
|
|
392
|
+
await transport.switchApplication?.('open-application.jwt', 'PersonalSettings:0')
|
|
393
|
+
const [, issueInit] = fetchMock.mock.calls.find(
|
|
394
|
+
([input]) => String(input) === issueTicketEndpoint,
|
|
395
|
+
)!
|
|
396
|
+
expect(issueInit?.body).toBe(JSON.stringify({}))
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('requests a short-lived ticket for an application from the current organization', async () => {
|
|
400
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
401
|
+
void init
|
|
402
|
+
const payload =
|
|
403
|
+
String(input) === issueTicketEndpoint
|
|
404
|
+
? {
|
|
405
|
+
rescode: 0,
|
|
406
|
+
data: { ticket: applicationTicket, expires_in: 15, source_system: sourceSystem },
|
|
407
|
+
}
|
|
408
|
+
: String(input) === organizationsEndpoint
|
|
409
|
+
? organizationListResponse()
|
|
410
|
+
: applicationListResponse()
|
|
411
|
+
return new Response(JSON.stringify(payload), {
|
|
412
|
+
status: 200,
|
|
413
|
+
headers: { 'content-type': 'application/json' },
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
417
|
+
serviceBaseUrl,
|
|
418
|
+
fetch: fetchMock as typeof fetch,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const result = await transport.switchApplication?.('current.application.jwt', '22')
|
|
422
|
+
|
|
423
|
+
expect(result).toEqual({
|
|
424
|
+
url:
|
|
425
|
+
`https://apps.example.com/enterprise#uniplat_identity_ticket=${applicationTicket}` +
|
|
426
|
+
`&source_system=${sourceSystem}`,
|
|
427
|
+
})
|
|
428
|
+
const [, issueInit] = fetchMock.mock.calls.find(
|
|
429
|
+
([input]) => String(input) === issueTicketEndpoint,
|
|
430
|
+
)!
|
|
431
|
+
expect(issueInit?.body).toBe(JSON.stringify({}))
|
|
432
|
+
expect(String(issueInit?.body)).not.toContain(applicationTicket)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('requests an external application ticket without local identity fields', async () => {
|
|
436
|
+
const listPayload = applicationListResponse()
|
|
437
|
+
const availableApplications = listPayload.data.available_applications as Array<
|
|
438
|
+
Record<string, unknown>
|
|
439
|
+
>
|
|
440
|
+
availableApplications.push({
|
|
441
|
+
xid: 214,
|
|
442
|
+
oid: 11,
|
|
443
|
+
application_name: 'hro',
|
|
444
|
+
application_scope: 'organization',
|
|
445
|
+
application_title: 'HRO',
|
|
446
|
+
entry_url: 'http://a.web.test.hrs100.cn/oidc-redirect',
|
|
447
|
+
open_mode: 'blank',
|
|
448
|
+
launch_target: {
|
|
449
|
+
type: 'external',
|
|
450
|
+
target_system: 'hro',
|
|
451
|
+
application_name: 'HRS100',
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
const launchUrl =
|
|
455
|
+
`http://a.web.test.hrs100.cn/oidc-redirect` +
|
|
456
|
+
`#uniplat_identity_ticket=${applicationTicket}` +
|
|
457
|
+
`&source_system=${sourceSystem}`
|
|
458
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
459
|
+
void init
|
|
460
|
+
const payload =
|
|
461
|
+
String(input) === issueTicketEndpoint
|
|
462
|
+
? {
|
|
463
|
+
rescode: 0,
|
|
464
|
+
data: { ticket: applicationTicket, expires_in: 15, source_system: sourceSystem },
|
|
465
|
+
}
|
|
466
|
+
: String(input) === organizationsEndpoint
|
|
467
|
+
? organizationListResponse()
|
|
468
|
+
: listPayload
|
|
469
|
+
return new Response(JSON.stringify(payload), {
|
|
470
|
+
status: 200,
|
|
471
|
+
headers: { 'content-type': 'application/json' },
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
475
|
+
serviceBaseUrl,
|
|
476
|
+
fetch: fetchMock as typeof fetch,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
await expect(transport.switchApplication?.('current.application.jwt', '214')).resolves.toEqual({
|
|
480
|
+
url: launchUrl,
|
|
481
|
+
})
|
|
482
|
+
const [, issueInit] = fetchMock.mock.calls.find(
|
|
483
|
+
([input]) => String(input) === issueTicketEndpoint,
|
|
484
|
+
)!
|
|
485
|
+
expect(issueInit?.body).toBe(JSON.stringify({}))
|
|
486
|
+
expect(String(issueInit?.body)).not.toContain('target_system')
|
|
487
|
+
expect(String(issueInit?.body)).not.toContain('application_name')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('rejects an incomplete external application target instead of falling back to local', async () => {
|
|
491
|
+
const payload = applicationListResponse()
|
|
492
|
+
const application = payload.data.available_applications[1] as Record<string, unknown>
|
|
493
|
+
application.launch_target = {
|
|
494
|
+
type: 'external',
|
|
495
|
+
target_system: 'hro',
|
|
496
|
+
}
|
|
497
|
+
const fetchMock = vi.fn(
|
|
498
|
+
async () =>
|
|
499
|
+
new Response(JSON.stringify(payload), {
|
|
500
|
+
status: 200,
|
|
501
|
+
headers: { 'content-type': 'application/json' },
|
|
502
|
+
}),
|
|
503
|
+
)
|
|
504
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
505
|
+
serviceBaseUrl,
|
|
506
|
+
fetch: fetchMock as typeof fetch,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
await expect(transport.listApplications('current.application.jwt')).rejects.toThrow(
|
|
510
|
+
'UniplatBase external application target is invalid',
|
|
511
|
+
)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('binds an open application ticket to the selected organization', async () => {
|
|
515
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
516
|
+
void init
|
|
517
|
+
const payload =
|
|
518
|
+
String(input) === issueTicketEndpoint
|
|
519
|
+
? {
|
|
520
|
+
rescode: 0,
|
|
521
|
+
data: { ticket: applicationTicket, expires_in: 15, source_system: sourceSystem },
|
|
522
|
+
}
|
|
523
|
+
: String(input) === organizationsEndpoint
|
|
524
|
+
? organizationListResponse()
|
|
525
|
+
: applicationListResponse()
|
|
526
|
+
return new Response(JSON.stringify(payload), {
|
|
527
|
+
status: 200,
|
|
528
|
+
headers: { 'content-type': 'application/json' },
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
532
|
+
serviceBaseUrl,
|
|
533
|
+
fetch: fetchMock as typeof fetch,
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
await expect(transport.switchApplication?.('current.application.jwt', '120')).rejects.toThrow(
|
|
537
|
+
'Application is unavailable',
|
|
538
|
+
)
|
|
539
|
+
const result = await transport.switchApplication?.('current.application.jwt', '900')
|
|
540
|
+
|
|
541
|
+
expect(result).toEqual({
|
|
542
|
+
url:
|
|
543
|
+
`${new URL('/portal/', document.baseURI).href}#uniplat_identity_ticket=${applicationTicket}` +
|
|
544
|
+
`&source_system=${sourceSystem}`,
|
|
545
|
+
})
|
|
546
|
+
const [, issueInit] = fetchMock.mock.calls.find(
|
|
547
|
+
([input]) => String(input) === issueTicketEndpoint,
|
|
548
|
+
)!
|
|
549
|
+
expect(issueInit?.body).toBe(JSON.stringify({}))
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('rejects the legacy direct-JWT response in ticket mode without changing Host JWT', async () => {
|
|
553
|
+
const setAccessToken = vi.fn()
|
|
554
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
555
|
+
const payload =
|
|
556
|
+
String(input) === issueTicketEndpoint
|
|
557
|
+
? { rescode: 0, data: { token: 'legacy.target.jwt' } }
|
|
558
|
+
: String(input) === organizationsEndpoint
|
|
559
|
+
? organizationListResponse()
|
|
560
|
+
: applicationListResponse()
|
|
561
|
+
return new Response(JSON.stringify(payload), {
|
|
562
|
+
status: 200,
|
|
563
|
+
headers: { 'content-type': 'application/json' },
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
567
|
+
serviceBaseUrl,
|
|
568
|
+
setAccessToken,
|
|
569
|
+
fetch: fetchMock as typeof fetch,
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
await expect(transport.switchApplication?.('current.application.jwt', '900')).rejects.toThrow(
|
|
573
|
+
'Identity ticket response is invalid',
|
|
574
|
+
)
|
|
575
|
+
expect(setAccessToken).not.toHaveBeenCalled()
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('rejects backend failures without exposing the backend message', async () => {
|
|
579
|
+
const fetchMock = vi.fn(
|
|
580
|
+
async () =>
|
|
581
|
+
new Response(
|
|
582
|
+
JSON.stringify({ rescode: 403, msg: 'stack with jwt=backend-secret', data: {} }),
|
|
583
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
587
|
+
serviceBaseUrl,
|
|
588
|
+
fetch: fetchMock as typeof fetch,
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
const error = await transport
|
|
592
|
+
.listApplications('safe.fake.jwt')
|
|
593
|
+
.catch((reason: unknown) => reason)
|
|
594
|
+
expect(error).toBeInstanceOf(Error)
|
|
595
|
+
expect(String(error)).not.toContain('backend-secret')
|
|
596
|
+
expect(String(error)).not.toContain('stack with')
|
|
597
|
+
})
|
|
598
|
+
})
|