@55387.ai/uniauth-client 1.1.1 → 1.1.2
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/dist/index.cjs +65 -4
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +65 -4
- package/package.json +1 -1
- package/src/index.ts +77 -4
- package/tests/client.test.ts +228 -0
- package/vitest.config.ts +14 -0
package/dist/index.cjs
CHANGED
|
@@ -270,11 +270,71 @@ var UniAuthClient = class {
|
|
|
270
270
|
* Send verification code to phone number
|
|
271
271
|
* 发送验证码到手机号
|
|
272
272
|
*
|
|
273
|
-
* @param phone - Phone number
|
|
273
|
+
* @param phone - Phone number in E.164 format (e.g., +8613800138000)
|
|
274
274
|
* @param type - Purpose of the verification code
|
|
275
275
|
* @param captchaToken - Captcha verification token from slider captcha
|
|
276
276
|
*/
|
|
277
277
|
async sendCode(phone, type = "login", captchaToken) {
|
|
278
|
+
if (!phone || typeof phone !== "string") {
|
|
279
|
+
throw this.createError("INVALID_PHONE", "Phone number is required / \u8BF7\u8F93\u5165\u624B\u673A\u53F7");
|
|
280
|
+
}
|
|
281
|
+
const countryValidation = {
|
|
282
|
+
// China: +86 followed by 11 digits starting with 1
|
|
283
|
+
"+86": {
|
|
284
|
+
regex: /^\+861[3-9]\d{9}$/,
|
|
285
|
+
example: "+8613800138000",
|
|
286
|
+
name: "\u4E2D\u56FD"
|
|
287
|
+
},
|
|
288
|
+
// USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
|
|
289
|
+
"+1": {
|
|
290
|
+
regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
|
|
291
|
+
example: "+14155552671",
|
|
292
|
+
name: "USA/Canada"
|
|
293
|
+
},
|
|
294
|
+
// Australia: +61 followed by 9 digits starting with 4 (mobile)
|
|
295
|
+
"+61": {
|
|
296
|
+
regex: /^\+614\d{8}$/,
|
|
297
|
+
example: "+61412345678",
|
|
298
|
+
name: "Australia"
|
|
299
|
+
},
|
|
300
|
+
// UK: +44 followed by 10 digits starting with 7 (mobile)
|
|
301
|
+
"+44": {
|
|
302
|
+
regex: /^\+447\d{9}$/,
|
|
303
|
+
example: "+447911123456",
|
|
304
|
+
name: "UK"
|
|
305
|
+
},
|
|
306
|
+
// Japan: +81 followed by 10-11 digits
|
|
307
|
+
"+81": {
|
|
308
|
+
regex: /^\+81[789]0\d{8}$/,
|
|
309
|
+
example: "+818012345678",
|
|
310
|
+
name: "Japan"
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
let countryCode = "";
|
|
314
|
+
let validation = null;
|
|
315
|
+
for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
|
|
316
|
+
if (phone.startsWith(code)) {
|
|
317
|
+
countryCode = code;
|
|
318
|
+
validation = countryValidation[code];
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (validation) {
|
|
323
|
+
if (!validation.regex.test(phone)) {
|
|
324
|
+
throw this.createError(
|
|
325
|
+
"INVALID_PHONE_FORMAT",
|
|
326
|
+
`Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}\u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u793A\u4F8B\uFF1A${validation.example}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const e164Regex = /^\+[1-9]\d{6,14}$/;
|
|
331
|
+
if (!e164Regex.test(phone)) {
|
|
332
|
+
throw this.createError(
|
|
333
|
+
"INVALID_PHONE_FORMAT",
|
|
334
|
+
"Phone number must be in E.164 format (e.g., +8613800138000) / \u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F7F\u7528\u56FD\u9645\u683C\u5F0F\uFF08\u5982 +8613800138000\uFF09"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
278
338
|
const response = await this.request("/api/v1/auth/phone/send-code", {
|
|
279
339
|
method: "POST",
|
|
280
340
|
body: JSON.stringify({
|
|
@@ -367,10 +427,10 @@ var UniAuthClient = class {
|
|
|
367
427
|
* Handle OAuth callback (for social login)
|
|
368
428
|
* 处理 OAuth 回调(社交登录)
|
|
369
429
|
*/
|
|
370
|
-
async handleOAuthCallback(provider, code) {
|
|
371
|
-
const response = await this.request(
|
|
430
|
+
async handleOAuthCallback(provider, code, redirectUri) {
|
|
431
|
+
const response = await this.request(`/api/v1/auth/oauth/${provider}/callback`, {
|
|
372
432
|
method: "POST",
|
|
373
|
-
body: JSON.stringify({
|
|
433
|
+
body: JSON.stringify({ code, redirect_uri: redirectUri })
|
|
374
434
|
});
|
|
375
435
|
if (!response.success || !response.data) {
|
|
376
436
|
throw this.createError(response.error?.code || "OAUTH_FAILED", response.error?.message || "OAuth callback failed");
|
|
@@ -963,6 +1023,7 @@ var UniAuthClient = class {
|
|
|
963
1023
|
this.storage.setAccessToken(response.data.access_token);
|
|
964
1024
|
this.storage.setRefreshToken(response.data.refresh_token);
|
|
965
1025
|
this.config.onTokenRefresh?.(response.data);
|
|
1026
|
+
this.notifyAuthStateChange(this.currentUser);
|
|
966
1027
|
return true;
|
|
967
1028
|
} catch (error) {
|
|
968
1029
|
this.storage.clear();
|
package/dist/index.d.cts
CHANGED
|
@@ -88,6 +88,8 @@ interface UniAuthConfig {
|
|
|
88
88
|
appKey?: string;
|
|
89
89
|
/** OAuth2 Client ID (for OAuth flows) */
|
|
90
90
|
clientId?: string;
|
|
91
|
+
/** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
|
|
92
|
+
clientSecret?: string;
|
|
91
93
|
/** Storage type for tokens */
|
|
92
94
|
storage?: 'localStorage' | 'sessionStorage' | 'memory';
|
|
93
95
|
/** Callback when tokens are refreshed */
|
|
@@ -238,7 +240,7 @@ declare class UniAuthClient {
|
|
|
238
240
|
* Send verification code to phone number
|
|
239
241
|
* 发送验证码到手机号
|
|
240
242
|
*
|
|
241
|
-
* @param phone - Phone number
|
|
243
|
+
* @param phone - Phone number in E.164 format (e.g., +8613800138000)
|
|
242
244
|
* @param type - Purpose of the verification code
|
|
243
245
|
* @param captchaToken - Captcha verification token from slider captcha
|
|
244
246
|
*/
|
|
@@ -271,7 +273,7 @@ declare class UniAuthClient {
|
|
|
271
273
|
* Handle OAuth callback (for social login)
|
|
272
274
|
* 处理 OAuth 回调(社交登录)
|
|
273
275
|
*/
|
|
274
|
-
handleOAuthCallback(provider: string, code: string): Promise<LoginResult>;
|
|
276
|
+
handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult>;
|
|
275
277
|
/**
|
|
276
278
|
* Register with email and password
|
|
277
279
|
* 使用邮箱密码注册
|
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ interface UniAuthConfig {
|
|
|
88
88
|
appKey?: string;
|
|
89
89
|
/** OAuth2 Client ID (for OAuth flows) */
|
|
90
90
|
clientId?: string;
|
|
91
|
+
/** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
|
|
92
|
+
clientSecret?: string;
|
|
91
93
|
/** Storage type for tokens */
|
|
92
94
|
storage?: 'localStorage' | 'sessionStorage' | 'memory';
|
|
93
95
|
/** Callback when tokens are refreshed */
|
|
@@ -238,7 +240,7 @@ declare class UniAuthClient {
|
|
|
238
240
|
* Send verification code to phone number
|
|
239
241
|
* 发送验证码到手机号
|
|
240
242
|
*
|
|
241
|
-
* @param phone - Phone number
|
|
243
|
+
* @param phone - Phone number in E.164 format (e.g., +8613800138000)
|
|
242
244
|
* @param type - Purpose of the verification code
|
|
243
245
|
* @param captchaToken - Captcha verification token from slider captcha
|
|
244
246
|
*/
|
|
@@ -271,7 +273,7 @@ declare class UniAuthClient {
|
|
|
271
273
|
* Handle OAuth callback (for social login)
|
|
272
274
|
* 处理 OAuth 回调(社交登录)
|
|
273
275
|
*/
|
|
274
|
-
handleOAuthCallback(provider: string, code: string): Promise<LoginResult>;
|
|
276
|
+
handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult>;
|
|
275
277
|
/**
|
|
276
278
|
* Register with email and password
|
|
277
279
|
* 使用邮箱密码注册
|
package/dist/index.js
CHANGED
|
@@ -236,11 +236,71 @@ var UniAuthClient = class {
|
|
|
236
236
|
* Send verification code to phone number
|
|
237
237
|
* 发送验证码到手机号
|
|
238
238
|
*
|
|
239
|
-
* @param phone - Phone number
|
|
239
|
+
* @param phone - Phone number in E.164 format (e.g., +8613800138000)
|
|
240
240
|
* @param type - Purpose of the verification code
|
|
241
241
|
* @param captchaToken - Captcha verification token from slider captcha
|
|
242
242
|
*/
|
|
243
243
|
async sendCode(phone, type = "login", captchaToken) {
|
|
244
|
+
if (!phone || typeof phone !== "string") {
|
|
245
|
+
throw this.createError("INVALID_PHONE", "Phone number is required / \u8BF7\u8F93\u5165\u624B\u673A\u53F7");
|
|
246
|
+
}
|
|
247
|
+
const countryValidation = {
|
|
248
|
+
// China: +86 followed by 11 digits starting with 1
|
|
249
|
+
"+86": {
|
|
250
|
+
regex: /^\+861[3-9]\d{9}$/,
|
|
251
|
+
example: "+8613800138000",
|
|
252
|
+
name: "\u4E2D\u56FD"
|
|
253
|
+
},
|
|
254
|
+
// USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
|
|
255
|
+
"+1": {
|
|
256
|
+
regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
|
|
257
|
+
example: "+14155552671",
|
|
258
|
+
name: "USA/Canada"
|
|
259
|
+
},
|
|
260
|
+
// Australia: +61 followed by 9 digits starting with 4 (mobile)
|
|
261
|
+
"+61": {
|
|
262
|
+
regex: /^\+614\d{8}$/,
|
|
263
|
+
example: "+61412345678",
|
|
264
|
+
name: "Australia"
|
|
265
|
+
},
|
|
266
|
+
// UK: +44 followed by 10 digits starting with 7 (mobile)
|
|
267
|
+
"+44": {
|
|
268
|
+
regex: /^\+447\d{9}$/,
|
|
269
|
+
example: "+447911123456",
|
|
270
|
+
name: "UK"
|
|
271
|
+
},
|
|
272
|
+
// Japan: +81 followed by 10-11 digits
|
|
273
|
+
"+81": {
|
|
274
|
+
regex: /^\+81[789]0\d{8}$/,
|
|
275
|
+
example: "+818012345678",
|
|
276
|
+
name: "Japan"
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
let countryCode = "";
|
|
280
|
+
let validation = null;
|
|
281
|
+
for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
|
|
282
|
+
if (phone.startsWith(code)) {
|
|
283
|
+
countryCode = code;
|
|
284
|
+
validation = countryValidation[code];
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (validation) {
|
|
289
|
+
if (!validation.regex.test(phone)) {
|
|
290
|
+
throw this.createError(
|
|
291
|
+
"INVALID_PHONE_FORMAT",
|
|
292
|
+
`Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}\u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u793A\u4F8B\uFF1A${validation.example}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
const e164Regex = /^\+[1-9]\d{6,14}$/;
|
|
297
|
+
if (!e164Regex.test(phone)) {
|
|
298
|
+
throw this.createError(
|
|
299
|
+
"INVALID_PHONE_FORMAT",
|
|
300
|
+
"Phone number must be in E.164 format (e.g., +8613800138000) / \u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F7F\u7528\u56FD\u9645\u683C\u5F0F\uFF08\u5982 +8613800138000\uFF09"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
244
304
|
const response = await this.request("/api/v1/auth/phone/send-code", {
|
|
245
305
|
method: "POST",
|
|
246
306
|
body: JSON.stringify({
|
|
@@ -333,10 +393,10 @@ var UniAuthClient = class {
|
|
|
333
393
|
* Handle OAuth callback (for social login)
|
|
334
394
|
* 处理 OAuth 回调(社交登录)
|
|
335
395
|
*/
|
|
336
|
-
async handleOAuthCallback(provider, code) {
|
|
337
|
-
const response = await this.request(
|
|
396
|
+
async handleOAuthCallback(provider, code, redirectUri) {
|
|
397
|
+
const response = await this.request(`/api/v1/auth/oauth/${provider}/callback`, {
|
|
338
398
|
method: "POST",
|
|
339
|
-
body: JSON.stringify({
|
|
399
|
+
body: JSON.stringify({ code, redirect_uri: redirectUri })
|
|
340
400
|
});
|
|
341
401
|
if (!response.success || !response.data) {
|
|
342
402
|
throw this.createError(response.error?.code || "OAUTH_FAILED", response.error?.message || "OAuth callback failed");
|
|
@@ -929,6 +989,7 @@ var UniAuthClient = class {
|
|
|
929
989
|
this.storage.setAccessToken(response.data.access_token);
|
|
930
990
|
this.storage.setRefreshToken(response.data.refresh_token);
|
|
931
991
|
this.config.onTokenRefresh?.(response.data);
|
|
992
|
+
this.notifyAuthStateChange(this.currentUser);
|
|
932
993
|
return true;
|
|
933
994
|
} catch (error) {
|
|
934
995
|
this.storage.clear();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface UniAuthConfig {
|
|
|
42
42
|
appKey?: string;
|
|
43
43
|
/** OAuth2 Client ID (for OAuth flows) */
|
|
44
44
|
clientId?: string;
|
|
45
|
+
/** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
|
|
46
|
+
clientSecret?: string;
|
|
45
47
|
/** Storage type for tokens */
|
|
46
48
|
storage?: 'localStorage' | 'sessionStorage' | 'memory';
|
|
47
49
|
/** Callback when tokens are refreshed */
|
|
@@ -345,7 +347,7 @@ export class UniAuthClient {
|
|
|
345
347
|
* Send verification code to phone number
|
|
346
348
|
* 发送验证码到手机号
|
|
347
349
|
*
|
|
348
|
-
* @param phone - Phone number
|
|
350
|
+
* @param phone - Phone number in E.164 format (e.g., +8613800138000)
|
|
349
351
|
* @param type - Purpose of the verification code
|
|
350
352
|
* @param captchaToken - Captcha verification token from slider captcha
|
|
351
353
|
*/
|
|
@@ -354,6 +356,76 @@ export class UniAuthClient {
|
|
|
354
356
|
type: 'login' | 'register' | 'reset' = 'login',
|
|
355
357
|
captchaToken?: string
|
|
356
358
|
): Promise<SendCodeResult> {
|
|
359
|
+
// Validate phone number format (E.164: +countrycode + number)
|
|
360
|
+
if (!phone || typeof phone !== 'string') {
|
|
361
|
+
throw this.createError('INVALID_PHONE', 'Phone number is required / 请输入手机号');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Country-specific validation rules
|
|
365
|
+
const countryValidation: Record<string, { regex: RegExp; example: string; name: string }> = {
|
|
366
|
+
// China: +86 followed by 11 digits starting with 1
|
|
367
|
+
'+86': {
|
|
368
|
+
regex: /^\+861[3-9]\d{9}$/,
|
|
369
|
+
example: '+8613800138000',
|
|
370
|
+
name: '中国'
|
|
371
|
+
},
|
|
372
|
+
// USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
|
|
373
|
+
'+1': {
|
|
374
|
+
regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
|
|
375
|
+
example: '+14155552671',
|
|
376
|
+
name: 'USA/Canada'
|
|
377
|
+
},
|
|
378
|
+
// Australia: +61 followed by 9 digits starting with 4 (mobile)
|
|
379
|
+
'+61': {
|
|
380
|
+
regex: /^\+614\d{8}$/,
|
|
381
|
+
example: '+61412345678',
|
|
382
|
+
name: 'Australia'
|
|
383
|
+
},
|
|
384
|
+
// UK: +44 followed by 10 digits starting with 7 (mobile)
|
|
385
|
+
'+44': {
|
|
386
|
+
regex: /^\+447\d{9}$/,
|
|
387
|
+
example: '+447911123456',
|
|
388
|
+
name: 'UK'
|
|
389
|
+
},
|
|
390
|
+
// Japan: +81 followed by 10-11 digits
|
|
391
|
+
'+81': {
|
|
392
|
+
regex: /^\+81[789]0\d{8}$/,
|
|
393
|
+
example: '+818012345678',
|
|
394
|
+
name: 'Japan'
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Extract country code (try +1, +86, +61, etc.)
|
|
399
|
+
let countryCode = '';
|
|
400
|
+
let validation = null;
|
|
401
|
+
|
|
402
|
+
for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
|
|
403
|
+
if (phone.startsWith(code)) {
|
|
404
|
+
countryCode = code;
|
|
405
|
+
validation = countryValidation[code];
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (validation) {
|
|
411
|
+
// Country-specific validation
|
|
412
|
+
if (!validation.regex.test(phone)) {
|
|
413
|
+
throw this.createError(
|
|
414
|
+
'INVALID_PHONE_FORMAT',
|
|
415
|
+
`Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}手机号格式错误,示例:${validation.example}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// Fallback: Generic E.164 validation for other countries
|
|
420
|
+
const e164Regex = /^\+[1-9]\d{6,14}$/;
|
|
421
|
+
if (!e164Regex.test(phone)) {
|
|
422
|
+
throw this.createError(
|
|
423
|
+
'INVALID_PHONE_FORMAT',
|
|
424
|
+
'Phone number must be in E.164 format (e.g., +8613800138000) / 手机号格式错误,请使用国际格式(如 +8613800138000)'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
357
429
|
const response = await this.request<SendCodeResult>('/api/v1/auth/phone/send-code', {
|
|
358
430
|
method: 'POST',
|
|
359
431
|
body: JSON.stringify({
|
|
@@ -471,10 +543,10 @@ export class UniAuthClient {
|
|
|
471
543
|
* Handle OAuth callback (for social login)
|
|
472
544
|
* 处理 OAuth 回调(社交登录)
|
|
473
545
|
*/
|
|
474
|
-
async handleOAuthCallback(provider: string, code: string): Promise<LoginResult> {
|
|
475
|
-
const response = await this.request<LoginResult>(
|
|
546
|
+
async handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult> {
|
|
547
|
+
const response = await this.request<LoginResult>(`/api/v1/auth/oauth/${provider}/callback`, {
|
|
476
548
|
method: 'POST',
|
|
477
|
-
body: JSON.stringify({
|
|
549
|
+
body: JSON.stringify({ code, redirect_uri: redirectUri }),
|
|
478
550
|
});
|
|
479
551
|
|
|
480
552
|
if (!response.success || !response.data) {
|
|
@@ -1207,6 +1279,7 @@ export class UniAuthClient {
|
|
|
1207
1279
|
this.storage.setRefreshToken(response.data.refresh_token);
|
|
1208
1280
|
|
|
1209
1281
|
this.config.onTokenRefresh?.(response.data);
|
|
1282
|
+
this.notifyAuthStateChange(this.currentUser);
|
|
1210
1283
|
|
|
1211
1284
|
return true;
|
|
1212
1285
|
} catch (error) {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { UniAuthClient, UniAuthError } from '../src/index';
|
|
3
|
+
|
|
4
|
+
describe('UniAuthClient - Phone Validation', () => {
|
|
5
|
+
let client: UniAuthClient;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
client = new UniAuthClient({
|
|
9
|
+
baseUrl: 'https://test.example.com',
|
|
10
|
+
clientId: 'test-client',
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('sendCode phone validation', () => {
|
|
15
|
+
it('should reject empty phone number', async () => {
|
|
16
|
+
await expect(client.sendCode('')).rejects.toThrow(UniAuthError);
|
|
17
|
+
await expect(client.sendCode('')).rejects.toMatchObject({
|
|
18
|
+
code: 'INVALID_PHONE',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should reject phone number without country code', async () => {
|
|
23
|
+
await expect(client.sendCode('13800138000')).rejects.toMatchObject({
|
|
24
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should reject phone number without + prefix', async () => {
|
|
29
|
+
await expect(client.sendCode('8613800138000')).rejects.toMatchObject({
|
|
30
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// China-specific validation
|
|
35
|
+
it('should reject invalid China phone number (wrong length)', async () => {
|
|
36
|
+
await expect(client.sendCode('+861380013800')).rejects.toMatchObject({
|
|
37
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should reject invalid China phone number (wrong prefix)', async () => {
|
|
42
|
+
await expect(client.sendCode('+8612800138000')).rejects.toMatchObject({
|
|
43
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// USA-specific validation
|
|
48
|
+
it('should reject invalid USA phone number (wrong format)', async () => {
|
|
49
|
+
await expect(client.sendCode('+11234567890')).rejects.toMatchObject({
|
|
50
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Australia-specific validation
|
|
55
|
+
it('should reject invalid Australia phone number (wrong prefix)', async () => {
|
|
56
|
+
await expect(client.sendCode('+61312345678')).rejects.toMatchObject({
|
|
57
|
+
code: 'INVALID_PHONE_FORMAT',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should accept valid country-specific phone numbers', async () => {
|
|
62
|
+
// Mock the fetch to avoid actual API calls
|
|
63
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
64
|
+
ok: true,
|
|
65
|
+
json: () => Promise.resolve({
|
|
66
|
+
success: true,
|
|
67
|
+
data: { expires_in: 300, retry_after: 60 },
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
global.fetch = mockFetch;
|
|
71
|
+
|
|
72
|
+
// Valid phone numbers for each country
|
|
73
|
+
const validNumbers = [
|
|
74
|
+
'+8613800138000', // China
|
|
75
|
+
'+8619912345678', // China (new prefix)
|
|
76
|
+
'+14155552671', // USA
|
|
77
|
+
'+12025551234', // USA
|
|
78
|
+
'+61412345678', // Australia
|
|
79
|
+
'+447911123456', // UK
|
|
80
|
+
'+818012345678', // Japan
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const phone of validNumbers) {
|
|
84
|
+
mockFetch.mockClear();
|
|
85
|
+
await expect(client.sendCode(phone)).resolves.toBeDefined();
|
|
86
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
87
|
+
expect.stringContaining('/api/v1/auth/phone/send-code'),
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: expect.stringContaining(phone),
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('UniAuthClient - Email Validation', () => {
|
|
99
|
+
let client: UniAuthClient;
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
client = new UniAuthClient({
|
|
103
|
+
baseUrl: 'https://test.example.com',
|
|
104
|
+
clientId: 'test-client',
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('sendEmailCode', () => {
|
|
109
|
+
it('should accept valid email and make API call', async () => {
|
|
110
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
111
|
+
ok: true,
|
|
112
|
+
json: () => Promise.resolve({
|
|
113
|
+
success: true,
|
|
114
|
+
data: { expires_in: 300, retry_after: 60 },
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
global.fetch = mockFetch;
|
|
118
|
+
|
|
119
|
+
await expect(client.sendEmailCode('test@example.com')).resolves.toBeDefined();
|
|
120
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
121
|
+
expect.stringContaining('/api/v1/auth/email/send-code'),
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: expect.stringContaining('test@example.com'),
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('UniAuthClient - OAuth Callback', () => {
|
|
132
|
+
let client: UniAuthClient;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
// Mock localStorage
|
|
136
|
+
const localStorageMock = {
|
|
137
|
+
getItem: vi.fn(),
|
|
138
|
+
setItem: vi.fn(),
|
|
139
|
+
removeItem: vi.fn(),
|
|
140
|
+
clear: vi.fn(),
|
|
141
|
+
};
|
|
142
|
+
Object.defineProperty(global, 'localStorage', {
|
|
143
|
+
value: localStorageMock,
|
|
144
|
+
writable: true
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
client = new UniAuthClient({
|
|
148
|
+
baseUrl: 'https://test.example.com',
|
|
149
|
+
clientId: 'test-client',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle OAuth callback with correct URL and parameters', async () => {
|
|
154
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
155
|
+
ok: true,
|
|
156
|
+
json: () => Promise.resolve({
|
|
157
|
+
success: true,
|
|
158
|
+
data: {
|
|
159
|
+
user: { id: '123' },
|
|
160
|
+
access_token: 'access_token',
|
|
161
|
+
refresh_token: 'refresh_token',
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
global.fetch = mockFetch;
|
|
166
|
+
|
|
167
|
+
const provider = 'google';
|
|
168
|
+
const code = 'auth_code';
|
|
169
|
+
const redirectUri = 'https://app.com/callback';
|
|
170
|
+
|
|
171
|
+
await client.handleOAuthCallback(provider, code, redirectUri);
|
|
172
|
+
|
|
173
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
174
|
+
expect.stringContaining(`/api/v1/auth/oauth/${provider}/callback`),
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: expect.stringContaining('"redirect_uri":"https://app.com/callback"'),
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Also verify code is present
|
|
182
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
183
|
+
expect.anything(),
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
body: expect.stringContaining('"code":"auth_code"'),
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('UniAuthClient - Error Handling', () => {
|
|
192
|
+
let client: UniAuthClient;
|
|
193
|
+
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
client = new UniAuthClient({
|
|
196
|
+
baseUrl: 'https://test.example.com',
|
|
197
|
+
clientId: 'test-client',
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should create UniAuthError with correct properties', async () => {
|
|
202
|
+
try {
|
|
203
|
+
await client.sendCode('');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
expect(error).toBeInstanceOf(UniAuthError);
|
|
206
|
+
expect((error as UniAuthError).code).toBe('INVALID_PHONE');
|
|
207
|
+
expect((error as UniAuthError).message).toContain('请输入手机号');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle API errors correctly', async () => {
|
|
212
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
213
|
+
ok: true,
|
|
214
|
+
json: () => Promise.resolve({
|
|
215
|
+
success: false,
|
|
216
|
+
error: {
|
|
217
|
+
code: 'DAILY_LIMIT_EXCEEDED',
|
|
218
|
+
message: '今日发送次数已达上限',
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
global.fetch = mockFetch;
|
|
223
|
+
|
|
224
|
+
await expect(client.sendCode('+8613800138000')).rejects.toMatchObject({
|
|
225
|
+
code: 'DAILY_LIMIT_EXCEEDED',
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['tests/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|