1use parking_lot::Mutex;
38use sha1::Digest;
39use std::collections::VecDeque;
40use std::sync::Arc;
41use thiserror::Error;
42
43#[derive(Debug, Error)]
49pub enum WechatError {
50 #[error("微信配置错误: {0}")]
52 Config(String),
53 #[error("微信字段缺失: {0}")]
55 MissingField(String),
56 #[error("微信 API 调用失败: {0}")]
58 ApiFailed(String),
59 #[error("微信 HTTP 传输失败: {0}")]
61 HttpTransport(String),
62 #[error("微信序列化失败: {0}")]
64 Serialize(String),
65 #[error("微信 access_token 获取失败: {0}")]
67 TokenFailed(String),
68 #[error("微信解密失败: {0}")]
70 DecryptFailed(String),
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum WechatAppType {
83 OfficialAccount,
85 MiniProgram,
87 OpenPlatform,
89 Work,
91}
92
93#[derive(Debug, Clone)]
131pub struct WechatConfig {
132 pub app_type: WechatAppType,
134 pub app_id: String,
136 pub app_secret: String,
138 pub token: Option<String>,
140 pub encoding_aes_key: Option<String>,
142 pub oauth_redirect_uri: Option<String>,
144}
145
146impl WechatConfig {
147 pub fn new(
155 app_type: WechatAppType,
156 app_id: impl Into<String>,
157 app_secret: impl Into<String>,
158 ) -> Self {
159 Self {
160 app_type,
161 app_id: app_id.into(),
162 app_secret: app_secret.into(),
163 token: None,
164 encoding_aes_key: None,
165 oauth_redirect_uri: None,
166 }
167 }
168
169 pub fn with_token(mut self, token: impl Into<String>) -> Self {
171 self.token = Some(token.into());
172 self
173 }
174
175 pub fn with_encoding_aes_key(mut self, key: impl Into<String>) -> Self {
177 self.encoding_aes_key = Some(key.into());
178 self
179 }
180
181 pub fn with_oauth_redirect_uri(mut self, uri: impl Into<String>) -> Self {
183 self.oauth_redirect_uri = Some(uri.into());
184 self
185 }
186
187 pub fn validate(&self) -> Result<(), WechatError> {
191 if self.app_id.is_empty() {
192 return Err(WechatError::Config("app_id".into()));
193 }
194 if self.app_secret.is_empty() {
195 return Err(WechatError::Config("app_secret".into()));
196 }
197 Ok(())
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum JsApiTicket {
208 JsApi,
210 WxCard,
212}
213
214#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
237pub struct WechatUser {
238 pub openid: String,
240 pub nickname: Option<String>,
242 pub sex: Option<i32>,
244 pub province: Option<String>,
246 pub city: Option<String>,
248 pub country: Option<String>,
250 pub headimgurl: Option<String>,
252 pub privilege: Option<Vec<String>>,
254 pub unionid: Option<String>,
256 pub raw: serde_json::Value,
258}
259
260pub trait WechatHttpTransport: Send + Sync {
273 fn get(&self, url: &str) -> Result<String, WechatError>;
283
284 fn post_json(&self, url: &str, body: &str) -> Result<String, WechatError>;
295
296 fn post_form(&self, url: &str, body: &str) -> Result<String, WechatError>;
307}
308
309#[derive(Debug, Default)]
333pub struct MemoryWechatHttpTransport {
334 responses: Mutex<VecDeque<String>>,
336 requests: Mutex<Vec<(String, String, String)>>,
338}
339
340impl MemoryWechatHttpTransport {
341 pub fn new() -> Self {
343 Self::default()
344 }
345
346 pub fn push_response(&self, response: impl Into<String>) {
348 self.responses.lock().push_back(response.into());
349 }
350
351 pub fn request_count(&self) -> usize {
353 self.requests.lock().len()
354 }
355
356 pub fn requests(&self) -> Vec<(String, String, String)> {
360 self.requests.lock().clone()
361 }
362
363 pub fn clear(&self) {
365 self.responses.lock().clear();
366 self.requests.lock().clear();
367 }
368
369 fn next_response(&self) -> Result<String, WechatError> {
371 match self.responses.lock().pop_front() {
372 Some(resp) => Ok(resp),
373 None => Err(WechatError::HttpTransport("无可用预置响应".into())),
374 }
375 }
376}
377
378impl WechatHttpTransport for MemoryWechatHttpTransport {
379 fn get(&self, url: &str) -> Result<String, WechatError> {
380 let response = self.next_response()?;
381 self.requests
382 .lock()
383 .push(("GET".to_string(), url.to_string(), String::new()));
384 Ok(response)
385 }
386
387 fn post_json(&self, url: &str, body: &str) -> Result<String, WechatError> {
388 let response = self.next_response()?;
389 self.requests
390 .lock()
391 .push(("POST_JSON".to_string(), url.to_string(), body.to_string()));
392 Ok(response)
393 }
394
395 fn post_form(&self, url: &str, body: &str) -> Result<String, WechatError> {
396 let response = self.next_response()?;
397 self.requests
398 .lock()
399 .push(("POST_FORM".to_string(), url.to_string(), body.to_string()));
400 Ok(response)
401 }
402}
403
404pub struct WechatSdk {
433 config: WechatConfig,
435 transport: Arc<dyn WechatHttpTransport>,
437}
438
439impl WechatSdk {
440 pub fn new(config: WechatConfig, transport: Arc<dyn WechatHttpTransport>) -> Self {
447 Self { config, transport }
448 }
449
450 pub fn config(&self) -> &WechatConfig {
452 &self.config
453 }
454
455 pub fn get_authorize_url(&self, scope: &str, state: &str) -> String {
466 let redirect_uri = self.config.oauth_redirect_uri.as_deref().unwrap_or("");
467 format!(
468 "https://open.weixin.qq.com/connect/oauth2/authorize?appid={}&redirect_uri={}&response_type=code&scope={}&state={}#wechat_redirect",
469 percent_encode(&self.config.app_id),
470 percent_encode(redirect_uri),
471 percent_encode(scope),
472 percent_encode(state),
473 )
474 }
475
476 pub fn get_user_by_code(&self, code: &str) -> Result<WechatUser, WechatError> {
490 self.config.validate()?;
491 if code.is_empty() {
492 return Err(WechatError::MissingField("code".into()));
493 }
494
495 let url = format!(
497 "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code",
498 percent_encode(&self.config.app_id),
499 percent_encode(&self.config.app_secret),
500 percent_encode(code),
501 );
502 let response = self.transport.get(&url)?;
503 let token_json: serde_json::Value = serde_json::from_str(&response)
504 .map_err(|err| WechatError::Serialize(format!("解析 token 响应失败: {err}")))?;
505
506 if let Some(errcode) = token_json.get("errcode").and_then(|v| v.as_i64()) {
507 if errcode != 0 {
508 return Err(WechatError::ApiFailed(format!(
509 "code 换取 token 失败: errcode={errcode}"
510 )));
511 }
512 }
513
514 let access_token = token_json
515 .get("access_token")
516 .and_then(|v| v.as_str())
517 .ok_or_else(|| WechatError::TokenFailed("响应缺少 access_token".into()))?
518 .to_string();
519 let openid = token_json
520 .get("openid")
521 .and_then(|v| v.as_str())
522 .ok_or_else(|| WechatError::MissingField("openid".into()))?
523 .to_string();
524
525 self.get_user_info(&openid, &access_token)
527 }
528
529 pub fn get_user_info(
543 &self,
544 openid: &str,
545 access_token: &str,
546 ) -> Result<WechatUser, WechatError> {
547 self.config.validate()?;
548 if openid.is_empty() {
549 return Err(WechatError::MissingField("openid".into()));
550 }
551 if access_token.is_empty() {
552 return Err(WechatError::MissingField("access_token".into()));
553 }
554
555 let url = format!(
556 "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}",
557 percent_encode(access_token),
558 percent_encode(openid),
559 );
560 let response = self.transport.get(&url)?;
561 let json: serde_json::Value = serde_json::from_str(&response)
562 .map_err(|err| WechatError::Serialize(format!("解析用户信息响应失败: {err}")))?;
563
564 if let Some(errcode) = json.get("errcode").and_then(|v| v.as_i64()) {
565 if errcode != 0 {
566 return Err(WechatError::ApiFailed(format!(
567 "获取用户信息失败: errcode={errcode}"
568 )));
569 }
570 }
571
572 Ok(WechatUser {
573 openid: json
574 .get("openid")
575 .and_then(|v| v.as_str())
576 .unwrap_or_default()
577 .to_string(),
578 nickname: json
579 .get("nickname")
580 .and_then(|v| v.as_str())
581 .map(String::from),
582 sex: json.get("sex").and_then(|v| v.as_i64()).map(|v| v as i32),
583 province: json
584 .get("province")
585 .and_then(|v| v.as_str())
586 .map(String::from),
587 city: json.get("city").and_then(|v| v.as_str()).map(String::from),
588 country: json
589 .get("country")
590 .and_then(|v| v.as_str())
591 .map(String::from),
592 headimgurl: json
593 .get("headimgurl")
594 .and_then(|v| v.as_str())
595 .map(String::from),
596 privilege: json.get("privilege").and_then(|v| v.as_array()).map(|arr| {
597 arr.iter()
598 .filter_map(|v| v.as_str().map(String::from))
599 .collect()
600 }),
601 unionid: json
602 .get("unionid")
603 .and_then(|v| v.as_str())
604 .map(String::from),
605 raw: json,
606 })
607 }
608
609 pub fn verify_signature(
624 &self,
625 signature: &str,
626 timestamp: &str,
627 nonce: &str,
628 token: &str,
629 ) -> bool {
630 let mut hasher = sha1::Sha1::new();
631 hasher.update(token.as_bytes());
632 hasher.update(timestamp.as_bytes());
633 hasher.update(nonce.as_bytes());
634 let computed = hex::encode(hasher.finalize());
635 computed == signature
636 }
637
638 pub fn send_template_message(
654 &self,
655 touser: &str,
656 template_id: &str,
657 data: &serde_json::Value,
658 ) -> Result<(), WechatError> {
659 self.config.validate()?;
660 if touser.is_empty() {
661 return Err(WechatError::MissingField("touser".into()));
662 }
663 if template_id.is_empty() {
664 return Err(WechatError::MissingField("template_id".into()));
665 }
666
667 let access_token = self.fetch_access_token()?;
669
670 let url = format!(
672 "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={}",
673 percent_encode(&access_token),
674 );
675 let body = serde_json::json!({
676 "touser": touser,
677 "template_id": template_id,
678 "data": data,
679 });
680 let body_str =
681 serde_json::to_string(&body).map_err(|err| WechatError::Serialize(err.to_string()))?;
682 let response = self.transport.post_json(&url, &body_str)?;
683 let json: serde_json::Value = serde_json::from_str(&response)
684 .map_err(|err| WechatError::Serialize(format!("解析响应失败: {err}")))?;
685
686 let errcode = json.get("errcode").and_then(|v| v.as_i64()).unwrap_or(0);
687 if errcode != 0 {
688 return Err(WechatError::ApiFailed(format!(
689 "发送模板消息失败: errcode={errcode}"
690 )));
691 }
692 Ok(())
693 }
694
695 pub fn generate_jsapi_signature(
710 &self,
711 url: &str,
712 noncestr: &str,
713 timestamp: i64,
714 jsapi_ticket: &str,
715 ) -> String {
716 let input = format!(
717 "jsapi_ticket={}&noncestr={}×tamp={}&url={}",
718 jsapi_ticket, noncestr, timestamp, url,
719 );
720 let mut hasher = sha1::Sha1::new();
721 hasher.update(input.as_bytes());
722 hex::encode(hasher.finalize())
723 }
724
725 pub fn get_qrcode_url(&self, scene_str: &str) -> Result<String, WechatError> {
740 self.config.validate()?;
741 if scene_str.is_empty() {
742 return Err(WechatError::MissingField("scene_str".into()));
743 }
744
745 let access_token = self.fetch_access_token()?;
747
748 let url = format!(
750 "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={}",
751 percent_encode(&access_token),
752 );
753 let body = serde_json::json!({
754 "action_name": "QR_LIMIT_STR_SCENE",
755 "action_info": {
756 "scene": {
757 "scene_str": scene_str,
758 }
759 }
760 });
761 let body_str =
762 serde_json::to_string(&body).map_err(|err| WechatError::Serialize(err.to_string()))?;
763 let response = self.transport.post_json(&url, &body_str)?;
764 let json: serde_json::Value = serde_json::from_str(&response)
765 .map_err(|err| WechatError::Serialize(format!("解析响应失败: {err}")))?;
766
767 if let Some(errcode) = json.get("errcode").and_then(|v| v.as_i64()) {
768 if errcode != 0 {
769 return Err(WechatError::ApiFailed(format!(
770 "获取二维码 ticket 失败: errcode={errcode}"
771 )));
772 }
773 }
774
775 let ticket = json
776 .get("ticket")
777 .and_then(|v| v.as_str())
778 .ok_or_else(|| WechatError::MissingField("ticket".into()))?;
779 Ok(format!(
780 "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={}",
781 percent_encode(ticket)
782 ))
783 }
784
785 fn fetch_access_token(&self) -> Result<String, WechatError> {
790 let url = format!(
791 "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}",
792 percent_encode(&self.config.app_id),
793 percent_encode(&self.config.app_secret),
794 );
795 let response = self.transport.get(&url)?;
796 let json: serde_json::Value = serde_json::from_str(&response)
797 .map_err(|err| WechatError::Serialize(format!("解析 token 响应失败: {err}")))?;
798
799 if let Some(errcode) = json.get("errcode").and_then(|v| v.as_i64()) {
800 if errcode != 0 {
801 return Err(WechatError::TokenFailed(format!(
802 "获取 access_token 失败: errcode={errcode}"
803 )));
804 }
805 }
806
807 json.get("access_token")
808 .and_then(|v| v.as_str())
809 .map(String::from)
810 .ok_or_else(|| WechatError::TokenFailed("响应缺少 access_token".into()))
811 }
812}
813
814fn percent_encode(input: &str) -> String {
823 let mut output = String::with_capacity(input.len());
824 for byte in input.as_bytes() {
825 if matches!(
826 byte,
827 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
828 ) {
829 output.push(*byte as char);
830 } else {
831 output.push_str(&format!("%{byte:02X}"));
832 }
833 }
834 output
835}
836
837#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
851 fn test_wechat_app_type() {
852 assert_eq!(
854 WechatAppType::OfficialAccount,
855 WechatAppType::OfficialAccount
856 );
857 assert_eq!(WechatAppType::MiniProgram, WechatAppType::MiniProgram);
858 assert_eq!(WechatAppType::OpenPlatform, WechatAppType::OpenPlatform);
859 assert_eq!(WechatAppType::Work, WechatAppType::Work);
860
861 assert_ne!(WechatAppType::OfficialAccount, WechatAppType::MiniProgram);
863 assert_ne!(WechatAppType::OpenPlatform, WechatAppType::Work);
864
865 let app_type = WechatAppType::OfficialAccount;
867 let cloned = app_type;
868 assert_eq!(app_type, cloned);
869
870 let set: std::collections::HashSet<WechatAppType> = [
872 WechatAppType::OfficialAccount,
873 WechatAppType::MiniProgram,
874 WechatAppType::OpenPlatform,
875 WechatAppType::Work,
876 ]
877 .into_iter()
878 .collect();
879 assert_eq!(set.len(), 4);
880 assert!(set.contains(&WechatAppType::OfficialAccount));
881 assert!(set.contains(&WechatAppType::MiniProgram));
882 assert!(set.contains(&WechatAppType::OpenPlatform));
883 assert!(set.contains(&WechatAppType::Work));
884
885 assert_eq!(
887 format!("{:?}", WechatAppType::OfficialAccount),
888 "OfficialAccount"
889 );
890 }
891
892 #[test]
898 fn test_wechat_config_builder() {
899 let config =
900 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret")
901 .with_token("wx_token")
902 .with_encoding_aes_key("wx_aes_key")
903 .with_oauth_redirect_uri("https://example.com/oauth/callback");
904
905 assert_eq!(config.app_type, WechatAppType::OfficialAccount);
906 assert_eq!(config.app_id, "wx_app_id");
907 assert_eq!(config.app_secret, "wx_app_secret");
908 assert_eq!(config.token.as_deref(), Some("wx_token"));
909 assert_eq!(config.encoding_aes_key.as_deref(), Some("wx_aes_key"));
910 assert_eq!(
911 config.oauth_redirect_uri.as_deref(),
912 Some("https://example.com/oauth/callback")
913 );
914
915 assert!(config.validate().is_ok());
917
918 let minimal = WechatConfig::new(WechatAppType::MiniProgram, "wx_mini", "secret");
920 assert_eq!(minimal.app_type, WechatAppType::MiniProgram);
921 assert_eq!(minimal.app_id, "wx_mini");
922 assert_eq!(minimal.app_secret, "secret");
923 assert!(minimal.token.is_none());
924 assert!(minimal.encoding_aes_key.is_none());
925 assert!(minimal.oauth_redirect_uri.is_none());
926 assert!(minimal.validate().is_ok());
927 }
928
929 #[test]
931 fn test_wechat_config_validate() {
932 let config = WechatConfig::new(WechatAppType::OfficialAccount, "app_id", "secret");
934 assert!(config.validate().is_ok());
935
936 let config = WechatConfig::new(WechatAppType::OfficialAccount, "", "secret");
938 let err = config.validate().unwrap_err();
939 match err {
940 WechatError::Config(field) => assert_eq!(field, "app_id"),
941 other => panic!("期望 Config, 实际 {other:?}"),
942 }
943
944 let config = WechatConfig::new(WechatAppType::OfficialAccount, "app_id", "");
946 let err = config.validate().unwrap_err();
947 match err {
948 WechatError::Config(field) => assert_eq!(field, "app_secret"),
949 other => panic!("期望 Config, 实际 {other:?}"),
950 }
951
952 let config = WechatConfig::new(WechatAppType::OfficialAccount, "", "");
954 let err = config.validate().unwrap_err();
955 match err {
956 WechatError::Config(field) => assert_eq!(field, "app_id"),
957 other => panic!("期望 Config, 实际 {other:?}"),
958 }
959 }
960
961 #[test]
967 fn test_wechat_user_default() {
968 let user = WechatUser::default();
969 assert!(user.openid.is_empty());
970 assert!(user.nickname.is_none());
971 assert!(user.sex.is_none());
972 assert!(user.province.is_none());
973 assert!(user.city.is_none());
974 assert!(user.country.is_none());
975 assert!(user.headimgurl.is_none());
976 assert!(user.privilege.is_none());
977 assert!(user.unionid.is_none());
978 assert!(user.raw.is_null());
979 }
980
981 #[test]
983 fn test_wechat_user_serialize() {
984 let user = WechatUser {
985 openid: "openid_abc".to_string(),
986 nickname: Some("test_user".into()),
987 sex: Some(1),
988 province: Some("广东".into()),
989 city: Some("深圳".into()),
990 country: Some("中国".into()),
991 headimgurl: Some("https://example.com/avatar.png".into()),
992 privilege: Some(vec!["priv1".into(), "priv2".into()]),
993 unionid: Some("unionid_xyz".into()),
994 raw: serde_json::json!({"custom": "field"}),
995 };
996
997 let json = serde_json::to_string(&user).expect("序列化失败");
998 let parsed: WechatUser = serde_json::from_str(&json).expect("反序列化失败");
999
1000 assert_eq!(parsed.openid, "openid_abc");
1001 assert_eq!(parsed.nickname.as_deref(), Some("test_user"));
1002 assert_eq!(parsed.sex, Some(1));
1003 assert_eq!(parsed.province.as_deref(), Some("广东"));
1004 assert_eq!(parsed.city.as_deref(), Some("深圳"));
1005 assert_eq!(parsed.country.as_deref(), Some("中国"));
1006 assert_eq!(
1007 parsed.headimgurl.as_deref(),
1008 Some("https://example.com/avatar.png")
1009 );
1010 assert_eq!(
1011 parsed.privilege.as_deref(),
1012 Some(&vec!["priv1".to_string(), "priv2".to_string()][..])
1013 );
1014 assert_eq!(parsed.unionid.as_deref(), Some("unionid_xyz"));
1015 assert_eq!(parsed.raw["custom"], "field");
1016 }
1017
1018 #[test]
1024 fn test_wechat_sdk_get_authorize_url() {
1025 let config =
1027 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret")
1028 .with_oauth_redirect_uri("https://example.com/oauth/callback");
1029 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1030
1031 let url = sdk.get_authorize_url("snsapi_userinfo", "state_abc");
1032 assert!(
1033 url.starts_with("https://open.weixin.qq.com/connect/oauth2/authorize?"),
1034 "URL 前缀错误: {url}"
1035 );
1036 assert!(url.contains("appid=wx_app_id"));
1037 assert!(url.contains("redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback"));
1039 assert!(url.contains("response_type=code"));
1040 assert!(url.contains("scope=snsapi_userinfo"));
1041 assert!(url.contains("state=state_abc"));
1042 assert!(url.ends_with("#wechat_redirect"));
1043
1044 let config =
1046 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1047 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1048 let url = sdk.get_authorize_url("snsapi_base", "state_123");
1049 assert!(url.contains("redirect_uri="));
1050 assert!(url.contains("scope=snsapi_base"));
1051 assert!(url.contains("state=state_123"));
1052 }
1053
1054 #[test]
1060 fn test_wechat_sdk_verify_signature() {
1061 let config =
1062 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1063 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1064
1065 let token = "test_token";
1066 let timestamp = "1234567890";
1067 let nonce = "nonce_abc";
1068
1069 let mut hasher = sha1::Sha1::new();
1071 hasher.update(token.as_bytes());
1072 hasher.update(timestamp.as_bytes());
1073 hasher.update(nonce.as_bytes());
1074 let expected = hex::encode(hasher.finalize());
1075
1076 assert!(sdk.verify_signature(&expected, timestamp, nonce, token));
1078
1079 assert!(!sdk.verify_signature("wrong_signature", timestamp, nonce, token));
1081
1082 assert!(sdk.verify_signature("da39a3ee5e6b4b0d3255bfef95601890afd80709", "", "", ""));
1084
1085 let mut hasher = sha1::Sha1::new();
1087 hasher.update(timestamp.as_bytes());
1088 hasher.update(token.as_bytes());
1089 hasher.update(nonce.as_bytes());
1090 let wrong_order = hex::encode(hasher.finalize());
1091 assert!(!sdk.verify_signature(&wrong_order, timestamp, nonce, token));
1092 }
1093
1094 #[test]
1100 fn test_wechat_sdk_generate_jsapi_signature() {
1101 let config =
1102 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1103 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1104
1105 let url = "https://example.com/page";
1106 let noncestr = "abc123";
1107 let timestamp = 1609459200_i64;
1108 let jsapi_ticket = "ticket_value";
1109
1110 let input = format!(
1112 "jsapi_ticket={}&noncestr={}×tamp={}&url={}",
1113 jsapi_ticket, noncestr, timestamp, url,
1114 );
1115 let mut hasher = sha1::Sha1::new();
1116 hasher.update(input.as_bytes());
1117 let expected = hex::encode(hasher.finalize());
1118
1119 let signature = sdk.generate_jsapi_signature(url, noncestr, timestamp, jsapi_ticket);
1120 assert_eq!(signature, expected);
1121 assert_eq!(signature.len(), 40);
1123
1124 let other =
1126 sdk.generate_jsapi_signature("https://other.com", noncestr, timestamp, jsapi_ticket);
1127 assert_ne!(signature, other);
1128
1129 let empty_sig = sdk.generate_jsapi_signature("", "", 0, "");
1131 assert_eq!(empty_sig.len(), 40);
1132 }
1133
1134 #[test]
1140 fn test_wechat_sdk_get_user_by_code() {
1141 let transport = Arc::new(MemoryWechatHttpTransport::new());
1142 transport.push_response(
1144 r#"{"access_token":"token123","expires_in":7200,"openid":"openid_abc"}"#,
1145 );
1146 transport.push_response(
1147 r#"{"openid":"openid_abc","nickname":"test_user","sex":1,"province":"广东","city":"深圳","country":"中国","headimgurl":"https://example.com/avatar.png","unionid":"unionid_xyz"}"#,
1148 );
1149
1150 let config =
1151 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1152 let sdk = WechatSdk::new(config, transport.clone());
1153
1154 let user = sdk
1155 .get_user_by_code("auth_code_abc")
1156 .expect("get_user_by_code 失败");
1157
1158 assert_eq!(user.openid, "openid_abc");
1160 assert_eq!(user.nickname.as_deref(), Some("test_user"));
1161 assert_eq!(user.sex, Some(1));
1162 assert_eq!(user.province.as_deref(), Some("广东"));
1163 assert_eq!(user.city.as_deref(), Some("深圳"));
1164 assert_eq!(user.country.as_deref(), Some("中国"));
1165 assert_eq!(
1166 user.headimgurl.as_deref(),
1167 Some("https://example.com/avatar.png")
1168 );
1169 assert_eq!(user.unionid.as_deref(), Some("unionid_xyz"));
1170 assert_eq!(user.raw["nickname"], "test_user");
1172
1173 assert_eq!(transport.request_count(), 2);
1175
1176 let requests = transport.requests();
1178 assert_eq!(requests[0].0, "GET");
1179 assert!(requests[0].1.contains("sns/oauth2/access_token"));
1180 assert!(requests[0].1.contains("code=auth_code_abc"));
1181
1182 assert_eq!(requests[1].0, "GET");
1184 assert!(requests[1].1.contains("sns/userinfo"));
1185 }
1186
1187 #[test]
1189 fn test_wechat_sdk_get_user_by_code_empty_code() {
1190 let config =
1191 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1192 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1193
1194 let err = sdk.get_user_by_code("").unwrap_err();
1195 match err {
1196 WechatError::MissingField(field) => assert_eq!(field, "code"),
1197 other => panic!("期望 MissingField, 实际 {other:?}"),
1198 }
1199 }
1200
1201 #[test]
1203 fn test_wechat_sdk_get_user_by_code_missing_access_token() {
1204 let transport = MemoryWechatHttpTransport::new();
1205 transport.push_response(r#"{"errcode":40029,"errmsg":"invalid code"}"#);
1206
1207 let config =
1208 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1209 let sdk = WechatSdk::new(config, Arc::new(transport));
1210
1211 let err = sdk.get_user_by_code("invalid_code").unwrap_err();
1212 assert!(matches!(err, WechatError::ApiFailed(_)));
1213 }
1214
1215 #[test]
1221 fn test_wechat_sdk_get_user_info() {
1222 let transport = Arc::new(MemoryWechatHttpTransport::new());
1223 transport.push_response(
1224 r#"{"openid":"openid_xyz","nickname":"user_info","sex":2,"province":"北京","city":"北京","country":"中国","headimgurl":"https://example.com/avatar2.png","privilege":["priv_a","priv_b"]}"#,
1225 );
1226
1227 let config =
1228 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1229 let sdk = WechatSdk::new(config, transport.clone());
1230
1231 let user = sdk
1232 .get_user_info("openid_xyz", "access_token_123")
1233 .expect("get_user_info 失败");
1234
1235 assert_eq!(user.openid, "openid_xyz");
1236 assert_eq!(user.nickname.as_deref(), Some("user_info"));
1237 assert_eq!(user.sex, Some(2));
1238 assert_eq!(user.province.as_deref(), Some("北京"));
1239 assert_eq!(user.city.as_deref(), Some("北京"));
1240 assert_eq!(user.country.as_deref(), Some("中国"));
1241 assert_eq!(
1242 user.headimgurl.as_deref(),
1243 Some("https://example.com/avatar2.png")
1244 );
1245 assert_eq!(
1246 user.privilege.as_deref(),
1247 Some(&vec!["priv_a".to_string(), "priv_b".to_string()][..])
1248 );
1249 assert_eq!(user.raw["openid"], "openid_xyz");
1251
1252 assert_eq!(transport.request_count(), 1);
1254 let requests = transport.requests();
1255 assert_eq!(requests[0].0, "GET");
1256 assert!(requests[0].1.contains("sns/userinfo"));
1257 assert!(requests[0].1.contains("access_token=access_token_123"));
1258 assert!(requests[0].1.contains("openid=openid_xyz"));
1259 }
1260
1261 #[test]
1263 fn test_wechat_sdk_get_user_info_empty_fields() {
1264 let config =
1265 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1266 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1267
1268 let err = sdk.get_user_info("", "token").unwrap_err();
1270 match err {
1271 WechatError::MissingField(field) => assert_eq!(field, "openid"),
1272 other => panic!("期望 MissingField, 实际 {other:?}"),
1273 }
1274
1275 let err = sdk.get_user_info("openid", "").unwrap_err();
1277 match err {
1278 WechatError::MissingField(field) => assert_eq!(field, "access_token"),
1279 other => panic!("期望 MissingField, 实际 {other:?}"),
1280 }
1281 }
1282
1283 #[test]
1289 fn test_wechat_sdk_send_template_message() {
1290 let transport = Arc::new(MemoryWechatHttpTransport::new());
1291 transport.push_response(r#"{"access_token":"token123","expires_in":7200}"#);
1293 transport.push_response(r#"{"errcode":0,"errmsg":"ok","msgid":123456789}"#);
1294
1295 let config =
1296 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1297 let sdk = WechatSdk::new(config, transport.clone());
1298
1299 let data = serde_json::json!({
1300 "first": {"value": "您好,订单已支付"},
1301 "keyword1": {"value": "鲜视达订单 #20240101"},
1302 "remark": {"value": "感谢您的惠顾"}
1303 });
1304 sdk.send_template_message("openid_user", "tpl_id_abc", &data)
1305 .expect("send_template_message 失败");
1306
1307 assert_eq!(transport.request_count(), 2);
1309 let requests = transport.requests();
1310
1311 assert_eq!(requests[0].0, "GET");
1313 assert!(requests[0].1.contains("cgi-bin/token"));
1314 assert!(requests[0].1.contains("grant_type=client_credential"));
1315
1316 assert_eq!(requests[1].0, "POST_JSON");
1318 assert!(requests[1].1.contains("cgi-bin/message/template/send"));
1319 assert!(requests[1].1.contains("access_token=token123"));
1320 let body: serde_json::Value = serde_json::from_str(&requests[1].2).expect("body 应为 JSON");
1322 assert_eq!(body["touser"], "openid_user");
1323 assert_eq!(body["template_id"], "tpl_id_abc");
1324 assert_eq!(body["data"]["first"]["value"], "您好,订单已支付");
1325 }
1326
1327 #[test]
1329 fn test_wechat_sdk_send_template_message_empty_fields() {
1330 let config =
1331 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1332 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1333
1334 let err = sdk
1336 .send_template_message("", "tpl_id", &serde_json::json!({}))
1337 .unwrap_err();
1338 match err {
1339 WechatError::MissingField(field) => assert_eq!(field, "touser"),
1340 other => panic!("期望 MissingField, 实际 {other:?}"),
1341 }
1342
1343 let err = sdk
1345 .send_template_message("user", "", &serde_json::json!({}))
1346 .unwrap_err();
1347 match err {
1348 WechatError::MissingField(field) => assert_eq!(field, "template_id"),
1349 other => panic!("期望 MissingField, 实际 {other:?}"),
1350 }
1351 }
1352
1353 #[test]
1355 fn test_wechat_sdk_send_template_message_token_failed() {
1356 let transport = MemoryWechatHttpTransport::new();
1357 transport.push_response(r#"{"errcode":40013,"errmsg":"invalid appid"}"#);
1359
1360 let config =
1361 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1362 let sdk = WechatSdk::new(config, Arc::new(transport));
1363
1364 let err = sdk
1365 .send_template_message("user", "tpl", &serde_json::json!({}))
1366 .unwrap_err();
1367 assert!(matches!(err, WechatError::TokenFailed(_)));
1368 }
1369
1370 #[test]
1372 fn test_wechat_sdk_send_template_message_api_failed() {
1373 let transport = MemoryWechatHttpTransport::new();
1374 transport.push_response(r#"{"access_token":"token123","expires_in":7200}"#);
1375 transport.push_response(r#"{"errcode":43004,"errmsg":"require subscribe"}"#);
1376
1377 let config =
1378 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1379 let sdk = WechatSdk::new(config, Arc::new(transport));
1380
1381 let err = sdk
1382 .send_template_message("user", "tpl", &serde_json::json!({}))
1383 .unwrap_err();
1384 assert!(matches!(err, WechatError::ApiFailed(_)));
1385 }
1386
1387 #[test]
1393 fn test_wechat_sdk_get_qrcode_url() {
1394 let transport = Arc::new(MemoryWechatHttpTransport::new());
1395 transport.push_response(r#"{"access_token":"token123","expires_in":7200}"#);
1397 transport.push_response(
1398 r#"{"ticket":"ticket_abc_xyz","url":"http://weixin.qq.com/q/abc","expire_seconds":0}"#,
1399 );
1400
1401 let config =
1402 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1403 let sdk = WechatSdk::new(config, transport.clone());
1404
1405 let url = sdk
1406 .get_qrcode_url("scene_123")
1407 .expect("get_qrcode_url 失败");
1408
1409 assert!(url.starts_with("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="));
1411 assert!(url.contains("ticket=ticket_abc_xyz"));
1413
1414 assert_eq!(transport.request_count(), 2);
1416 let requests = transport.requests();
1417
1418 assert_eq!(requests[0].0, "GET");
1420 assert!(requests[0].1.contains("cgi-bin/token"));
1421
1422 assert_eq!(requests[1].0, "POST_JSON");
1424 assert!(requests[1].1.contains("cgi-bin/qrcode/create"));
1425 assert!(requests[1].1.contains("access_token=token123"));
1426 let body: serde_json::Value = serde_json::from_str(&requests[1].2).expect("body 应为 JSON");
1428 assert_eq!(body["action_name"], "QR_LIMIT_STR_SCENE");
1429 assert_eq!(body["action_info"]["scene"]["scene_str"], "scene_123");
1430 }
1431
1432 #[test]
1434 fn test_wechat_sdk_get_qrcode_url_empty_scene() {
1435 let config =
1436 WechatConfig::new(WechatAppType::OfficialAccount, "wx_app_id", "wx_app_secret");
1437 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1438
1439 let err = sdk.get_qrcode_url("").unwrap_err();
1440 match err {
1441 WechatError::MissingField(field) => assert_eq!(field, "scene_str"),
1442 other => panic!("期望 MissingField, 实际 {other:?}"),
1443 }
1444 }
1445
1446 #[test]
1452 fn test_wechat_sdk_missing_config() {
1453 let config = WechatConfig::new(WechatAppType::OfficialAccount, "", "");
1454 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1455
1456 let err = sdk.get_user_by_code("code").unwrap_err();
1458 assert!(matches!(err, WechatError::Config(_)));
1459
1460 let err = sdk.get_user_info("openid", "token").unwrap_err();
1462 assert!(matches!(err, WechatError::Config(_)));
1463
1464 let err = sdk
1466 .send_template_message("user", "tpl", &serde_json::json!({}))
1467 .unwrap_err();
1468 assert!(matches!(err, WechatError::Config(_)));
1469
1470 let err = sdk.get_qrcode_url("scene").unwrap_err();
1472 assert!(matches!(err, WechatError::Config(_)));
1473
1474 let config = WechatConfig::new(WechatAppType::OfficialAccount, "", "secret");
1476 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1477 let err = sdk.get_user_by_code("code").unwrap_err();
1478 match err {
1479 WechatError::Config(field) => assert_eq!(field, "app_id"),
1480 other => panic!("期望 Config, 实际 {other:?}"),
1481 }
1482
1483 let config = WechatConfig::new(WechatAppType::OfficialAccount, "app_id", "");
1485 let sdk = WechatSdk::new(config, Arc::new(MemoryWechatHttpTransport::new()));
1486 let err = sdk.get_user_info("openid", "token").unwrap_err();
1487 match err {
1488 WechatError::Config(field) => assert_eq!(field, "app_secret"),
1489 other => panic!("期望 Config, 实际 {other:?}"),
1490 }
1491
1492 let _url = sdk.get_authorize_url("snsapi_base", "state");
1495 assert!(sdk.verify_signature("sig", "ts", "nonce", "token") || true);
1496 let _sig = sdk.generate_jsapi_signature("url", "ns", 0, "ticket");
1497 }
1498
1499 #[test]
1505 fn test_memory_wechat_http_transport() {
1506 let transport = MemoryWechatHttpTransport::new();
1507
1508 let err = transport.get("https://api.example.com/get").unwrap_err();
1510 match err {
1511 WechatError::HttpTransport(msg) => assert!(msg.contains("无可用预置响应")),
1512 other => panic!("期望 HttpTransport, 实际 {other:?}"),
1513 }
1514 assert_eq!(transport.request_count(), 0);
1515
1516 transport.push_response(r#"{"code":"ok"}"#);
1518 let resp = transport
1519 .get("https://api.example.com/get?foo=bar")
1520 .expect("GET 应返回预置响应");
1521 assert_eq!(resp, r#"{"code":"ok"}"#);
1522 assert_eq!(transport.request_count(), 1);
1523 let requests = transport.requests();
1524 assert_eq!(requests[0].0, "GET");
1525 assert_eq!(requests[0].1, "https://api.example.com/get?foo=bar");
1526 assert_eq!(requests[0].2, ""); transport.push_response(r#"{"errcode":0}"#);
1530 let resp = transport
1531 .post_json("https://api.example.com/post", r#"{"key":"value"}"#)
1532 .expect("POST_JSON 应返回预置响应");
1533 assert_eq!(resp, r#"{"errcode":0}"#);
1534 assert_eq!(transport.request_count(), 2);
1535 let requests = transport.requests();
1536 assert_eq!(requests[1].0, "POST_JSON");
1537 assert_eq!(requests[1].1, "https://api.example.com/post");
1538 assert_eq!(requests[1].2, r#"{"key":"value"}"#);
1539
1540 transport.push_response("form_response");
1542 let resp = transport
1543 .post_form("https://api.example.com/form", "a=1&b=2")
1544 .expect("POST_FORM 应返回预置响应");
1545 assert_eq!(resp, "form_response");
1546 assert_eq!(transport.request_count(), 3);
1547 let requests = transport.requests();
1548 assert_eq!(requests[2].0, "POST_FORM");
1549 assert_eq!(requests[2].1, "https://api.example.com/form");
1550 assert_eq!(requests[2].2, "a=1&b=2");
1551
1552 transport.clear();
1554 transport.push_response("resp1");
1555 transport.push_response("resp2");
1556 transport.push_response("resp3");
1557
1558 let r1 = transport.post_json("url1", "body1").expect("应返回 resp1");
1559 let r2 = transport.get("url2").expect("应返回 resp2");
1560 let r3 = transport.post_form("url3", "body3").expect("应返回 resp3");
1561 assert_eq!(r1, "resp1");
1562 assert_eq!(r2, "resp2");
1563 assert_eq!(r3, "resp3");
1564
1565 assert!(transport.get("url4").is_err());
1567 assert!(transport.post_json("url4", "body4").is_err());
1568 assert!(transport.post_form("url4", "body4").is_err());
1569
1570 assert_eq!(transport.request_count(), 3);
1572 let requests = transport.requests();
1573 assert_eq!(requests[0].0, "POST_JSON");
1574 assert_eq!(requests[0].1, "url1");
1575 assert_eq!(requests[0].2, "body1");
1576 assert_eq!(requests[1].0, "GET");
1577 assert_eq!(requests[1].1, "url2");
1578 assert_eq!(requests[1].2, "");
1579 assert_eq!(requests[2].0, "POST_FORM");
1580 assert_eq!(requests[2].1, "url3");
1581 assert_eq!(requests[2].2, "body3");
1582
1583 transport.clear();
1585 assert_eq!(transport.request_count(), 0);
1586 assert!(transport.get("url").is_err());
1587 }
1588}