@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.
Files changed (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. 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
  7. 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
  8. 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
  9. 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
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,443 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import {
5
+ createUniplatBaseUserLoginTransport,
6
+ UserLoginTransportError,
7
+ type UniplatBaseUserLoginMetadata,
8
+ } from '../src'
9
+
10
+ describe('createUniplatBaseUserLoginTransport()', () => {
11
+ it('uses the SMS and application login endpoints without an image captcha', async () => {
12
+ const accessToken = 'obviously-fake-login-jwt'
13
+ const setAccessToken = vi.fn(async (token: string, metadata: UniplatBaseUserLoginMetadata) => {
14
+ void token
15
+ void metadata
16
+ })
17
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
18
+ void init
19
+ const data = String(input).endsWith('/login-application')
20
+ ? {
21
+ token: accessToken,
22
+ application_name: 'HrSaas',
23
+ is_new_user: true,
24
+ user_id: 7,
25
+ org_list: [{ xid: 21, name: '示例团队' }],
26
+ }
27
+ : {}
28
+ return new Response(JSON.stringify({ rescode: 0, data }), {
29
+ status: 200,
30
+ headers: { 'content-type': 'application/json' },
31
+ })
32
+ })
33
+ const transport = createUniplatBaseUserLoginTransport({
34
+ apiBaseUrl: 'https://api.example.com/api',
35
+ setAccessToken,
36
+ fetch: fetchMock as typeof fetch,
37
+ deviceInfo: {
38
+ deviceType: 'web',
39
+ deviceName: 'Chrome',
40
+ deviceId: 'host-device-123',
41
+ deviceBrand: 'MacIntel',
42
+ },
43
+ })
44
+
45
+ await transport.sendSmsCode({
46
+ mobile: '13800138000',
47
+ })
48
+ expect(
49
+ await transport.login({
50
+ mobile: '13800138000',
51
+ smsCode: '123456',
52
+ applicationName: 'HrSaas',
53
+ }),
54
+ ).toEqual({ applicationName: 'HrSaas', isNewUser: true })
55
+
56
+ expect(fetchMock).toHaveBeenCalledTimes(2)
57
+ const [smsUrl, smsInit] = fetchMock.mock.calls[0]!
58
+ expect(smsUrl).toBe(
59
+ 'https://api.example.com/api/general/project/uniplat_base/service/anonymous/system.user/sendVerifyCode',
60
+ )
61
+ expect(smsInit?.body).toBe(JSON.stringify({ mobile: '13800138000' }))
62
+ const [loginUrl, loginInit] = fetchMock.mock.calls[1]!
63
+ expect(loginUrl).toBe(
64
+ 'https://api.example.com/api/general/project/uniplat_base/service/anonymous/application.identity/login-application',
65
+ )
66
+ expect(loginInit?.body).toBe(
67
+ JSON.stringify({
68
+ auth_method: 'sms',
69
+ mobile: '13800138000',
70
+ verify_code: '123456',
71
+ application_name: 'HrSaas',
72
+ client_id: 'HrSaas',
73
+ device_type: 'web',
74
+ device_name: 'Chrome',
75
+ device_id: 'host-device-123',
76
+ device_brand: 'MacIntel',
77
+ }),
78
+ )
79
+ expect(String(smsUrl)).not.toContain(accessToken)
80
+ expect(String(loginUrl)).not.toContain(accessToken)
81
+ expect(String(smsInit?.body)).not.toContain(accessToken)
82
+ expect(String(loginInit?.body)).not.toContain(accessToken)
83
+ expect(setAccessToken).toHaveBeenCalledWith(accessToken, {
84
+ application_name: 'HrSaas',
85
+ is_new_user: true,
86
+ user_id: 7,
87
+ org_list: [{ xid: 21, name: '示例团队' }],
88
+ })
89
+ expect(JSON.stringify(setAccessToken.mock.calls[0]?.[1])).not.toContain(accessToken)
90
+ })
91
+
92
+ it('does not expose backend error text', async () => {
93
+ const fetchMock = vi.fn(
94
+ async () =>
95
+ new Response(
96
+ JSON.stringify({
97
+ rescode: 500,
98
+ error_code: 'SMS_CODE_INVALID_OR_EXPIRED',
99
+ msg: 'internal stack with a credential',
100
+ data: {},
101
+ }),
102
+ { status: 200, headers: { 'content-type': 'application/json' } },
103
+ ),
104
+ )
105
+ const transport = createUniplatBaseUserLoginTransport({
106
+ apiBaseUrl: 'https://api.example.com/api',
107
+ setAccessToken: vi.fn(),
108
+ fetch: fetchMock as typeof fetch,
109
+ })
110
+
111
+ const error = await transport
112
+ .login({ mobile: '13800138000', smsCode: '123456', applicationName: 'HrSaas' })
113
+ .catch((reason: unknown) => reason)
114
+ expect(error).toBeInstanceOf(Error)
115
+ expect(error).toBeInstanceOf(UserLoginTransportError)
116
+ expect((error as UserLoginTransportError).code).toBe('SMS_CODE_INVALID_OR_EXPIRED')
117
+ expect(String(error)).not.toContain('internal stack')
118
+ expect(String(error)).not.toContain('credential')
119
+ })
120
+
121
+ it('retains a safe organization access denial message for component display', async () => {
122
+ const fetchMock = vi.fn(
123
+ async () =>
124
+ new Response(
125
+ JSON.stringify({
126
+ rescode: 999,
127
+ error_code: 'ORG_ACCESS_DENIED',
128
+ msg: '当前用户没有可用的平台设置应用实例',
129
+ data: {},
130
+ }),
131
+ { status: 200, headers: { 'content-type': 'application/json' } },
132
+ ),
133
+ )
134
+ const transport = createUniplatBaseUserLoginTransport({
135
+ apiBaseUrl: 'https://api.example.com/api',
136
+ setAccessToken: vi.fn(),
137
+ fetch: fetchMock as typeof fetch,
138
+ })
139
+
140
+ const error = await transport
141
+ .login({ mobile: '13800138000', smsCode: '123456', applicationName: 'qqxb-plat-admin' })
142
+ .catch((reason: unknown) => reason)
143
+
144
+ expect(error).toBeInstanceOf(UserLoginTransportError)
145
+ expect((error as UserLoginTransportError).code).toBe('ORG_ACCESS_DENIED')
146
+ expect((error as UserLoginTransportError).displayMessage).toBe(
147
+ '当前用户没有可用的平台设置应用实例',
148
+ )
149
+ })
150
+
151
+ it('keeps legacy success responses without is_new_user compatible', async () => {
152
+ const fetchMock = vi.fn(
153
+ async () =>
154
+ new Response(
155
+ JSON.stringify({
156
+ rescode: 0,
157
+ data: { token: 'obviously-fake-legacy-jwt', application_name: 'HrSaas' },
158
+ }),
159
+ { status: 200, headers: { 'content-type': 'application/json' } },
160
+ ),
161
+ )
162
+ const transport = createUniplatBaseUserLoginTransport({
163
+ apiBaseUrl: 'https://api.example.com/api',
164
+ setAccessToken: vi.fn(),
165
+ fetch: fetchMock as typeof fetch,
166
+ })
167
+
168
+ await expect(
169
+ transport.login({
170
+ mobile: '13800138000',
171
+ smsCode: '123456',
172
+ applicationName: 'HrSaas',
173
+ }),
174
+ ).resolves.toEqual({ applicationName: 'HrSaas' })
175
+ })
176
+
177
+ it('logs in with a password through login-application and strips credentials from metadata', async () => {
178
+ const accessToken = 'obviously-fake-password-login-jwt'
179
+ const setAccessToken = vi.fn()
180
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
181
+ void input
182
+ void init
183
+ return new Response(
184
+ JSON.stringify({
185
+ rescode: 0,
186
+ data: {
187
+ token: accessToken,
188
+ application_name: 'OfficialWebsite',
189
+ is_new_user: false,
190
+ user_id: 7,
191
+ },
192
+ }),
193
+ { status: 200, headers: { 'content-type': 'application/json' } },
194
+ )
195
+ })
196
+ const transport = createUniplatBaseUserLoginTransport({
197
+ apiBaseUrl: 'https://api.example.com/api',
198
+ setAccessToken,
199
+ fetch: fetchMock as typeof fetch,
200
+ deviceInfo: {
201
+ deviceType: 'web',
202
+ deviceName: 'Chrome',
203
+ deviceId: 'host-device-123',
204
+ deviceBrand: 'MacIntel',
205
+ },
206
+ })
207
+
208
+ await expect(
209
+ transport.loginWithPassword?.({
210
+ mobile: '13800138000',
211
+ password: 'safe-test-password',
212
+ applicationName: 'OfficialWebsite',
213
+ rememberSession: true,
214
+ }),
215
+ ).resolves.toEqual({ applicationName: 'OfficialWebsite', isNewUser: false })
216
+
217
+ const [url, init] = fetchMock.mock.calls[0]!
218
+ expect(url).toBe(
219
+ 'https://api.example.com/api/general/project/uniplat_base/service/anonymous/application.identity/login-application',
220
+ )
221
+ expect(init?.body).toBe(
222
+ JSON.stringify({
223
+ auth_method: 'password',
224
+ mobile: '13800138000',
225
+ password: 'safe-test-password',
226
+ application_name: 'OfficialWebsite',
227
+ client_id: 'OfficialWebsite',
228
+ remember_session: true,
229
+ device_type: 'web',
230
+ device_name: 'Chrome',
231
+ device_id: 'host-device-123',
232
+ device_brand: 'MacIntel',
233
+ }),
234
+ )
235
+ expect(setAccessToken).toHaveBeenCalledWith(accessToken, {
236
+ application_name: 'OfficialWebsite',
237
+ is_new_user: false,
238
+ user_id: 7,
239
+ })
240
+ expect(JSON.stringify(setAccessToken.mock.calls[0]?.[1])).not.toContain(accessToken)
241
+ expect(JSON.stringify(setAccessToken.mock.calls[0]?.[1])).not.toContain('safe-test-password')
242
+ })
243
+
244
+ it('creates and polls a QR login before atomically saving the confirmed JWT', async () => {
245
+ const accessToken = 'obviously-fake-qr-login-jwt'
246
+ const challengeId = 'obviously-fake-qr-challenge-id-1234567890'
247
+ const setAccessToken = vi.fn()
248
+ let pollCount = 0
249
+ const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
250
+ const body = JSON.parse(String(init?.body)) as Record<string, unknown>
251
+ let data: Record<string, unknown>
252
+ if (body.action === 'create') {
253
+ data = {
254
+ challenge_id: challengeId,
255
+ image_data_url: 'data:image/png;base64,b2J2aW91c2x5LWZha2UtcG5n',
256
+ expires_in: 120,
257
+ poll_interval_ms: 2000,
258
+ }
259
+ } else if (pollCount++ === 0) {
260
+ data = { status: 'scanned' }
261
+ } else {
262
+ data = {
263
+ status: 'confirmed',
264
+ token: accessToken,
265
+ application_name: 'OfficialWebsite',
266
+ is_new_user: false,
267
+ user_id: 7,
268
+ }
269
+ }
270
+ return new Response(JSON.stringify({ rescode: 0, data }), {
271
+ status: 200,
272
+ headers: { 'content-type': 'application/json' },
273
+ })
274
+ })
275
+ const transport = createUniplatBaseUserLoginTransport({
276
+ apiBaseUrl: 'https://api.example.com/api',
277
+ setAccessToken,
278
+ fetch: fetchMock as typeof fetch,
279
+ deviceInfo: {
280
+ deviceType: 'web',
281
+ deviceName: 'Chrome',
282
+ deviceId: 'host-device-123',
283
+ deviceBrand: 'MacIntel',
284
+ },
285
+ })
286
+
287
+ await expect(
288
+ transport.createQrChallenge?.({ applicationName: 'OfficialWebsite' }),
289
+ ).resolves.toEqual({
290
+ challengeId,
291
+ imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2UtcG5n',
292
+ expiresInSeconds: 120,
293
+ pollIntervalMs: 2000,
294
+ })
295
+ await expect(transport.pollQrChallenge?.(challengeId, 'OfficialWebsite')).resolves.toEqual({
296
+ status: 'scanned',
297
+ })
298
+ await expect(transport.pollQrChallenge?.(challengeId, 'OfficialWebsite')).resolves.toEqual({
299
+ status: 'confirmed',
300
+ result: { applicationName: 'OfficialWebsite', isNewUser: false },
301
+ })
302
+
303
+ const requestBodies = fetchMock.mock.calls.map(([, init]) => JSON.parse(String(init?.body)))
304
+ expect(requestBodies).toEqual([
305
+ {
306
+ auth_method: 'qr',
307
+ action: 'create',
308
+ application_name: 'OfficialWebsite',
309
+ client_id: 'OfficialWebsite',
310
+ device_type: 'web',
311
+ device_name: 'Chrome',
312
+ device_id: 'host-device-123',
313
+ device_brand: 'MacIntel',
314
+ },
315
+ {
316
+ auth_method: 'qr',
317
+ action: 'poll',
318
+ application_name: 'OfficialWebsite',
319
+ client_id: 'OfficialWebsite',
320
+ challenge_id: challengeId,
321
+ device_type: 'web',
322
+ device_name: 'Chrome',
323
+ device_id: 'host-device-123',
324
+ device_brand: 'MacIntel',
325
+ },
326
+ {
327
+ auth_method: 'qr',
328
+ action: 'poll',
329
+ application_name: 'OfficialWebsite',
330
+ client_id: 'OfficialWebsite',
331
+ challenge_id: challengeId,
332
+ device_type: 'web',
333
+ device_name: 'Chrome',
334
+ device_id: 'host-device-123',
335
+ device_brand: 'MacIntel',
336
+ },
337
+ ])
338
+ expect(setAccessToken).toHaveBeenCalledTimes(1)
339
+ expect(setAccessToken).toHaveBeenCalledWith(accessToken, {
340
+ status: 'confirmed',
341
+ application_name: 'OfficialWebsite',
342
+ is_new_user: false,
343
+ user_id: 7,
344
+ })
345
+ expect(JSON.stringify(setAccessToken.mock.calls[0]?.[1])).not.toContain(challengeId)
346
+ expect(JSON.stringify(setAccessToken.mock.calls[0]?.[1])).not.toContain(accessToken)
347
+ })
348
+
349
+ it('maps QR expiry and rejection without exposing backend text', async () => {
350
+ const responses = [
351
+ {
352
+ rescode: 999,
353
+ error_code: 'QR_CHALLENGE_EXPIRED',
354
+ msg: 'internal expiry detail with credential',
355
+ },
356
+ {
357
+ rescode: 999,
358
+ error_code: 'QR_LOGIN_REJECTED',
359
+ msg: 'internal rejection detail with credential',
360
+ },
361
+ ]
362
+ const fetchMock = vi.fn(
363
+ async () =>
364
+ new Response(JSON.stringify(responses.shift()), {
365
+ status: 200,
366
+ headers: { 'content-type': 'application/json' },
367
+ }),
368
+ )
369
+ const transport = createUniplatBaseUserLoginTransport({
370
+ apiBaseUrl: 'https://api.example.com/api',
371
+ applicationName: 'OfficialWebsite',
372
+ setAccessToken: vi.fn(),
373
+ fetch: fetchMock as typeof fetch,
374
+ })
375
+ const challengeId = 'obviously-fake-qr-challenge-id-1234567890'
376
+
377
+ await expect(transport.pollQrChallenge?.(challengeId)).resolves.toEqual({ status: 'expired' })
378
+ await expect(transport.pollQrChallenge?.(challengeId)).resolves.toEqual({ status: 'rejected' })
379
+ })
380
+
381
+ it('automatically reports a stable browser fingerprint device context by default', async () => {
382
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
383
+ void input
384
+ void init
385
+ return new Response(
386
+ JSON.stringify({
387
+ rescode: 0,
388
+ data: { token: 'obviously-fake-default-device-jwt', application_name: 'HrSaas' },
389
+ }),
390
+ { status: 200, headers: { 'content-type': 'application/json' } },
391
+ )
392
+ })
393
+ const transport = createUniplatBaseUserLoginTransport({
394
+ apiBaseUrl: 'https://api.example.com/api',
395
+ setAccessToken: vi.fn(),
396
+ fetch: fetchMock as typeof fetch,
397
+ })
398
+
399
+ await transport.login({
400
+ mobile: '13800138000',
401
+ smsCode: '123456',
402
+ applicationName: 'HrSaas',
403
+ })
404
+
405
+ const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)) as Record<string, unknown>
406
+ expect(body).toMatchObject({
407
+ auth_method: 'sms',
408
+ application_name: 'HrSaas',
409
+ client_id: 'HrSaas',
410
+ device_type: 'web',
411
+ })
412
+ expect(body.device_name).toEqual(expect.any(String))
413
+ expect(body.device_brand).toEqual(expect.any(String))
414
+ expect(body.device_id).toEqual(expect.stringMatching(/^web-/))
415
+
416
+ const secondFetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
417
+ void input
418
+ void init
419
+ return new Response(
420
+ JSON.stringify({
421
+ rescode: 0,
422
+ data: { token: 'obviously-fake-second-device-jwt', application_name: 'HrSaas' },
423
+ }),
424
+ { status: 200, headers: { 'content-type': 'application/json' } },
425
+ )
426
+ })
427
+ const secondTransport = createUniplatBaseUserLoginTransport({
428
+ apiBaseUrl: 'https://api.example.com/api',
429
+ setAccessToken: vi.fn(),
430
+ fetch: secondFetchMock as typeof fetch,
431
+ })
432
+ await secondTransport.login({
433
+ mobile: '13800138000',
434
+ smsCode: '123456',
435
+ applicationName: 'HrSaas',
436
+ })
437
+ const secondBody = JSON.parse(String(secondFetchMock.mock.calls[0]?.[1]?.body)) as Record<
438
+ string,
439
+ unknown
440
+ >
441
+ expect(secondBody.device_id).toBe(body.device_id)
442
+ })
443
+ })