1use crate::client::HTTPClient;
2use crate::errors::UbiClientError;
3use crate::{make_json_request, make_request};
4use serde::Deserialize;
5use serde::Serialize;
6use std::sync::Arc;
7
8#[derive(Debug, Serialize, Deserialize, Default)]
9pub struct ProjectList {
10 pub count: u32,
11 pub items: Vec<UserProject>,
12}
13
14#[derive(Debug, Serialize, Deserialize, Default)]
15pub struct UserProject {
16 pub credit: f64,
17 pub discount: u32,
18 pub id: String,
19 pub name: String,
20}
21
22pub struct Project {
23 http_client: Arc<HTTPClient>,
24}
25impl Project {
26 pub fn new(http_client: Arc<HTTPClient>) -> Self {
27 Project { http_client }
28 }
29
30 pub async fn list_projects(&self) -> Result<ProjectList, UbiClientError> {
34 let url = "project";
35 let resp = make_request!(self, reqwest::Method::GET, url)?;
36 Ok(resp.json().await?)
37 }
38
39 pub async fn create_project(&self, name: &str) -> Result<UserProject, UbiClientError> {
44 let url = "project";
45 let body = serde_json::json!({
46 "name": name,
47 });
48 let query = [(); 0];
49 make_json_request!(self, reqwest::Method::POST, url, body, query, UserProject)
50 }
51
52 pub async fn delete_project(&self, project_id: &str) -> Result<(), UbiClientError> {
57 let url = format!("project/{}", project_id);
58 let _resp = make_request!(self, reqwest::Method::DELETE, &url)?;
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::client::HTTPClient;
67 use mockito::{Matcher, Server};
68 use std::sync::Arc;
69
70 fn create_test_client(server_url: &str) -> Project {
71 let reqwest_client = reqwest::Client::new();
72 let http_client = HTTPClient::new(server_url, reqwest_client, "v1");
73 Project::new(Arc::new(http_client))
74 }
75
76 #[tokio::test]
77 async fn test_list_projects_success() {
78 let mut server = Server::new_async().await;
79 let mock_response = ProjectList {
80 count: 2,
81 items: vec![
82 UserProject {
83 credit: 100.50,
84 discount: 10,
85 id: "project-1".to_string(),
86 name: "Test Project 1".to_string(),
87 },
88 UserProject {
89 credit: 250.75,
90 discount: 15,
91 id: "project-2".to_string(),
92 name: "Test Project 2".to_string(),
93 },
94 ],
95 };
96
97 let mock = server
98 .mock("GET", "/v1/project")
99 .with_status(200)
100 .with_header("content-type", "application/json")
101 .with_body(serde_json::to_string(&mock_response).unwrap())
102 .create_async()
103 .await;
104
105 let client = create_test_client(&server.url());
106 let result = client.list_projects().await.unwrap();
107
108 mock.assert_async().await;
109 assert_eq!(result.count, 2);
110 assert_eq!(result.items.len(), 2);
111 assert_eq!(result.items[0].name, "Test Project 1");
112 assert_eq!(result.items[0].id, "project-1");
113 assert_eq!(result.items[0].credit, 100.50);
114 assert_eq!(result.items[0].discount, 10);
115 assert_eq!(result.items[1].name, "Test Project 2");
116 assert_eq!(result.items[1].id, "project-2");
117 assert_eq!(result.items[1].credit, 250.75);
118 assert_eq!(result.items[1].discount, 15);
119 }
120
121 #[tokio::test]
122 async fn test_list_projects_empty() {
123 let mut server = Server::new_async().await;
124 let mock_response = ProjectList {
125 count: 0,
126 items: vec![],
127 };
128
129 let mock = server
130 .mock("GET", "/v1/project")
131 .with_status(200)
132 .with_header("content-type", "application/json")
133 .with_body(serde_json::to_string(&mock_response).unwrap())
134 .create_async()
135 .await;
136
137 let client = create_test_client(&server.url());
138 let result = client.list_projects().await.unwrap();
139
140 mock.assert_async().await;
141 assert_eq!(result.count, 0);
142 assert_eq!(result.items.len(), 0);
143 }
144
145 #[tokio::test]
146 async fn test_create_project_success() {
147 let mut server = Server::new_async().await;
148 let mock_response = UserProject {
149 credit: 0.0,
150 discount: 0,
151 id: "new-project-123".to_string(),
152 name: "My New Project".to_string(),
153 };
154
155 let mock = server
156 .mock("POST", "/v1/project")
157 .match_body(Matcher::Json(serde_json::json!({
158 "name": "My New Project"
159 })))
160 .with_status(201)
161 .with_header("content-type", "application/json")
162 .with_body(serde_json::to_string(&mock_response).unwrap())
163 .create_async()
164 .await;
165
166 let client = create_test_client(&server.url());
167 let result = client.create_project("My New Project").await.unwrap();
168
169 mock.assert_async().await;
170 assert_eq!(result.name, "My New Project");
171 assert_eq!(result.id, "new-project-123");
172 assert_eq!(result.credit, 0.0);
173 assert_eq!(result.discount, 0);
174 }
175
176 #[tokio::test]
177 async fn test_create_project_with_special_characters() {
178 let mut server = Server::new_async().await;
179 let project_name = "Project with @special #characters & symbols!";
180 let mock_response = UserProject {
181 credit: 50.0,
182 discount: 5,
183 id: "special-project-456".to_string(),
184 name: project_name.to_string(),
185 };
186
187 let mock = server
188 .mock("POST", "/v1/project")
189 .match_body(Matcher::Json(serde_json::json!({
190 "name": project_name
191 })))
192 .with_status(201)
193 .with_header("content-type", "application/json")
194 .with_body(serde_json::to_string(&mock_response).unwrap())
195 .create_async()
196 .await;
197
198 let client = create_test_client(&server.url());
199 let result = client.create_project(project_name).await.unwrap();
200
201 mock.assert_async().await;
202 assert_eq!(result.name, project_name);
203 assert_eq!(result.id, "special-project-456");
204 }
205
206 #[tokio::test]
207 async fn test_delete_project_success() {
208 let mut server = Server::new_async().await;
209
210 let mock = server
211 .mock("DELETE", "/v1/project/test-project-123")
212 .with_status(204)
213 .create_async()
214 .await;
215
216 let client = create_test_client(&server.url());
217 let result = client.delete_project("test-project-123").await;
218
219 mock.assert_async().await;
220 assert!(result.is_ok());
221 }
222
223 #[tokio::test]
224 async fn test_delete_project_with_special_id() {
225 let mut server = Server::new_async().await;
226 let project_id = "project-with-dashes-and-numbers-123";
227
228 let mock = server
229 .mock("DELETE", format!("/v1/project/{}", project_id).as_str())
230 .with_status(204)
231 .create_async()
232 .await;
233
234 let client = create_test_client(&server.url());
235 let result = client.delete_project(project_id).await;
236
237 mock.assert_async().await;
238 assert!(result.is_ok());
239 }
240
241 #[tokio::test]
242 async fn test_list_projects_api_error() {
243 let mut server = Server::new_async().await;
244 let error_response = serde_json::json!({
245 "error": {
246 "type": "Unauthorized",
247 "message": "Invalid authentication token",
248 "details": "The provided token is expired or invalid"
249 }
250 });
251
252 let mock = server
253 .mock("GET", "/v1/project")
254 .with_status(401)
255 .with_header("content-type", "application/json")
256 .with_body(error_response.to_string())
257 .create_async()
258 .await;
259
260 let client = create_test_client(&server.url());
261 let result = client.list_projects().await;
262
263 mock.assert_async().await;
264 assert!(result.is_err());
265
266 if let Err(UbiClientError::APIResponseError {
267 etype,
268 message,
269 details,
270 }) = result
271 {
272 assert_eq!(etype, "Unauthorized");
273 assert_eq!(message, "Invalid authentication token");
274 assert_eq!(
275 details,
276 Some("The provided token is expired or invalid".to_string())
277 );
278 } else {
279 panic!("Expected APIResponseError");
280 }
281 }
282
283 #[tokio::test]
284 async fn test_create_project_validation_error() {
285 let mut server = Server::new_async().await;
286 let error_response = serde_json::json!({
287 "error": {
288 "type": "ValidationError",
289 "message": "Project name is required",
290 "details": "The project name cannot be empty"
291 }
292 });
293
294 let mock = server
295 .mock("POST", "/v1/project")
296 .match_body(Matcher::Json(serde_json::json!({
297 "name": ""
298 })))
299 .with_status(400)
300 .with_header("content-type", "application/json")
301 .with_body(error_response.to_string())
302 .create_async()
303 .await;
304
305 let client = create_test_client(&server.url());
306 let result = client.create_project("").await;
307
308 mock.assert_async().await;
309 assert!(result.is_err());
310
311 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
312 assert_eq!(etype, "ValidationError");
313 assert_eq!(message, "Project name is required");
314 } else {
315 panic!("Expected APIResponseError");
316 }
317 }
318
319 #[tokio::test]
320 async fn test_create_project_conflict_error() {
321 let mut server = Server::new_async().await;
322 let error_response = serde_json::json!({
323 "error": {
324 "type": "Conflict",
325 "message": "Project with this name already exists",
326 "details": null
327 }
328 });
329
330 let mock = server
331 .mock("POST", "/v1/project")
332 .match_body(Matcher::Json(serde_json::json!({
333 "name": "Existing Project"
334 })))
335 .with_status(409)
336 .with_header("content-type", "application/json")
337 .with_body(error_response.to_string())
338 .create_async()
339 .await;
340
341 let client = create_test_client(&server.url());
342 let result = client.create_project("Existing Project").await;
343
344 mock.assert_async().await;
345 assert!(result.is_err());
346
347 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
348 assert_eq!(etype, "Conflict");
349 assert_eq!(message, "Project with this name already exists");
350 } else {
351 panic!("Expected APIResponseError");
352 }
353 }
354
355 #[tokio::test]
356 async fn test_delete_project_not_found() {
357 let mut server = Server::new_async().await;
358 let error_response = serde_json::json!({
359 "error": {
360 "type": "NotFound",
361 "message": "Project not found",
362 "details": "The specified project does not exist or you don't have permission to delete it"
363 }
364 });
365
366 let mock = server
367 .mock("DELETE", "/v1/project/nonexistent-project")
368 .with_status(404)
369 .with_header("content-type", "application/json")
370 .with_body(error_response.to_string())
371 .create_async()
372 .await;
373
374 let client = create_test_client(&server.url());
375 let result = client.delete_project("nonexistent-project").await;
376
377 mock.assert_async().await;
378 assert!(result.is_err());
379
380 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
381 assert_eq!(etype, "NotFound");
382 assert_eq!(message, "Project not found");
383 } else {
384 panic!("Expected APIResponseError");
385 }
386 }
387
388 #[tokio::test]
389 async fn test_delete_project_forbidden() {
390 let mut server = Server::new_async().await;
391 let error_response = serde_json::json!({
392 "error": {
393 "type": "Forbidden",
394 "message": "Insufficient permissions to delete project",
395 "details": "Only project owners can delete projects"
396 }
397 });
398
399 let mock = server
400 .mock("DELETE", "/v1/project/restricted-project")
401 .with_status(403)
402 .with_header("content-type", "application/json")
403 .with_body(error_response.to_string())
404 .create_async()
405 .await;
406
407 let client = create_test_client(&server.url());
408 let result = client.delete_project("restricted-project").await;
409
410 mock.assert_async().await;
411 assert!(result.is_err());
412
413 if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
414 assert_eq!(etype, "Forbidden");
415 assert_eq!(message, "Insufficient permissions to delete project");
416 } else {
417 panic!("Expected APIResponseError");
418 }
419 }
420
421 #[test]
422 fn test_project_list_default() {
423 let project_list = ProjectList::default();
424 assert_eq!(project_list.count, 0);
425 assert_eq!(project_list.items.len(), 0);
426 }
427
428 #[test]
429 fn test_user_project_default() {
430 let user_project = UserProject::default();
431 assert_eq!(user_project.credit, 0.0);
432 assert_eq!(user_project.discount, 0);
433 assert_eq!(user_project.id, "");
434 assert_eq!(user_project.name, "");
435 }
436
437 #[test]
438 fn test_user_project_serialization() {
439 let project = UserProject {
440 credit: 123.45,
441 discount: 20,
442 id: "test-project-789".to_string(),
443 name: "Serialization Test Project".to_string(),
444 };
445
446 let serialized = serde_json::to_string(&project).unwrap();
447 let deserialized: UserProject = serde_json::from_str(&serialized).unwrap();
448
449 assert_eq!(project.credit, deserialized.credit);
450 assert_eq!(project.discount, deserialized.discount);
451 assert_eq!(project.id, deserialized.id);
452 assert_eq!(project.name, deserialized.name);
453 }
454
455 #[test]
456 fn test_project_list_serialization() {
457 let project_list = ProjectList {
458 count: 1,
459 items: vec![UserProject {
460 credit: 99.99,
461 discount: 5,
462 id: "serialize-test".to_string(),
463 name: "Serialize Test".to_string(),
464 }],
465 };
466
467 let serialized = serde_json::to_string(&project_list).unwrap();
468 let deserialized: ProjectList = serde_json::from_str(&serialized).unwrap();
469
470 assert_eq!(project_list.count, deserialized.count);
471 assert_eq!(project_list.items.len(), deserialized.items.len());
472 assert_eq!(project_list.items[0].name, deserialized.items[0].name);
473 }
474
475 #[test]
476 fn test_project_new() {
477 let reqwest_client = reqwest::Client::new();
478 let http_client = HTTPClient::new("https://api.example.com", reqwest_client, "v1");
479 let project_client = Project::new(Arc::new(http_client));
480
481 assert!(std::ptr::addr_of!(project_client) as usize != 0);
484 }
485}