AGC 认证错误改为每个变体一个具名载荷类型

- auth_error.rs:枚举改为 newtype 变体,每个变体持有一个同名 `#[ts(export)]` 载荷结构体
- auth_session.rs:41 处构造点改为 `ClientAuthError::X(X { .. })` 并导入载荷类型
- 重新生成 services/generated:ClientAuthError.ts 变成 `{ type } & 具名类型`,新增 19 个载荷类型文件
- tests/clientAuthError.test.ts:新增编译期用例,switch (error.type) 分支里错误窄化成具名载荷类型
This commit is contained in:
2026-10-01 18:08:18 +08:00
parent 2f51e043b7
commit f435d48399
23 changed files with 613 additions and 271 deletions
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,14 @@
//! 只在 Rust 内存与本进程会话快照里,refresh 凭据只写在 AppData 私有文件里。换号、登出或
//! origin 变化都会让旧身份的在途请求失败关闭;同一身份的凭据轮换不改变身份代次。
use crate::auth_error::ClientAuthError;
use crate::auth_error::{
AuthClientInitFailed, AuthNetworkUnavailable, AuthResponseMalformed, AuthServiceUnavailable,
ClientAuthError, ClientSessionPersistFailed, LoginCodeMissing, PasswordEntryInputRejected,
PasswordMissing, PermissionDenied, PhoneLoginInputRejected, PhoneNumberInvalid,
PhoneOrPasswordMismatch, RuntimeSessionInstallFailed, SendCodeInputRejected,
ServerAddressRejected, SessionAuthorityRejected, SmsCodeInvalidOrExpired, SmsCodeThrottled,
UnexpectedRejection,
};
use crate::http_client::agc_main_site_client_builder;
use crate::platform_session::{current_platform_session, PlatformSessionSnapshot};
use reqwest::{header::SET_COOKIE, Method, StatusCode};
@@ -223,30 +230,32 @@ fn session_file_path(app: &tauri::AppHandle) -> Result<PathBuf, ClientAuthError>
app.path()
.app_data_dir()
.map(|root| root.join(SESSION_FILE_NAME))
.map_err(|error| ClientAuthError::ClientSessionPersistFailed {
message: format!("无法读取 AGC 应用数据目录:{error}"),
.map_err(|error| {
ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed {
message: format!("无法读取 AGC 应用数据目录:{error}"),
})
})
}
/// 凭据文件相关的字符串错误统一收口成凭据落盘失败变体。
fn session_persist_error(message: impl Into<String>) -> ClientAuthError {
ClientAuthError::ClientSessionPersistFailed {
ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed {
message: message.into(),
}
})
}
/// 本机运行时安装 / 清理失败的字符串错误统一收口。
fn runtime_session_error(message: impl Into<String>) -> ClientAuthError {
ClientAuthError::RuntimeSessionInstallFailed {
ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed {
message: message.into(),
}
})
}
/// 服务地址校验(含渠道范围门禁)失败统一收口成业务变体。
fn server_address_rejected(message: impl Into<String>) -> ClientAuthError {
ClientAuthError::ServerAddressRejected {
ClientAuthError::ServerAddressRejected(ServerAddressRejected {
message: message.into(),
}
})
}
/// 校验并归一化平台服务 origin。
@@ -428,19 +437,21 @@ fn build_client() -> Result<reqwest::Client, ClientAuthError> {
.connect_timeout(Duration::from_secs(10))
.timeout(HTTP_TIMEOUT)
.build()
.map_err(|_| ClientAuthError::AuthClientInitFailed {
message: "创建登录网络客户端失败".to_string(),
.map_err(|_| {
ClientAuthError::AuthClientInitFailed(AuthClientInitFailed {
message: "创建登录网络客户端失败".to_string(),
})
})
}
fn network_error_message(error: &reqwest::Error) -> ClientAuthError {
ClientAuthError::AuthNetworkUnavailable {
ClientAuthError::AuthNetworkUnavailable(AuthNetworkUnavailable {
message: if error.is_timeout() {
AUTH_NETWORK_TIMEOUT.to_string()
} else {
AUTH_NETWORK_ERROR.to_string()
},
}
})
}
fn error_message(body: &str) -> Option<String> {
@@ -508,79 +519,92 @@ fn map_auth_failure(
};
if status == StatusCode::UNAUTHORIZED {
return match route {
AuthRoute::Session => ClientAuthError::SessionAuthorityRejected {
message: server_message.unwrap_or_else(|| AUTH_AUTHORITY_MESSAGE.to_string()),
},
AuthRoute::PasswordEntry => ClientAuthError::PhoneOrPasswordMismatch {
message: server_message.unwrap_or_else(|| fallback.to_string()),
},
AuthRoute::PhoneLogin => ClientAuthError::SmsCodeInvalidOrExpired {
message: server_message.unwrap_or_else(|| fallback.to_string()),
},
_ => ClientAuthError::UnexpectedRejection {
AuthRoute::Session => {
ClientAuthError::SessionAuthorityRejected(SessionAuthorityRejected {
message: server_message.unwrap_or_else(|| AUTH_AUTHORITY_MESSAGE.to_string()),
})
}
AuthRoute::PasswordEntry => {
ClientAuthError::PhoneOrPasswordMismatch(PhoneOrPasswordMismatch {
message: server_message.unwrap_or_else(|| fallback.to_string()),
})
}
AuthRoute::PhoneLogin => {
ClientAuthError::SmsCodeInvalidOrExpired(SmsCodeInvalidOrExpired {
message: server_message.unwrap_or_else(|| fallback.to_string()),
})
}
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
status: status_code,
message: server_message.unwrap_or_else(|| fallback.to_string()),
},
}),
};
}
if status == StatusCode::FORBIDDEN {
return match route {
AuthRoute::Session => ClientAuthError::PermissionDenied {
AuthRoute::Session => ClientAuthError::PermissionDenied(PermissionDenied {
message: server_message
.unwrap_or_else(|| "当前陶泥儿账号没有执行此操作的权限".to_string()),
},
_ => ClientAuthError::UnexpectedRejection {
}),
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
status: status_code,
message: server_message.unwrap_or_else(|| fallback.to_string()),
},
}),
};
}
if status == StatusCode::TOO_MANY_REQUESTS && route == AuthRoute::SendCode {
return ClientAuthError::SmsCodeThrottled {
return ClientAuthError::SmsCodeThrottled(SmsCodeThrottled {
message: prefixed(),
};
});
}
if status == StatusCode::BAD_REQUEST {
return match route {
AuthRoute::PasswordEntry => ClientAuthError::PasswordEntryInputRejected {
AuthRoute::PasswordEntry => {
ClientAuthError::PasswordEntryInputRejected(PasswordEntryInputRejected {
message: prefixed(),
})
}
AuthRoute::PhoneLogin => {
ClientAuthError::PhoneLoginInputRejected(PhoneLoginInputRejected {
message: prefixed(),
})
}
AuthRoute::SendCode => ClientAuthError::SendCodeInputRejected(SendCodeInputRejected {
message: prefixed(),
},
AuthRoute::PhoneLogin => ClientAuthError::PhoneLoginInputRejected {
message: prefixed(),
},
AuthRoute::SendCode => ClientAuthError::SendCodeInputRejected {
message: prefixed(),
},
_ => ClientAuthError::UnexpectedRejection {
}),
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
status: status_code,
message: prefixed(),
},
}),
};
}
if status.is_server_error() {
return ClientAuthError::AuthServiceUnavailable {
return ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable {
status: status_code,
message: prefixed(),
};
});
}
ClientAuthError::UnexpectedRejection {
ClientAuthError::UnexpectedRejection(UnexpectedRejection {
status: status_code,
message: prefixed(),
}
})
}
fn response_data(body: &str, fallback: &str) -> Result<Value, ClientAuthError> {
let value: Value =
serde_json::from_str(body).map_err(|_| ClientAuthError::AuthResponseMalformed {
let value: Value = serde_json::from_str(body).map_err(|_| {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: format!("{fallback}:登录服务响应不是合法 JSON"),
})?;
})
})?;
if value.get("ok").and_then(Value::as_bool) == Some(false) {
return Err(ClientAuthError::AuthResponseMalformed {
message: format!(
"{fallback}:{}",
error_message(body).unwrap_or_else(|| "登录服务请求失败".to_string())
),
});
return Err(ClientAuthError::AuthResponseMalformed(
AuthResponseMalformed {
message: format!(
"{fallback}:{}",
error_message(body).unwrap_or_else(|| "登录服务请求失败".to_string())
),
},
));
}
Ok(value.get("data").cloned().unwrap_or(value))
}
@@ -657,19 +681,20 @@ async fn request_auth(
CookiePolicy::Capture | CookiePolicy::Require => refresh_cookie_from_response(&response),
CookiePolicy::Ignore => None,
};
let text = response
.text()
.await
.map_err(|_| ClientAuthError::AuthResponseMalformed {
let text = response.text().await.map_err(|_| {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: format!("{fallback}:读取响应失败"),
})?;
})
})?;
if !status.is_success() {
return Err(map_auth_failure(status, &text, fallback, auth_route(route)));
}
if matches!(policy, CookiePolicy::Require) && captured.is_none() {
return Err(ClientAuthError::AuthResponseMalformed {
message: "登录服务未返回新的续期凭据,已停止使用旧凭据".to_string(),
});
return Err(ClientAuthError::AuthResponseMalformed(
AuthResponseMalformed {
message: "登录服务未返回新的续期凭据,已停止使用旧凭据".to_string(),
},
));
}
Ok(AuthResponse {
data: response_data(&text, fallback)?,
@@ -708,9 +733,11 @@ async fn commit_authenticated_session(
identity_change: bool,
) -> Result<AuthUserPayload, ClientAuthError> {
if token.chars().count() > MAX_SECRET_CHARS {
return Err(ClientAuthError::AuthResponseMalformed {
message: "登录服务返回的凭据无效".to_string(),
});
return Err(ClientAuthError::AuthResponseMalformed(
AuthResponseMalformed {
message: "登录服务返回的凭据无效".to_string(),
},
));
}
// 主体先于凭据落盘解析:没有主体就不写 client-session.json,避免留下半截会话文件。
let (user_id, known_user) = identity.resolve()?;
@@ -802,9 +829,11 @@ fn phone_is_valid(phone: &str) -> bool {
fn validated_session_user_id(user_id: &str) -> Result<String, ClientAuthError> {
let user_id = user_id.trim();
if user_id.is_empty() {
return Err(ClientAuthError::AuthResponseMalformed {
message: "登录失败:登录服务未返回用户身份".to_string(),
});
return Err(ClientAuthError::AuthResponseMalformed(
AuthResponseMalformed {
message: "登录失败:登录服务未返回用户身份".to_string(),
},
));
}
Ok(user_id.to_string())
}
@@ -837,9 +866,9 @@ async fn fetch_current_user(
)
.await?;
let me: MeResponse = serde_json::from_value(response.data).map_err(|_| {
ClientAuthError::AuthResponseMalformed {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "读取当前用户失败:响应格式无效".to_string(),
}
})
})?;
Ok(me.user)
}
@@ -917,17 +946,16 @@ async fn refresh_session_inner(
}
};
let token: TokenResponse = serde_json::from_value(refreshed.data).map_err(|_| {
ClientAuthError::AuthResponseMalformed {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "刷新登录状态失败:凭据响应格式无效".to_string(),
}
})
})?;
let install_token = token.token.clone();
let new_cookie =
refreshed
.refresh_cookie
.ok_or_else(|| ClientAuthError::AuthResponseMalformed {
message: "刷新登录状态失败:缺少新的续期凭据".to_string(),
})?;
let new_cookie = refreshed.refresh_cookie.ok_or_else(|| {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "刷新登录状态失败:缺少新的续期凭据".to_string(),
})
})?;
commit_authenticated_session(
app,
&session.api_base_url,
@@ -1073,9 +1101,9 @@ pub(crate) async fn send_client_phone_login_code(
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
let phone = phone.trim();
if !phone_is_valid(phone) {
return Err(ClientAuthError::PhoneNumberInvalid {
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
message: "请输入正确的手机号".to_string(),
});
}));
}
let client = build_client()?;
let response = request_auth(
@@ -1094,9 +1122,9 @@ pub(crate) async fn send_client_phone_login_code(
)
.await?;
let payload: SendCodeResponse = serde_json::from_value(response.data).map_err(|_| {
ClientAuthError::AuthResponseMalformed {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "发送验证码失败:响应格式无效".to_string(),
}
})
})?;
Ok(ClientLoginCodeView {
cooldown_seconds: payload.cooldown_seconds,
@@ -1110,15 +1138,15 @@ async fn complete_login(
response: AuthResponse,
) -> Result<AuthUserPayload, ClientAuthError> {
let payload: TokenUserResponse = serde_json::from_value(response.data).map_err(|_| {
ClientAuthError::AuthResponseMalformed {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "登录失败:登录服务响应格式无效".to_string(),
}
})
})?;
let cookie = response
.refresh_cookie
.ok_or_else(|| ClientAuthError::AuthResponseMalformed {
let cookie = response.refresh_cookie.ok_or_else(|| {
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
message: "登录失败:登录服务未返回续期凭据".to_string(),
})?;
})
})?;
let user = commit_authenticated_session(
app,
origin,
@@ -1148,14 +1176,14 @@ pub(crate) async fn login_client_with_password(
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
let phone = phone.trim();
if !phone_is_valid(phone) {
return Err(ClientAuthError::PhoneNumberInvalid {
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
message: "请输入正确的手机号".to_string(),
});
}));
}
if password.trim().is_empty() {
return Err(ClientAuthError::PasswordMissing {
return Err(ClientAuthError::PasswordMissing(PasswordMissing {
message: "请输入密码".to_string(),
});
}));
}
let client = build_client()?;
let response = request_auth(
@@ -1186,14 +1214,14 @@ pub(crate) async fn login_client_with_phone_code(
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
let phone = phone.trim();
if !phone_is_valid(phone) {
return Err(ClientAuthError::PhoneNumberInvalid {
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
message: "请输入正确的手机号".to_string(),
});
}));
}
if code.trim().is_empty() {
return Err(ClientAuthError::LoginCodeMissing {
return Err(ClientAuthError::LoginCodeMissing(LoginCodeMissing {
message: "请输入验证码".to_string(),
});
}));
}
let client = build_client()?;
let response = request_auth(
@@ -1321,7 +1349,7 @@ mod tests {
assert!(!transient.is_authority_failure());
assert!(matches!(
transient,
ClientAuthError::AuthServiceUnavailable { status: 500, .. }
ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable { status: 500, .. })
));
assert!(transient.message().starts_with("刷新失败"));
}
@@ -1336,9 +1364,9 @@ mod tests {
);
assert_eq!(
wrong_password,
ClientAuthError::PhoneOrPasswordMismatch {
ClientAuthError::PhoneOrPasswordMismatch(PhoneOrPasswordMismatch {
message: "手机号或密码错误".to_string()
}
})
);
assert!(!wrong_password.is_authority_failure());
@@ -1361,7 +1389,7 @@ mod tests {
);
assert!(matches!(
password_length,
ClientAuthError::PasswordEntryInputRejected { .. }
ClientAuthError::PasswordEntryInputRejected(PasswordEntryInputRejected { .. })
));
assert_eq!(
password_length.message(),
@@ -1376,7 +1404,7 @@ mod tests {
);
assert!(matches!(
throttled,
ClientAuthError::SmsCodeThrottled { .. }
ClientAuthError::SmsCodeThrottled(SmsCodeThrottled { .. })
));
assert_eq!(throttled.message(), "发送验证码失败:发送过于频繁");
@@ -1388,7 +1416,7 @@ mod tests {
);
assert!(matches!(
bad_code,
ClientAuthError::SmsCodeInvalidOrExpired { .. }
ClientAuthError::SmsCodeInvalidOrExpired(SmsCodeInvalidOrExpired { .. })
));
assert_eq!(bad_code.message(), "验证码错误");
}
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 认证网络客户端构建失败。
*/
export type AuthClientInitFailed = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 连接 / 超时 / DNS 等网络失败。
*/
export type AuthNetworkUnavailable = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 响应不是合法 JSON、缺少必需字段或契约不成立。
*/
export type AuthResponseMalformed = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 服务端 5xx。
*/
export type AuthServiceUnavailable = { status: number; message: string };
@@ -1,25 +1,44 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AuthClientInitFailed } from './AuthClientInitFailed';
import type { AuthNetworkUnavailable } from './AuthNetworkUnavailable';
import type { AuthResponseMalformed } from './AuthResponseMalformed';
import type { AuthServiceUnavailable } from './AuthServiceUnavailable';
import type { ClientSessionPersistFailed } from './ClientSessionPersistFailed';
import type { LoginCodeMissing } from './LoginCodeMissing';
import type { PasswordEntryInputRejected } from './PasswordEntryInputRejected';
import type { PasswordMissing } from './PasswordMissing';
import type { PermissionDenied } from './PermissionDenied';
import type { PhoneLoginInputRejected } from './PhoneLoginInputRejected';
import type { PhoneNumberInvalid } from './PhoneNumberInvalid';
import type { PhoneOrPasswordMismatch } from './PhoneOrPasswordMismatch';
import type { RuntimeSessionInstallFailed } from './RuntimeSessionInstallFailed';
import type { SendCodeInputRejected } from './SendCodeInputRejected';
import type { ServerAddressRejected } from './ServerAddressRejected';
import type { SessionAuthorityRejected } from './SessionAuthorityRejected';
import type { SmsCodeInvalidOrExpired } from './SmsCodeInvalidOrExpired';
import type { SmsCodeThrottled } from './SmsCodeThrottled';
import type { UnexpectedRejection } from './UnexpectedRejection';
/**
* 变体名就是线上的分流键(`type`)。
* 变体名就是线上的分流键(`type`),每个变体持有同名载荷类型。
*/
export type ClientAuthError =
| { type: 'serverAddressRejected'; message: string }
| { type: 'phoneNumberInvalid'; message: string }
| { type: 'passwordMissing'; message: string }
| { type: 'loginCodeMissing'; message: string }
| { type: 'passwordEntryInputRejected'; message: string }
| { type: 'phoneOrPasswordMismatch'; message: string }
| { type: 'sendCodeInputRejected'; message: string }
| { type: 'smsCodeThrottled'; message: string }
| { type: 'phoneLoginInputRejected'; message: string }
| { type: 'smsCodeInvalidOrExpired'; message: string }
| { type: 'sessionAuthorityRejected'; message: string }
| { type: 'permissionDenied'; message: string }
| { type: 'authNetworkUnavailable'; message: string }
| { type: 'authServiceUnavailable'; status: number; message: string }
| { type: 'unexpectedRejection'; status: number; message: string }
| { type: 'authResponseMalformed'; message: string }
| { type: 'clientSessionPersistFailed'; message: string }
| { type: 'runtimeSessionInstallFailed'; message: string }
| { type: 'authClientInitFailed'; message: string };
| ({ type: 'serverAddressRejected' } & ServerAddressRejected)
| ({ type: 'phoneNumberInvalid' } & PhoneNumberInvalid)
| ({ type: 'passwordMissing' } & PasswordMissing)
| ({ type: 'loginCodeMissing' } & LoginCodeMissing)
| ({ type: 'passwordEntryInputRejected' } & PasswordEntryInputRejected)
| ({ type: 'phoneOrPasswordMismatch' } & PhoneOrPasswordMismatch)
| ({ type: 'sendCodeInputRejected' } & SendCodeInputRejected)
| ({ type: 'smsCodeThrottled' } & SmsCodeThrottled)
| ({ type: 'phoneLoginInputRejected' } & PhoneLoginInputRejected)
| ({ type: 'smsCodeInvalidOrExpired' } & SmsCodeInvalidOrExpired)
| ({ type: 'sessionAuthorityRejected' } & SessionAuthorityRejected)
| ({ type: 'permissionDenied' } & PermissionDenied)
| ({ type: 'authNetworkUnavailable' } & AuthNetworkUnavailable)
| ({ type: 'authServiceUnavailable' } & AuthServiceUnavailable)
| ({ type: 'unexpectedRejection' } & UnexpectedRejection)
| ({ type: 'authResponseMalformed' } & AuthResponseMalformed)
| ({ type: 'clientSessionPersistFailed' } & ClientSessionPersistFailed)
| ({ type: 'runtimeSessionInstallFailed' } & RuntimeSessionInstallFailed)
| ({ type: 'authClientInitFailed' } & AuthClientInitFailed);
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 本机登录凭据文件读写失败。
*/
export type ClientSessionPersistFailed = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 本地前置校验:验证码为空。
*/
export type LoginCodeMissing = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/entry` 返回 400:服务端拒绝本次输入。
*/
export type PasswordEntryInputRejected = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 本地前置校验:密码为空。
*/
export type PasswordMissing = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 会话路由 403:当前账号没有执行此操作的权限。
*/
export type PermissionDenied = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/phone/login` 返回 400。
*/
export type PhoneLoginInputRejected = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 本地前置校验:手机号为空或格式不合法。
*/
export type PhoneNumberInvalid = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/entry` 返回 401:手机号或密码错误。
*/
export type PhoneOrPasswordMismatch = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 本机运行时会话安装 / 清理失败。
*/
export type RuntimeSessionInstallFailed = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/phone/send-code` 返回 400。
*/
export type SendCodeInputRejected = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 服务地址不是合法 origin / 非本机未用 HTTPS / 不在构建渠道范围内。
*/
export type ServerAddressRejected = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 会话路由 401:登录态权威失效。
*/
export type SessionAuthorityRejected = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/phone/login` 返回 401:验证码错误或过期。
*/
export type SmsCodeInvalidOrExpired = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* `/api/auth/phone/send-code` 返回 429:发送过于频繁。
*/
export type SmsCodeThrottled = { message: string };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 其它未识别的拒绝(未列举的 4xx、登录路由 403 等)。
*/
export type UnexpectedRejection = { status: number; message: string };
@@ -1,8 +1,46 @@
import { describe, expect, it } from 'vitest';
import { isClientAuthError } from '../src/services/clientAuthError';
import type { AuthServiceUnavailable } from '../src/services/generated/AuthServiceUnavailable';
import type { ClientAuthError } from '../src/services/generated/ClientAuthError';
import type { PhoneNumberInvalid } from '../src/services/generated/PhoneNumberInvalid';
/**
* 编译期用例:每个变体在 `type` 上判别后都能落到一个**具名载荷类型**,
* 等价于 Java 的 `catch (PhoneNumberInvalid e)`;类型不匹配时 `tsc` 会直接报错。
*/
function handleByNamedType(error: ClientAuthError) {
switch (error.type) {
case 'phoneNumberInvalid': {
const payload: PhoneNumberInvalid = error;
return payload.message;
}
case 'authServiceUnavailable': {
const payload: AuthServiceUnavailable = error;
return `${payload.status}:${payload.message}`;
}
default:
return '';
}
}
describe('AGC 认证命令错误的形状读取', () => {
it('判别后按具名载荷类型分支处理', () => {
expect(
handleByNamedType({
type: 'phoneNumberInvalid',
message: '请输入正确的手机号',
}),
).toBe('请输入正确的手机号');
expect(
handleByNamedType({
type: 'authServiceUnavailable',
status: 503,
message: '登录服务暂时不可用',
}),
).toBe('503:登录服务暂时不可用');
});
it('只按形状读取 Rust 的结构化拒绝,裸字符串与 Error 都不算', () => {
expect(
isClientAuthError({