Skip to main content

wechat_mp_sdk/api/
template.rs

1//! Template Message Management API
2//!
3//! Provides APIs for managing WeChat Mini Program template messages.
4//! Delegates to [`super::subscribe::SubscribeApi`] for the underlying implementation.
5//!
6//! # Features
7//!
8//! - Add templates from template library
9//! - Get template list
10//! - Delete templates
11//! - Get category list
12//!
13//! # Example
14//!
15//! ```no_run
16//! # use std::sync::Arc;
17//! # use wechat_mp_sdk::api::template::TemplateApi;
18//! # use wechat_mp_sdk::api::WechatContext;
19//! # use wechat_mp_sdk::client::WechatClient;
20//! # use wechat_mp_sdk::token::TokenManager;
21//! # use wechat_mp_sdk::types::{AppId, AppSecret};
22//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! # let client = Arc::new(WechatClient::builder()
24//! #     .appid(AppId::new("wx1234567890abcdef")?)
25//! #     .secret(AppSecret::new("test_secret")?)
26//! #     .build()?);
27//! # let token_manager = Arc::new(TokenManager::new((*client).clone()));
28//! # let context = Arc::new(WechatContext::new(client, token_manager));
29//! let template_api = TemplateApi::new(context);
30//!
31//! // Get template list
32//! let templates = template_api.get_template_list().await?;
33//! for tmpl in templates {
34//!     println!("Template: {} - {}", tmpl.private_template_id, tmpl.title);
35//! }
36//! # Ok(())
37//! # }
38//! ```
39
40use std::sync::Arc;
41
42use super::subscribe::SubscribeApi;
43use super::{WechatApi, WechatContext};
44use crate::error::WechatError;
45
46// Re-export shared types from subscribe module for backward compatibility
47pub use super::subscribe::{
48    AddTemplateResponse, CategoryInfo, CategoryListResponse, TemplateInfo, TemplateListResponse,
49};
50
51/// Template Message Management API
52///
53/// Thin wrapper around [`SubscribeApi`] for template management operations.
54/// Subscribe messages and template management share the same underlying WeChat APIs.
55pub struct TemplateApi {
56    subscribe_api: SubscribeApi,
57}
58
59impl TemplateApi {
60    /// Create a new TemplateApi instance
61    pub fn new(context: Arc<WechatContext>) -> Self {
62        Self {
63            subscribe_api: SubscribeApi::new(context),
64        }
65    }
66
67    /// Add template from template library
68    ///
69    /// Delegates to [`SubscribeApi::add_template`].
70    pub async fn add_template(
71        &self,
72        tid: &str,
73        kid_list: Option<Vec<i32>>,
74        scene_desc: Option<&str>,
75    ) -> Result<String, WechatError> {
76        self.subscribe_api
77            .add_template(tid, kid_list, scene_desc)
78            .await
79    }
80
81    /// Get template list
82    ///
83    /// Delegates to [`SubscribeApi::get_template_list`].
84    pub async fn get_template_list(&self) -> Result<Vec<TemplateInfo>, WechatError> {
85        self.subscribe_api.get_template_list().await
86    }
87
88    /// Delete template
89    ///
90    /// Delegates to [`SubscribeApi::delete_template`].
91    pub async fn delete_template(&self, pri_tmpl_id: &str) -> Result<(), WechatError> {
92        self.subscribe_api.delete_template(pri_tmpl_id).await
93    }
94
95    /// Get category list
96    ///
97    /// Delegates to [`SubscribeApi::get_category`].
98    pub async fn get_category(&self) -> Result<Vec<CategoryInfo>, WechatError> {
99        self.subscribe_api.get_category().await
100    }
101}
102
103impl WechatApi for TemplateApi {
104    fn context(&self) -> &WechatContext {
105        self.subscribe_api.context()
106    }
107
108    fn api_name(&self) -> &'static str {
109        "template"
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::client::WechatClient;
117    use crate::token::TokenManager;
118    use crate::types::{AppId, AppSecret};
119
120    fn create_test_context(base_url: &str) -> Arc<WechatContext> {
121        let appid = AppId::new("wx1234567890abcdef").unwrap();
122        let secret = AppSecret::new("secret1234567890ab").unwrap();
123        let client = Arc::new(
124            WechatClient::builder()
125                .appid(appid)
126                .secret(secret)
127                .base_url(base_url)
128                .build()
129                .unwrap(),
130        );
131        let token_manager = Arc::new(TokenManager::new((*client).clone()));
132        Arc::new(WechatContext::new(client, token_manager))
133    }
134
135    #[test]
136    fn test_template_info_deserialize() {
137        let json = serde_json::json!({
138            "priTmplId": "test_template_id",
139            "title": "Test Template",
140            "content": "Content here",
141            "example": "Example content",
142            "type": 2
143        });
144
145        let info: TemplateInfo = serde_json::from_value(json).unwrap();
146        assert_eq!(info.private_template_id, "test_template_id");
147        assert_eq!(info.title, "Test Template");
148        assert_eq!(info.content, "Content here");
149        assert_eq!(info.example, Some("Example content".to_string()));
150        assert_eq!(info.template_type, 2);
151    }
152
153    #[test]
154    fn test_template_info_without_example() {
155        let json = serde_json::json!({
156            "priTmplId": "test_template_id",
157            "title": "Test Template",
158            "content": "Content here",
159            "type": 2
160        });
161
162        let info: TemplateInfo = serde_json::from_value(json).unwrap();
163        assert_eq!(info.example, None);
164    }
165
166    #[test]
167    fn test_category_info_deserialize() {
168        let json = serde_json::json!({
169            "id": 123,
170            "name": "Category Name"
171        });
172
173        let info: CategoryInfo = serde_json::from_value(json).unwrap();
174        assert_eq!(info.id, 123);
175        assert_eq!(info.name, "Category Name");
176    }
177
178    #[tokio::test]
179    async fn test_get_template_list_success() {
180        use wiremock::matchers::{method, path, query_param};
181        use wiremock::{Mock, MockServer, ResponseTemplate};
182
183        let mock_server = MockServer::start().await;
184
185        Mock::given(method("GET"))
186            .and(path("/wxaapi/newtmpl/gettemplate"))
187            .and(query_param("access_token", "test_token"))
188            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
189                "data": [
190                    {
191                        "priTmplId": "template_id_1",
192                        "title": "Purchase Notification",
193                        "content": "Purchase: {{thing1.DATA}}",
194                        "type": 2
195                    }
196                ],
197                "errcode": 0,
198                "errmsg": "ok"
199            })))
200            .mount(&mock_server)
201            .await;
202
203        Mock::given(method("GET"))
204            .and(path("/cgi-bin/token"))
205            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
206                "access_token": "test_token",
207                "expires_in": 7200,
208                "errcode": 0,
209                "errmsg": ""
210            })))
211            .mount(&mock_server)
212            .await;
213
214        let context = create_test_context(&mock_server.uri());
215        let template_api = TemplateApi::new(context);
216
217        let result = template_api.get_template_list().await;
218
219        assert!(result.is_ok());
220        let templates = result.unwrap();
221        assert_eq!(templates.len(), 1);
222        assert_eq!(templates[0].private_template_id, "template_id_1");
223    }
224
225    #[tokio::test]
226    async fn test_add_template_success() {
227        use wiremock::matchers::{method, path, query_param};
228        use wiremock::{Mock, MockServer, ResponseTemplate};
229
230        let mock_server = MockServer::start().await;
231
232        Mock::given(method("POST"))
233            .and(path("/wxaapi/newtmpl/addtemplate"))
234            .and(query_param("access_token", "test_token"))
235            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
236                "priTmplId": "new_private_template_id",
237                "errcode": 0,
238                "errmsg": "ok"
239            })))
240            .mount(&mock_server)
241            .await;
242
243        Mock::given(method("GET"))
244            .and(path("/cgi-bin/token"))
245            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
246                "access_token": "test_token",
247                "expires_in": 7200,
248                "errcode": 0,
249                "errmsg": ""
250            })))
251            .mount(&mock_server)
252            .await;
253
254        let context = create_test_context(&mock_server.uri());
255        let template_api = TemplateApi::new(context);
256
257        let result = template_api
258            .add_template("AA1234", Some(vec![1, 2, 3]), Some("test scene"))
259            .await;
260
261        assert!(result.is_ok());
262        assert_eq!(result.unwrap(), "new_private_template_id");
263    }
264
265    #[tokio::test]
266    async fn test_delete_template_success() {
267        use wiremock::matchers::{method, path, query_param};
268        use wiremock::{Mock, MockServer, ResponseTemplate};
269
270        let mock_server = MockServer::start().await;
271
272        Mock::given(method("POST"))
273            .and(path("/wxaapi/newtmpl/deltemplate"))
274            .and(query_param("access_token", "test_token"))
275            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
276                "errcode": 0,
277                "errmsg": "ok"
278            })))
279            .mount(&mock_server)
280            .await;
281
282        Mock::given(method("GET"))
283            .and(path("/cgi-bin/token"))
284            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
285                "access_token": "test_token",
286                "expires_in": 7200,
287                "errcode": 0,
288                "errmsg": ""
289            })))
290            .mount(&mock_server)
291            .await;
292
293        let context = create_test_context(&mock_server.uri());
294        let template_api = TemplateApi::new(context);
295
296        let result = template_api.delete_template("template_to_delete").await;
297
298        assert!(result.is_ok());
299    }
300
301    #[tokio::test]
302    async fn test_get_category_success() {
303        use wiremock::matchers::{method, path, query_param};
304        use wiremock::{Mock, MockServer, ResponseTemplate};
305
306        let mock_server = MockServer::start().await;
307
308        Mock::given(method("GET"))
309            .and(path("/wxaapi/newtmpl/getcategory"))
310            .and(query_param("access_token", "test_token"))
311            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
312                "data": [
313                    {"id": 1, "name": "IT Technology"},
314                    {"id": 2, "name": "E-commerce"}
315                ],
316                "errcode": 0,
317                "errmsg": "ok"
318            })))
319            .mount(&mock_server)
320            .await;
321
322        Mock::given(method("GET"))
323            .and(path("/cgi-bin/token"))
324            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
325                "access_token": "test_token",
326                "expires_in": 7200,
327                "errcode": 0,
328                "errmsg": ""
329            })))
330            .mount(&mock_server)
331            .await;
332
333        let context = create_test_context(&mock_server.uri());
334        let template_api = TemplateApi::new(context);
335
336        let result = template_api.get_category().await;
337
338        assert!(result.is_ok());
339        let categories = result.unwrap();
340        assert_eq!(categories.len(), 2);
341        assert_eq!(categories[0].name, "IT Technology");
342        assert_eq!(categories[1].name, "E-commerce");
343    }
344}