diff --git a/Cargo.lock b/Cargo.lock index e59fc01..76f8edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,7 +1971,7 @@ dependencies = [ [[package]] name = "wechat-cli" -version = "0.4.0" +version = "0.5.0" dependencies = [ "aes", "anyhow", diff --git a/src/wechat/api.rs b/src/wechat/api.rs index 619e598..a019a72 100644 --- a/src/wechat/api.rs +++ b/src/wechat/api.rs @@ -121,23 +121,17 @@ impl WeixinApiClient { headers } - async fn post_json( + async fn request( &self, path: &str, - body: &TReq, + body_provider: impl FnOnce() -> reqwest::RequestBuilder, timeout: Duration, ) -> Result where - TReq: Serialize + ?Sized, TResp: DeserializeOwned, { let url = format!("{}/{}", ILINK_API_ROOT, path); - let body_bytes = serde_json::to_vec(body).context("failed to serialize request body")?; - let response_bytes = self - .client - .post(&url) - .headers(self.json_headers(body_bytes.len())) - .body(body_bytes) + let response_bytes = body_provider() .timeout(timeout) .send() .await @@ -149,6 +143,28 @@ impl WeixinApiClient { Self::decode_response(&response_bytes) } + async fn post_json( + &self, + path: &str, + body: &TReq, + timeout: Duration, + ) -> Result + where + TReq: Serialize + ?Sized, + TResp: DeserializeOwned, + { + let body_bytes = serde_json::to_vec(body).context("failed to serialize request body")?; + let headers = self.json_headers(body_bytes.len()); + let url = format!("{}/{}", ILINK_API_ROOT, path); + + self.request( + path, + || self.client.post(&url).headers(headers).body(body_bytes), + timeout, + ) + .await + } + async fn post_form( &self, path: &str, @@ -159,20 +175,17 @@ impl WeixinApiClient { TResp: DeserializeOwned, { let url = format!("{}/{}", ILINK_API_ROOT, path); - let response_bytes = self - .client - .post(&url) - .headers(self.auth_headers()) - .form(form) - .timeout(timeout) - .send() - .await - .with_context(|| format!("error sending request for url ({url})"))? - .bytes() - .await - .with_context(|| format!("error reading response body for url ({url})"))?; - - Self::decode_response(&response_bytes) + self.request( + path, + || { + self.client + .post(&url) + .headers(self.auth_headers()) + .form(form) + }, + timeout, + ) + .await } fn decode_response(response_bytes: &[u8]) -> Result @@ -182,6 +195,7 @@ impl WeixinApiClient { let status: ApiStatus = serde_json::from_slice(response_bytes).context("failed to decode API status")?; + // Priority 1: errcode (often for session/auth errors) if let Some(code) = status.errcode { if code == SESSION_EXPIRED_ERRCODE { return Err(SessionExpiredError.into()); @@ -195,15 +209,17 @@ impl WeixinApiClient { } } + // Priority 2: ret (often for business logic errors) if let Some(code) = status.ret { if code != 0 { - return Err(ApiError { - code, - message: status + let message = match code { + -2 => "Invalid context token".to_string(), + -3 => "User ID mismatch or not found".to_string(), + _ => status .err_msg .unwrap_or_else(|| "unknown error".to_string()), - } - .into()); + }; + return Err(ApiError { code, message }.into()); } } @@ -294,4 +310,51 @@ mod tests { assert_eq!(client.bot_token, "tok_123"); assert!(client.route_tag.is_none()); } + + #[test] + fn test_decode_response_success() { + let bytes = b"{}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_ok()); + } + + #[test] + fn test_decode_response_invalid_token() { + let bytes = b"{\"ret\":-2}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("Invalid context token")); + assert!(err.to_string().contains("-2")); + } + + #[test] + fn test_decode_response_wrong_user() { + let bytes = b"{\"ret\":-3}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("User ID mismatch or not found")); + assert!(err.to_string().contains("-3")); + } + + #[test] + fn test_decode_response_session_expired() { + let bytes = b"{\"errcode\":-14,\"errmsg\":\"session timeout\"}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(is_session_expired(&err)); + assert!(err.to_string().contains("Session expired")); + } + + #[test] + fn test_decode_response_unknown_api_error() { + let bytes = b"{\"ret\":-99, \"err_msg\":\"something went wrong\"}"; + let res: Result = WeixinApiClient::decode_response(bytes); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!(err.to_string().contains("something went wrong")); + assert!(err.to_string().contains("-99")); + } }