e1628b2ccd
- clientActionError.ts 改名 clientAuthErrorWrapper.ts,类名同步为 ClientAuthErrorWrapper,error 字段标注 ClientAuthError - invokeClientAuth 改为薄包装:不读变体 message、不塞 context,只把拒绝原样装成 JS Error - 三处 catch 改用 error.error 分流,每个 case 继续用 as 取具名载荷 - captureClientError 从 error 字段取原始错误;上下文只认显式入参 - 载体、host、错误上报与 appSurface 用例同步新载体与新默认文案
154 lines
6.5 KiB
TypeScript
154 lines
6.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* 认证命令失败的 JS 侧载体。
|
|
*
|
|
* `invokeClientAuth` 是薄包装:Rust 的结构化拒绝原样装进 `ClientAuthErrorWrapper.error`,不读变体
|
|
* 字段(`message` 之类不保证每个变体都有)、不注入上下文、载体自身 `message` 留空;分流只按
|
|
* `error.error.type`。另外用编译期用例钉住"每个变体一个具名载荷类型":漏列任何变体,
|
|
* `expectNever(error)` 都编译不过。
|
|
*/
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { loginClientWithPassword } from '../src/services/clientAuth';
|
|
import { ClientAuthErrorWrapper } from '../src/services/clientAuthErrorWrapper';
|
|
import type { AuthClientInitFailed } from '../src/services/generated/AuthClientInitFailed';
|
|
import type { AuthNetworkUnavailable } from '../src/services/generated/AuthNetworkUnavailable';
|
|
import type { AuthResponseMalformed } from '../src/services/generated/AuthResponseMalformed';
|
|
import type { AuthServiceUnavailable } from '../src/services/generated/AuthServiceUnavailable';
|
|
import type { ClientAuthError } from '../src/services/generated/ClientAuthError';
|
|
import type { ClientSessionPersistFailed } from '../src/services/generated/ClientSessionPersistFailed';
|
|
import type { LoginCodeMissing } from '../src/services/generated/LoginCodeMissing';
|
|
import type { PasswordEntryInputRejected } from '../src/services/generated/PasswordEntryInputRejected';
|
|
import type { PasswordMissing } from '../src/services/generated/PasswordMissing';
|
|
import type { PermissionDenied } from '../src/services/generated/PermissionDenied';
|
|
import type { PhoneLoginInputRejected } from '../src/services/generated/PhoneLoginInputRejected';
|
|
import type { PhoneNumberInvalid } from '../src/services/generated/PhoneNumberInvalid';
|
|
import type { PhoneOrPasswordMismatch } from '../src/services/generated/PhoneOrPasswordMismatch';
|
|
import type { RuntimeSessionInstallFailed } from '../src/services/generated/RuntimeSessionInstallFailed';
|
|
import type { SendCodeInputRejected } from '../src/services/generated/SendCodeInputRejected';
|
|
import type { ServerAddressRejected } from '../src/services/generated/ServerAddressRejected';
|
|
import type { SessionAuthorityRejected } from '../src/services/generated/SessionAuthorityRejected';
|
|
import type { SmsCodeInvalidOrExpired } from '../src/services/generated/SmsCodeInvalidOrExpired';
|
|
import type { SmsCodeThrottled } from '../src/services/generated/SmsCodeThrottled';
|
|
import type { UnexpectedRejection } from '../src/services/generated/UnexpectedRejection';
|
|
|
|
function expectNever(value: never): void {
|
|
void value;
|
|
}
|
|
|
|
/** 编译期用例:19 个变体逐个列全,每个分支用 `as` 取具名载荷类型。 */
|
|
function variantMessage(error: ClientAuthError): string {
|
|
switch (error.type) {
|
|
case 'serverAddressRejected':
|
|
return (error as ServerAddressRejected).message;
|
|
case 'phoneNumberInvalid':
|
|
return (error as PhoneNumberInvalid).message;
|
|
case 'passwordMissing':
|
|
return (error as PasswordMissing).message;
|
|
case 'loginCodeMissing':
|
|
return (error as LoginCodeMissing).message;
|
|
case 'passwordEntryInputRejected':
|
|
return (error as PasswordEntryInputRejected).message;
|
|
case 'phoneOrPasswordMismatch':
|
|
return (error as PhoneOrPasswordMismatch).message;
|
|
case 'sendCodeInputRejected':
|
|
return (error as SendCodeInputRejected).message;
|
|
case 'smsCodeThrottled':
|
|
return (error as SmsCodeThrottled).message;
|
|
case 'phoneLoginInputRejected':
|
|
return (error as PhoneLoginInputRejected).message;
|
|
case 'smsCodeInvalidOrExpired':
|
|
return (error as SmsCodeInvalidOrExpired).message;
|
|
case 'sessionAuthorityRejected':
|
|
return (error as SessionAuthorityRejected).message;
|
|
case 'permissionDenied':
|
|
return (error as PermissionDenied).message;
|
|
case 'authNetworkUnavailable':
|
|
return (error as AuthNetworkUnavailable).message;
|
|
case 'authServiceUnavailable':
|
|
return (error as AuthServiceUnavailable).message;
|
|
case 'unexpectedRejection':
|
|
return (error as UnexpectedRejection).message;
|
|
case 'authResponseMalformed':
|
|
return (error as AuthResponseMalformed).message;
|
|
case 'clientSessionPersistFailed':
|
|
return (error as ClientSessionPersistFailed).message;
|
|
case 'runtimeSessionInstallFailed':
|
|
return (error as RuntimeSessionInstallFailed).message;
|
|
case 'authClientInitFailed':
|
|
return (error as AuthClientInitFailed).message;
|
|
default: {
|
|
expectNever(error);
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
|
|
function installInvoke(handler: () => Promise<unknown>) {
|
|
window.__TAURI__ = { core: { invoke: vi.fn(handler) } } as never;
|
|
}
|
|
|
|
describe('认证命令失败的 JS 侧载体', () => {
|
|
it('结构化拒绝原样装进 error 字段,不读变体字段', async () => {
|
|
// 故意不带 message:包装层绝不能假设每个变体都有这个字段。
|
|
const rejection = { type: 'phoneNumberInvalid', status: 400 };
|
|
installInvoke(async () => {
|
|
throw rejection;
|
|
});
|
|
|
|
const failure = await loginClientWithPassword(
|
|
'13800000000',
|
|
'secret',
|
|
'https://dev.genarrative.world',
|
|
).catch((error: unknown) => error);
|
|
|
|
expect(failure).toBeInstanceOf(ClientAuthErrorWrapper);
|
|
expect((failure as ClientAuthErrorWrapper).error).toBe(rejection);
|
|
expect((failure as Error).message).toBe('');
|
|
});
|
|
|
|
it('认证桥未安装时保持原样抛出,不包装成命令失败', async () => {
|
|
delete window.__TAURI__;
|
|
|
|
await expect(
|
|
loginClientWithPassword(
|
|
'13800000000',
|
|
'secret',
|
|
'https://dev.genarrative.world',
|
|
),
|
|
).rejects.toThrow('需要在 Tauri App 内登录');
|
|
});
|
|
|
|
it('Tauri 的意外形状也按映射包装:不做嗅探,但仍是可上报的 Error', async () => {
|
|
const rejection = new Error('IPC 桥接异常');
|
|
installInvoke(async () => {
|
|
throw rejection;
|
|
});
|
|
|
|
const failure = await loginClientWithPassword(
|
|
'13800000000',
|
|
'secret',
|
|
'https://dev.genarrative.world',
|
|
).catch((error: unknown) => error);
|
|
|
|
expect(failure).toBeInstanceOf(ClientAuthErrorWrapper);
|
|
expect((failure as Error).message).toBe('');
|
|
expect((failure as ClientAuthErrorWrapper).error).toBe(rejection);
|
|
});
|
|
});
|
|
|
|
describe('变体分流', () => {
|
|
it('判别后按具名载荷类型取值', () => {
|
|
expect(
|
|
variantMessage({ type: 'phoneNumberInvalid', message: '格式不对' }),
|
|
).toBe('格式不对');
|
|
expect(
|
|
variantMessage({
|
|
type: 'authServiceUnavailable',
|
|
status: 503,
|
|
message: '服务暂不可用',
|
|
}),
|
|
).toBe('服务暂不可用');
|
|
});
|
|
});
|