use std::sync::Arc;
use std::time::Duration;
use wecomx_auth::TokenProvider;
use wecomx_transport::{HttpTransportBackend, Transport};
use crate::backend::WecomBackend;
use crate::client::WecomClient;
pub const DEFAULT_BASE_URL: &str = "https://qyapi.weixin.qq.com/cli";
pub const DEFAULT_BIN_NAME: &str = "wecom-cli";
#[derive(Clone)]
pub struct WecomClientBuilder {
base_url: String,
provider: Option<Arc<dyn TokenProvider>>,
initial_token: Option<String>,
timeout: Option<Duration>,
headers: Vec<(String, String)>,
bin_name: String,
}
impl Default for WecomClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl WecomClientBuilder {
pub fn new() -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_string(),
provider: None,
initial_token: None,
timeout: None,
headers: Vec::new(),
bin_name: DEFAULT_BIN_NAME.to_string(),
}
}
#[must_use]
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
#[must_use]
pub fn token_provider(mut self, provider: Arc<dyn TokenProvider>) -> Self {
self.provider = Some(provider);
self
}
#[must_use]
pub fn initial_token(mut self, token: impl Into<String>) -> Self {
self.initial_token = Some(token.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn bin_name(mut self, bin_name: impl Into<String>) -> Self {
self.bin_name = bin_name.into();
self
}
pub async fn build(self) -> Result<WecomClient, wecomx::Error> {
let transport = self.build_transport().await.map_err(wecomx::Error::from)?;
WecomClient::from_transport(transport)
}
pub async fn build_transport(self) -> Result<Transport, wecomx_transport::Error> {
let mut builder = HttpTransportBackend::builder().base_url(self.base_url);
for (name, value) in &self.headers {
builder = builder.header(name.as_str(), value.as_str());
}
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
let transport = builder.build()?;
let initial_token = match self.initial_token {
Some(token) => Some(token),
None => match &self.provider {
Some(provider) => provider
.access_token()
.await
.map_err(wecomx_transport::Error::from)?
.map(|t| t.into_inner()),
None => None,
},
};
Ok(transport.wrap_backend(move |backend| {
Arc::new(
WecomBackend::new(backend, self.provider, initial_token)
.with_bin_name(self.bin_name),
)
}))
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::sync::Arc;
use serde_json::json;
use wecomx_auth::{BotCredential, BotGatewayTokenProvider, Credentials, MemoryCredentialStore};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
pub(crate) fn auth_ep(url: &str) -> wecomx_transport::Endpoint {
wecomx_auth::auth_endpoint(url)
}
#[tokio::test]
async fn build_reads_initial_token_from_provider() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api"))
.and(wiremock::matchers::header(
"authorization",
"Bearer tok-stored",
))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({"result": "{\"ok\":true}"})),
)
.expect(1)
.mount(&server)
.await;
let store = MemoryCredentialStore::new(Credentials {
bot: Some(BotCredential::new("bot1".into(), "secret1".into())),
token: Some("tok-stored".into()),
})
.shared();
let provider = BotGatewayTokenProvider::from_store(store)
.with_auth_endpoint(auth_ep(&format!("{}/bootstrap", server.uri())));
let client = WecomClientBuilder::new()
.base_url(server.uri())
.token_provider(Arc::new(provider))
.timeout(std::time::Duration::from_secs(5))
.build()
.await
.unwrap();
let endpoint = wecomx_transport::Endpoint::new()
.with(wecomx_transport::HttpEndpoint::new("/api").with_service(server.uri()));
let v = client
.transport()
.invoke(&endpoint, json!({}))
.await
.unwrap()
.into_result()
.unwrap();
assert_eq!(v, json!({"ok": true}));
server.verify().await;
}
#[tokio::test]
async fn initial_token_overrides_provider() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api"))
.and(wiremock::matchers::header(
"authorization",
"Bearer tok-override",
))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({"result": "{\"ok\":true}"})),
)
.expect(1)
.mount(&server)
.await;
let store = MemoryCredentialStore::new(Credentials {
bot: Some(BotCredential::new("bot1".into(), "secret1".into())),
token: Some("tok-stored".into()),
})
.shared();
let provider = BotGatewayTokenProvider::from_store(store);
let transport = WecomClientBuilder::new()
.base_url(server.uri())
.token_provider(Arc::new(provider))
.initial_token("tok-override")
.build_transport()
.await
.unwrap();
let endpoint = wecomx_transport::Endpoint::new()
.with(wecomx_transport::HttpEndpoint::new("/api").with_service(server.uri()));
let v = transport
.invoke(&endpoint, json!({}))
.await
.unwrap()
.into_result()
.unwrap();
assert_eq!(v, json!({"ok": true}));
server.verify().await;
}
#[tokio::test]
async fn build_without_provider() {
let client = WecomClientBuilder::new()
.base_url("http://localhost")
.build()
.await
.unwrap();
assert_eq!(client.transport().name(), "http");
}
#[tokio::test]
async fn build_attaches_gateway_catalog() {
let client = WecomClientBuilder::new()
.base_url("http://localhost")
.build()
.await
.unwrap();
let _endpoint = client.inner().endpoint("media/upload");
}
}