diff --git a/crates/presenter/src/host/accounts/failure.rs b/crates/presenter/src/host/accounts/failure.rs index 541e071..b04833e 100644 --- a/crates/presenter/src/host/accounts/failure.rs +++ b/crates/presenter/src/host/accounts/failure.rs @@ -5,6 +5,8 @@ use unlucky_core::auth::AuthError; +use unlucky_core::auth::BodyKind; + use crate::dto::{AccountNoticeCode, LoginFailure}; /// 账户写操作的失败:**结构化码 + 原文**(原文只进日志/详细信息,不上屏)。 @@ -44,10 +46,17 @@ pub(super) fn classify(error: &AuthError) -> LoginFailure { LoginFailure::Denied } } + // 只看 `ResponseDetail` 的**结构化字段**(status / kind / server_message), + // 不把整条错误 Debug 成字符串再去扫——那会把中文 `what`/`digest` 也算进判据里。 AuthError::Response(detail) => { - let blob = format!("{detail:?}"); - if mentions_whitelist(&blob) { + let server = detail.server_message.as_deref().unwrap_or_default(); + let digest = detail.digest.as_str(); + if mentions_whitelist(server) || mentions_whitelist(digest) { LoginFailure::NotWhitelisted + } else if detail.status >= 500 || matches!(detail.kind, BodyKind::Html | BodyKind::Text) + { + // 5xx / 拦截页:服务器或链路出了问题,不是「它拒绝了你」。 + LoginFailure::Unreachable } else { LoginFailure::Denied } @@ -67,3 +76,51 @@ pub(super) fn mentions_whitelist(text: &str) -> bool { || lowered.contains("whitelist") || lowered.contains("not approved") } + +#[cfg(test)] +mod tests { + use super::*; + use unlucky_core::auth::body::ResponseDetail; + + fn response(status: u16, kind: BodyKind, server_message: Option<&str>) -> AuthError { + AuthError::Response(Box::new(ResponseDetail { + step: "xsts".to_string(), + what: "XSTS 授权".to_string(), + status, + content_type: None, + kind, + server_message: server_message.map(str::to_string), + digest: "digest".to_string(), + })) + } + + /// 403 带 AADSTS 码 → 白名单未批;403 不带 → 一般性拒绝。 + #[test] + fn aadsts_markers_win_over_the_generic_denial() { + assert_eq!( + classify(&response( + 403, + BodyKind::Json, + Some("AADSTS7000218: app not approved") + )), + LoginFailure::NotWhitelisted + ); + assert_eq!( + classify(&response(403, BodyKind::Json, Some("invalid_grant"))), + LoginFailure::Denied + ); + } + + /// 5xx 与「拦截页」(HTML/纯文本)归「连不上」,不是「服务器拒绝了你」。 + #[test] + fn upstream_failures_and_intercept_pages_are_unreachable() { + assert_eq!( + classify(&response(503, BodyKind::Json, None)), + LoginFailure::Unreachable + ); + assert_eq!( + classify(&response(200, BodyKind::Html, None)), + LoginFailure::Unreachable + ); + } +}