use crate::client::HTTPClient;
use crate::errors::UbiClientError;
use crate::{make_json_request, make_request};
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProjectList {
pub count: u32,
pub items: Vec<UserProject>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct UserProject {
pub credit: f64,
pub discount: u32,
pub id: String,
pub name: String,
}
pub struct Project {
http_client: Arc<HTTPClient>,
}
impl Project {
pub fn new(http_client: Arc<HTTPClient>) -> Self {
Project { http_client }
}
pub async fn list_projects(&self) -> Result<ProjectList, UbiClientError> {
let url = "project";
let resp = make_request!(self, reqwest::Method::GET, url)?;
Ok(resp.json().await?)
}
pub async fn create_project(&self, name: &str) -> Result<UserProject, UbiClientError> {
let url = "project";
let body = serde_json::json!({
"name": name,
});
let query = [(); 0];
make_json_request!(self, reqwest::Method::POST, url, body, query, UserProject)
}
pub async fn delete_project(&self, project_id: &str) -> Result<(), UbiClientError> {
let url = format!("project/{}", project_id);
let _resp = make_request!(self, reqwest::Method::DELETE, &url)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::HTTPClient;
use mockito::{Matcher, Server};
use std::sync::Arc;
fn create_test_client(server_url: &str) -> Project {
let reqwest_client = reqwest::Client::new();
let http_client = HTTPClient::new(server_url, reqwest_client, "v1");
Project::new(Arc::new(http_client))
}
#[tokio::test]
async fn test_list_projects_success() {
let mut server = Server::new_async().await;
let mock_response = ProjectList {
count: 2,
items: vec![
UserProject {
credit: 100.50,
discount: 10,
id: "project-1".to_string(),
name: "Test Project 1".to_string(),
},
UserProject {
credit: 250.75,
discount: 15,
id: "project-2".to_string(),
name: "Test Project 2".to_string(),
},
],
};
let mock = server
.mock("GET", "/v1/project")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.list_projects().await.unwrap();
mock.assert_async().await;
assert_eq!(result.count, 2);
assert_eq!(result.items.len(), 2);
assert_eq!(result.items[0].name, "Test Project 1");
assert_eq!(result.items[0].id, "project-1");
assert_eq!(result.items[0].credit, 100.50);
assert_eq!(result.items[0].discount, 10);
assert_eq!(result.items[1].name, "Test Project 2");
assert_eq!(result.items[1].id, "project-2");
assert_eq!(result.items[1].credit, 250.75);
assert_eq!(result.items[1].discount, 15);
}
#[tokio::test]
async fn test_list_projects_empty() {
let mut server = Server::new_async().await;
let mock_response = ProjectList {
count: 0,
items: vec![],
};
let mock = server
.mock("GET", "/v1/project")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.list_projects().await.unwrap();
mock.assert_async().await;
assert_eq!(result.count, 0);
assert_eq!(result.items.len(), 0);
}
#[tokio::test]
async fn test_create_project_success() {
let mut server = Server::new_async().await;
let mock_response = UserProject {
credit: 0.0,
discount: 0,
id: "new-project-123".to_string(),
name: "My New Project".to_string(),
};
let mock = server
.mock("POST", "/v1/project")
.match_body(Matcher::Json(serde_json::json!({
"name": "My New Project"
})))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.create_project("My New Project").await.unwrap();
mock.assert_async().await;
assert_eq!(result.name, "My New Project");
assert_eq!(result.id, "new-project-123");
assert_eq!(result.credit, 0.0);
assert_eq!(result.discount, 0);
}
#[tokio::test]
async fn test_create_project_with_special_characters() {
let mut server = Server::new_async().await;
let project_name = "Project with @special #characters & symbols!";
let mock_response = UserProject {
credit: 50.0,
discount: 5,
id: "special-project-456".to_string(),
name: project_name.to_string(),
};
let mock = server
.mock("POST", "/v1/project")
.match_body(Matcher::Json(serde_json::json!({
"name": project_name
})))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&mock_response).unwrap())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.create_project(project_name).await.unwrap();
mock.assert_async().await;
assert_eq!(result.name, project_name);
assert_eq!(result.id, "special-project-456");
}
#[tokio::test]
async fn test_delete_project_success() {
let mut server = Server::new_async().await;
let mock = server
.mock("DELETE", "/v1/project/test-project-123")
.with_status(204)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.delete_project("test-project-123").await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_project_with_special_id() {
let mut server = Server::new_async().await;
let project_id = "project-with-dashes-and-numbers-123";
let mock = server
.mock("DELETE", format!("/v1/project/{}", project_id).as_str())
.with_status(204)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.delete_project(project_id).await;
mock.assert_async().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_projects_api_error() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "Unauthorized",
"message": "Invalid authentication token",
"details": "The provided token is expired or invalid"
}
});
let mock = server
.mock("GET", "/v1/project")
.with_status(401)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.list_projects().await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError {
etype,
message,
details,
}) = result
{
assert_eq!(etype, "Unauthorized");
assert_eq!(message, "Invalid authentication token");
assert_eq!(
details,
Some("The provided token is expired or invalid".to_string())
);
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_create_project_validation_error() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "ValidationError",
"message": "Project name is required",
"details": "The project name cannot be empty"
}
});
let mock = server
.mock("POST", "/v1/project")
.match_body(Matcher::Json(serde_json::json!({
"name": ""
})))
.with_status(400)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.create_project("").await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "ValidationError");
assert_eq!(message, "Project name is required");
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_create_project_conflict_error() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "Conflict",
"message": "Project with this name already exists",
"details": null
}
});
let mock = server
.mock("POST", "/v1/project")
.match_body(Matcher::Json(serde_json::json!({
"name": "Existing Project"
})))
.with_status(409)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.create_project("Existing Project").await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "Conflict");
assert_eq!(message, "Project with this name already exists");
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_delete_project_not_found() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "NotFound",
"message": "Project not found",
"details": "The specified project does not exist or you don't have permission to delete it"
}
});
let mock = server
.mock("DELETE", "/v1/project/nonexistent-project")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.delete_project("nonexistent-project").await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "NotFound");
assert_eq!(message, "Project not found");
} else {
panic!("Expected APIResponseError");
}
}
#[tokio::test]
async fn test_delete_project_forbidden() {
let mut server = Server::new_async().await;
let error_response = serde_json::json!({
"error": {
"type": "Forbidden",
"message": "Insufficient permissions to delete project",
"details": "Only project owners can delete projects"
}
});
let mock = server
.mock("DELETE", "/v1/project/restricted-project")
.with_status(403)
.with_header("content-type", "application/json")
.with_body(error_response.to_string())
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.delete_project("restricted-project").await;
mock.assert_async().await;
assert!(result.is_err());
if let Err(UbiClientError::APIResponseError { etype, message, .. }) = result {
assert_eq!(etype, "Forbidden");
assert_eq!(message, "Insufficient permissions to delete project");
} else {
panic!("Expected APIResponseError");
}
}
#[test]
fn test_project_list_default() {
let project_list = ProjectList::default();
assert_eq!(project_list.count, 0);
assert_eq!(project_list.items.len(), 0);
}
#[test]
fn test_user_project_default() {
let user_project = UserProject::default();
assert_eq!(user_project.credit, 0.0);
assert_eq!(user_project.discount, 0);
assert_eq!(user_project.id, "");
assert_eq!(user_project.name, "");
}
#[test]
fn test_user_project_serialization() {
let project = UserProject {
credit: 123.45,
discount: 20,
id: "test-project-789".to_string(),
name: "Serialization Test Project".to_string(),
};
let serialized = serde_json::to_string(&project).unwrap();
let deserialized: UserProject = serde_json::from_str(&serialized).unwrap();
assert_eq!(project.credit, deserialized.credit);
assert_eq!(project.discount, deserialized.discount);
assert_eq!(project.id, deserialized.id);
assert_eq!(project.name, deserialized.name);
}
#[test]
fn test_project_list_serialization() {
let project_list = ProjectList {
count: 1,
items: vec![UserProject {
credit: 99.99,
discount: 5,
id: "serialize-test".to_string(),
name: "Serialize Test".to_string(),
}],
};
let serialized = serde_json::to_string(&project_list).unwrap();
let deserialized: ProjectList = serde_json::from_str(&serialized).unwrap();
assert_eq!(project_list.count, deserialized.count);
assert_eq!(project_list.items.len(), deserialized.items.len());
assert_eq!(project_list.items[0].name, deserialized.items[0].name);
}
#[test]
fn test_project_new() {
let reqwest_client = reqwest::Client::new();
let http_client = HTTPClient::new("https://api.example.com", reqwest_client, "v1");
let project_client = Project::new(Arc::new(http_client));
assert!(std::ptr::addr_of!(project_client) as usize != 0);
}
}