Skip to main content

wecomx_auth/
error.rs

1//! wecomx-auth 统一错误(错误码段 893300–893399)。
2//!
3//! 每个变体对应本层特有失败;下层(wecomx-transport)错误经 [`AuthError::Transport`]
4//! 委托透传(code 一路透传至 transport 层或后台错误码)。
5
6// Error code range: 893300 - 893399, this crate uses 893300 - 893399.
7
8/// 无可用凭据/token(需要授权但凭据缺失,或缺 bot 凭据无法刷新)。
9pub const E_AUTH_MISSING: i64 = 893301;
10/// 扫码超时(5 分钟)。
11pub const E_QR_TIMEOUT: i64 = 893302;
12/// 凭据加密/解密/密钥失败。
13pub const E_CRYPTO: i64 = 893303;
14/// 凭据存储读写失败(落盘/删除等本地 IO)。
15pub const E_STORAGE: i64 = 893304;
16/// 共享兜底码(仅意料之外的分支 / 系统失败)。
17pub const E_OTHER: i64 = 893999;
18
19/// wecomx-auth 统一错误。
20///
21/// - [`AuthError::Transport`]:wecomx-transport 层错误透传(含后台 errcode 语义)。
22/// - [`AuthError::Other`] 仅用于意料之外的分支 / 系统失败;逻辑错误必须有自己的变体。
23#[derive(Debug)]
24pub enum AuthError {
25    /// 无可用凭据/token:需要授权但凭据缺失,或缺 bot 凭据无法静默刷新。
26    MissingCredentials(String),
27
28    /// 扫码超时(5 分钟),请重试。
29    QrTimeout,
30
31    /// 凭据加密/解密/密钥相关失败。
32    Crypto(String),
33
34    /// 凭据存储读写失败(落盘 / 删除等本地 IO)。
35    Storage(String),
36
37    /// 下层 transport 错误透传(网络 / HTTP / 协议解析 / 后台 errcode)。
38    Transport(wecomx_transport::Error),
39
40    /// 兜底:仅意料之外的分支 / 系统失败使用;逻辑错误必须有自己的变体。
41    Other(Box<dyn std::error::Error + Send + Sync>),
42}
43
44impl AuthError {
45    /// Category error code for this variant.
46    ///
47    /// [`AuthError::Transport`] 委托内层 [`wecomx_transport::Error::code`];
48    /// 本层变体返回各自的 8933xx 码;[`AuthError::Other`] 返回共享兜底码 893999。
49    #[must_use]
50    pub fn code(&self) -> i64 {
51        match self {
52            AuthError::Transport(inner) => inner.code(),
53            AuthError::MissingCredentials(_) => E_AUTH_MISSING,
54            AuthError::QrTimeout => E_QR_TIMEOUT,
55            AuthError::Crypto(_) => E_CRYPTO,
56            AuthError::Storage(_) => E_STORAGE,
57            AuthError::Other(_) => E_OTHER,
58        }
59    }
60
61    #[must_use]
62    pub fn message(&self) -> String {
63        match self {
64            AuthError::Transport(inner) => inner.to_string(),
65            AuthError::MissingCredentials(message)
66            | AuthError::Crypto(message)
67            | AuthError::Storage(message) => message.clone(),
68            AuthError::QrTimeout => "扫码超时(5 分钟),请重试".to_string(),
69            AuthError::Other(e) => e.to_string(),
70        }
71    }
72}
73
74impl std::fmt::Display for AuthError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        let code = self.code();
77        match self {
78            AuthError::MissingCredentials(msg) => write!(f, "AuthError: {msg} [code={code}]"),
79            AuthError::QrTimeout => write!(f, "QrTimeout: {} [code={code}]", self.message()),
80            AuthError::Crypto(msg) => write!(f, "CryptoError: {msg} [code={code}]"),
81            AuthError::Storage(msg) => write!(f, "StorageError: {msg} [code={code}]"),
82            AuthError::Transport(inner) => write!(f, "{inner}"),
83            AuthError::Other(e) => write!(f, "UnknownError: {e} [code={code}]"),
84        }
85    }
86}
87
88impl std::error::Error for AuthError {
89    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90        match self {
91            AuthError::Transport(inner) => Some(inner),
92            AuthError::Other(inner) => Some(inner.as_ref()),
93            _ => None,
94        }
95    }
96}
97
98// ── 下层 → 本层 ────────────────────────────────────────────────
99
100impl From<wecomx_transport::Error> for AuthError {
101    fn from(e: wecomx_transport::Error) -> Self {
102        AuthError::Transport(e)
103    }
104}
105
106// ── 本层 → 下层(跨边界出口)────────────────────────────────────
107
108impl From<AuthError> for wecomx_transport::Error {
109    fn from(e: AuthError) -> Self {
110        match e {
111            // 委托错误直接拆包:保留 Api 等变体的 errcode 语义。
112            AuthError::Transport(inner) => inner,
113            other => wecomx_transport::Error::Other(Box::new(other)),
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    //! ## 模块摘要:AuthError(wecomx-auth 统一错误)
121    //!
122    //! ### 关键接口
123    //! - [AuthError::code] / [AuthError::message] — 本层变体按各自 8933xx 码产出,
124    //!   Transport 委托内层
125    //! - `From` 双向转换 — 下层统一包裹为 Transport;出口方向 Transport 拆包
126
127    use super::*;
128
129    /// P0:[AuthError::code] 本层各变体映射到专属 8933xx 码
130    /// 条件:分别构造各本层变体
131    /// 断言:code() 返回对应常量
132    #[test]
133    fn code_maps_each_variant() {
134        assert_eq!(
135            AuthError::MissingCredentials("x".into()).code(),
136            E_AUTH_MISSING
137        );
138        assert_eq!(AuthError::QrTimeout.code(), E_QR_TIMEOUT);
139        assert_eq!(AuthError::Crypto("x".into()).code(), E_CRYPTO);
140        assert_eq!(AuthError::Storage("x".into()).code(), E_STORAGE);
141        assert_eq!(AuthError::Other("x".into()).code(), E_OTHER);
142    }
143
144    /// P0:[AuthError::code] Transport 委托透传后台错误码
145    /// 条件:Transport(Api{code:853000})
146    /// 断言:code() == 853000
147    #[test]
148    fn code_transport_delegates() {
149        let e = AuthError::Transport(wecomx_transport::Error::Api {
150            message: "invalid".into(),
151            action: "/x".into(),
152            code: Some(853000),
153            body: Box::new(serde_json::Value::Null),
154        });
155        assert_eq!(e.code(), 853000);
156    }
157
158    /// P0:From<wecomx_transport::Error> 包裹为 Transport;反向拆包还原
159    /// 条件:transport Api 错误 → AuthError → transport::Error
160    /// 断言:包裹后匹配 Transport;拆包后保留 Api 变体与 errcode
161    #[test]
162    fn transport_error_roundtrip() {
163        let api = wecomx_transport::Error::Api {
164            message: "expired".into(),
165            action: "/x".into(),
166            code: Some(853004),
167            body: Box::new(serde_json::Value::Null),
168        };
169        let wrapped = AuthError::from(api);
170        assert!(matches!(wrapped, AuthError::Transport(_)));
171
172        let back: wecomx_transport::Error = wrapped.into();
173        assert!(matches!(
174            back,
175            wecomx_transport::Error::Api {
176                code: Some(853004),
177                ..
178            }
179        ));
180    }
181
182    /// P1:本层变体出口装箱为 transport Other,文案经 Display 保留
183    /// 条件:MissingCredentials → transport::Error
184    /// 断言:Other 中可 downcast 回 AuthError,Display 含原始消息与错误码
185    #[test]
186    fn local_variant_boxes_into_transport_other() {
187        let e: wecomx_transport::Error = AuthError::MissingCredentials("need login".into()).into();
188        match e {
189            wecomx_transport::Error::Other(boxed) => {
190                let down = boxed.downcast_ref::<AuthError>();
191                assert!(
192                    down.is_some_and(|e| matches!(e, AuthError::MissingCredentials(_))),
193                    "expected AuthError::MissingCredentials"
194                );
195            }
196            other => panic!("expected Other, got {other:?}"),
197        }
198    }
199}