1use std::sync::Arc;
41
42use super::subscribe::SubscribeApi;
43use super::{WechatApi, WechatContext};
44use crate::error::WechatError;
45
46pub use super::subscribe::{
48 AddTemplateResponse, CategoryInfo, CategoryListResponse, TemplateInfo, TemplateListResponse,
49};
50
51pub struct TemplateApi {
56 subscribe_api: SubscribeApi,
57}
58
59impl TemplateApi {
60 pub fn new(context: Arc<WechatContext>) -> Self {
62 Self {
63 subscribe_api: SubscribeApi::new(context),
64 }
65 }
66
67 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 pub async fn get_template_list(&self) -> Result<Vec<TemplateInfo>, WechatError> {
85 self.subscribe_api.get_template_list().await
86 }
87
88 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 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}