use std::fmt;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::api::r#trait::{WechatApi, WechatContext};
use crate::error::WechatError;
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginResponse {
#[serde(default)]
pub openid: String,
#[serde(default)]
pub session_key: String,
#[serde(default)]
pub unionid: Option<String>,
#[serde(default)]
pub(crate) errcode: i32,
#[serde(default)]
pub(crate) errmsg: String,
}
impl LoginResponse {
pub fn new(
openid: impl Into<String>,
session_key: impl Into<String>,
unionid: Option<String>,
) -> Self {
Self {
openid: openid.into(),
session_key: session_key.into(),
unionid,
errcode: 0,
errmsg: String::new(),
}
}
pub fn is_success(&self) -> bool {
self.errcode == 0
}
pub fn errcode(&self) -> i32 {
self.errcode
}
pub fn errmsg(&self) -> &str {
&self.errmsg
}
}
#[derive(Clone, Serialize)]
struct StableAccessTokenRequest {
grant_type: String,
appid: String,
secret: String,
force_refresh: bool,
}
impl fmt::Debug for StableAccessTokenRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StableAccessTokenRequest")
.field("grant_type", &self.grant_type)
.field("appid", &self.appid)
.field("secret", &"[REDACTED]")
.field("force_refresh", &self.force_refresh)
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StableAccessTokenResponse {
#[serde(default)]
pub access_token: String,
#[serde(default)]
pub expires_in: i64,
#[serde(default)]
pub(crate) errcode: i32,
#[serde(default)]
pub(crate) errmsg: String,
}
#[derive(Clone, Serialize)]
struct CheckSessionKeyRequest {
openid: String,
signature: String,
sig_method: String,
}
impl fmt::Debug for CheckSessionKeyRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CheckSessionKeyRequest")
.field("openid", &self.openid)
.field("signature", &"[REDACTED]")
.field("sig_method", &self.sig_method)
.finish()
}
}
#[derive(Clone, Serialize)]
struct ResetUserSessionKeyRequest {
openid: String,
signature: String,
sig_method: String,
}
impl fmt::Debug for ResetUserSessionKeyRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResetUserSessionKeyRequest")
.field("openid", &self.openid)
.field("signature", &"[REDACTED]")
.field("sig_method", &self.sig_method)
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResetSessionKeyResponse {
#[serde(default)]
pub openid: String,
#[serde(default)]
pub session_key: String,
#[serde(default)]
pub(crate) errcode: i32,
#[serde(default)]
pub(crate) errmsg: String,
}
#[derive(Debug, Clone, Deserialize)]
struct BaseApiResponse {
#[serde(default)]
errcode: i32,
#[serde(default)]
errmsg: String,
}
pub struct AuthApi {
context: Arc<WechatContext>,
}
impl AuthApi {
pub fn new(context: Arc<WechatContext>) -> Self {
Self { context }
}
pub async fn login(&self, js_code: &str) -> Result<LoginResponse, WechatError> {
let path = "/sns/jscode2session";
let query = [
("appid", self.context.client.appid()),
("secret", self.context.client.secret()),
("js_code", js_code),
("grant_type", "authorization_code"),
];
let response: LoginResponse = self.context.client.get(path, &query).await?;
WechatError::check_api(response.errcode, &response.errmsg)?;
Ok(response)
}
pub async fn get_stable_access_token(
&self,
force_refresh: bool,
) -> Result<StableAccessTokenResponse, WechatError> {
let path = "/cgi-bin/stable_token";
let body = StableAccessTokenRequest {
grant_type: "client_credential".to_string(),
appid: self.context.client.appid().to_string(),
secret: self.context.client.secret().to_string(),
force_refresh,
};
let response: StableAccessTokenResponse = self.context.client.post(path, &body).await?;
WechatError::check_api(response.errcode, &response.errmsg)?;
Ok(response)
}
pub async fn check_session_key(
&self,
openid: &str,
signature: &str,
sig_method: &str,
) -> Result<(), WechatError> {
let body = CheckSessionKeyRequest {
openid: openid.to_string(),
signature: signature.to_string(),
sig_method: sig_method.to_string(),
};
let response: BaseApiResponse =
self.context.authed_post("/wxa/checksession", &body).await?;
WechatError::check_api(response.errcode, &response.errmsg)?;
Ok(())
}
pub async fn reset_user_session_key(
&self,
openid: &str,
signature: &str,
sig_method: &str,
) -> Result<ResetSessionKeyResponse, WechatError> {
let body = ResetUserSessionKeyRequest {
openid: openid.to_string(),
signature: signature.to_string(),
sig_method: sig_method.to_string(),
};
let response: ResetSessionKeyResponse = self
.context
.authed_post("/wxa/resetusersessionkey", &body)
.await?;
WechatError::check_api(response.errcode, &response.errmsg)?;
Ok(response)
}
}
impl WechatApi for AuthApi {
fn context(&self) -> &WechatContext {
&self.context
}
fn api_name(&self) -> &'static str {
"auth"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_login_response_success_parse() {
let json = r#"{
"openid": "oABC123xyz",
"session_key": "test_session_key_abc",
"errcode": 0,
"errmsg": "ok"
}"#;
let response: LoginResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.openid, "oABC123xyz");
assert_eq!(response.session_key, "test_session_key_abc");
assert!(response.is_success());
assert!(response.unionid.is_none());
}
#[test]
fn test_login_response_with_unionid() {
let json = r#"{
"openid": "oABC123xyz",
"session_key": "test_session_key_abc",
"unionid": "uABC123union",
"errcode": 0,
"errmsg": "ok"
}"#;
let response: LoginResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.openid, "oABC123xyz");
assert_eq!(response.session_key, "test_session_key_abc");
assert_eq!(response.unionid, Some("uABC123union".to_string()));
assert!(response.is_success());
}
#[test]
fn test_login_response_error_parse() {
let json = r#"{
"errcode": 40013,
"errmsg": "invalid appid"
}"#;
let response: LoginResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.errcode, 40013);
assert_eq!(response.errmsg, "invalid appid");
assert!(!response.is_success());
assert!(response.openid.is_empty());
assert!(response.session_key.is_empty());
}
#[test]
fn test_is_success_true_for_zero() {
let response = LoginResponse {
openid: "test".to_string(),
session_key: "test".to_string(),
unionid: None,
errcode: 0,
errmsg: "ok".to_string(),
};
assert!(response.is_success());
}
#[test]
fn test_is_success_false_for_nonzero() {
let response = LoginResponse {
openid: "test".to_string(),
session_key: "test".to_string(),
unionid: None,
errcode: -1,
errmsg: "system error".to_string(),
};
assert!(!response.is_success());
}
#[test]
fn test_stable_access_token_response_parse() {
let json = r#"{
"access_token": "stable_token_abc123",
"expires_in": 7200,
"errcode": 0,
"errmsg": "ok"
}"#;
let response: StableAccessTokenResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.access_token, "stable_token_abc123");
assert_eq!(response.expires_in, 7200);
assert_eq!(response.errcode, 0);
}
#[test]
fn test_stable_access_token_response_defaults() {
let json = r#"{"errcode": 0, "errmsg": "ok"}"#;
let response: StableAccessTokenResponse = serde_json::from_str(json).unwrap();
assert!(response.access_token.is_empty());
assert_eq!(response.expires_in, 0);
}
#[test]
fn test_reset_session_key_response_parse() {
let json = r#"{
"openid": "oABC123xyz",
"session_key": "new_session_key_abc",
"errcode": 0,
"errmsg": "ok"
}"#;
let response: ResetSessionKeyResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.openid, "oABC123xyz");
assert_eq!(response.session_key, "new_session_key_abc");
assert_eq!(response.errcode, 0);
}
#[test]
fn test_reset_session_key_response_defaults() {
let json = r#"{"errcode": 0, "errmsg": "ok"}"#;
let response: ResetSessionKeyResponse = serde_json::from_str(json).unwrap();
assert!(response.openid.is_empty());
assert!(response.session_key.is_empty());
}
#[test]
fn test_stable_access_token_request_debug_redacts_secret() {
let request = StableAccessTokenRequest {
grant_type: "client_credential".to_string(),
appid: "wx1234567890abcdef".to_string(),
secret: "top-secret-value".to_string(),
force_refresh: false,
};
let output = format!("{:?}", request);
assert!(output.contains("secret"));
assert!(output.contains("[REDACTED]"));
assert!(!output.contains("top-secret-value"));
}
#[test]
fn test_check_session_key_request_debug_redacts_signature() {
let request = CheckSessionKeyRequest {
openid: "o123".to_string(),
signature: "sensitive-signature".to_string(),
sig_method: "hmac_sha256".to_string(),
};
let output = format!("{:?}", request);
assert!(output.contains("signature"));
assert!(output.contains("[REDACTED]"));
assert!(!output.contains("sensitive-signature"));
}
#[test]
fn test_reset_user_session_key_request_debug_redacts_signature() {
let request = ResetUserSessionKeyRequest {
openid: "o123".to_string(),
signature: "another-sensitive-signature".to_string(),
sig_method: "hmac_sha256".to_string(),
};
let output = format!("{:?}", request);
assert!(output.contains("signature"));
assert!(output.contains("[REDACTED]"));
assert!(!output.contains("another-sensitive-signature"));
}
}