tests.rs
· 9.6 KiB · Rust
Исходник
"use std::sync::{Arc, Mutex};\n\nuse axum::{\n body::Body,\n extract::{Path, Request, State},\n http::{Method, Request as HttpRequest, StatusCode},\n middleware::{self, Next},\n response::{IntoResponse, Json, Response},\n routing::get,\n Router,\n};\nuse chrono::{DateTime, Utc};\nuse serde::{Deserialize, Serialize};\nuse tower::ServiceExt;\nuse uuid::Uuid;\n\n// Reutilizamos los mismos tipos que en main.rs\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\nstruct Task {\n id: String,\n title: String,\n description: String,\n completed: bool,\n created_at: DateTime<Utc>,\n updated_at: DateTime<Utc>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct CreateTaskRequest {\n title: String,\n description: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct UpdateTaskRequest {\n title: Option<String>,\n description: Option<String>,\n completed: Option<bool>,\n}\n\ntype TaskDb = Arc<Mutex<Vec<Task>>>;\n\n#[derive(Clone)]\nstruct AppState {\n db: TaskDb,\n}\n\n#[derive(Debug)]\nenum AppError {\n NotFound(String),\n BadRequest(String),\n Internal(String),\n}\n\nimpl IntoResponse for AppError {\n fn into_response(self) -> Response {\n let (status, message) = match self {\n AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),\n AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),\n AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),\n };\n\n let body = Json(serde_json::json!({\n \"error\": message,\n \"code\": status.as_u16(),\n }));\n\n (status, body).into_response()\n }\n}\n\n// ─── Handlers ──────────────────────────────────────────────────────\n\nasync fn create_task(\n State(state): State<AppState>,\n Json(payload): Json<CreateTaskRequest>,\n) -> Result<(StatusCode, Json<Task>), AppError> {\n let title = payload.title.trim().to_string();\n if title.is_empty() {\n return Err(AppError::BadRequest(\n \"El campo 'title' es obligatorio y no puede estar vacío\".into(),\n ));\n }\n\n let task = Task {\n id: Uuid::new_v4().to_string(),\n title,\n description: payload.description.unwrap_or_default(),\n completed: false,\n created_at: Utc::now(),\n updated_at: Utc::now(),\n };\n\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let response = task.clone();\n db.push(task);\n\n Ok((StatusCode::CREATED, Json(response)))\n}\n\nasync fn list_tasks(\n State(state): State<AppState>,\n) -> Result<Json<Vec<Task>>, AppError> {\n let db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n Ok(Json(db.clone()))\n}\n\nasync fn get_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n) -> Result<Json<Task>, AppError> {\n let db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let task = db\n .iter()\n .find(|t| t.id == id)\n .cloned()\n .ok_or_else(|| AppError::NotFound(format!(\"Tarea {} no encontrada\", id)))?;\n\n Ok(Json(task))\n}\n\nasync fn update_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n Json(payload): Json<UpdateTaskRequest>,\n) -> Result<Json<Task>, AppError> {\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let task = db\n .iter_mut()\n .find(|t| t.id == id)\n .ok_or_else(|| AppError::NotFound(format!(\"Tarea {} no encontrada\", id)))?;\n\n if let Some(title) = payload.title {\n let trimmed = title.trim().to_string();\n if trimmed.is_empty() {\n return Err(AppError::BadRequest(\n \"El campo 'title' no puede estar vacío\".into(),\n ));\n }\n task.title = trimmed;\n }\n\n if let Some(description) = payload.description {\n task.description = description;\n }\n\n if let Some(completed) = payload.completed {\n task.completed = completed;\n }\n\n task.updated_at = Utc::now();\n\n Ok(Json(task.clone()))\n}\n\nasync fn delete_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n) -> Result<StatusCode, AppError> {\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let len_before = db.len();\n db.retain(|t| t.id != id);\n\n if db.len() == len_before {\n return Err(AppError::NotFound(format!(\"Tarea {} no encontrada\", id)));\n }\n\n Ok(StatusCode::NO_CONTENT)\n}\n\n// ─── Tests ─────────────────────────────────────────────────────────\n\nfn test_app() -> Router {\n let state = AppState {\n db: Arc::new(Mutex::new(Vec::new())),\n };\n\n Router::new()\n .route(\"/tasks\", get(list_tasks).post(create_task))\n .route(\"/tasks/{id}\", get(get_task).put(update_task).delete(delete_task))\n .with_state(state)\n}\n\nasync fn send_request(app: Router, method: Method, path: &str, body: Option<&str>) -> Response {\n let mut builder = HttpRequest::builder()\n .method(method)\n .uri(path)\n .header(\"Content-Type\", \"application/json\");\n\n let req = if let Some(b) = body {\n builder.body(Body::from(b.to_string())).unwrap()\n } else {\n builder.body(Body::empty()).unwrap()\n };\n\n app.oneshot(req).await.unwrap()\n}\n\n#[tokio::test]\nasync fn test_create_task() {\n let app = test_app();\n\n let response = send_request(\n app,\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Test task\", \"description\": \"A test\"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::CREATED);\n\n let body: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(!body.id.is_empty());\n assert_eq!(body.title, \"Test task\");\n assert_eq!(body.description, \"A test\");\n assert!(!body.completed);\n}\n\n#[tokio::test]\nasync fn test_create_task_empty_title() {\n let app = test_app();\n\n let response = send_request(\n app,\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \" \"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::BAD_REQUEST);\n}\n\n#[tokio::test]\nasync fn test_list_tasks_empty() {\n let app = test_app();\n\n let response = send_request(app, Method::GET, \"/tasks\", None).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let body: Vec<Task> = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(body.is_empty());\n}\n\n#[tokio::test]\nasync fn test_list_tasks_with_data() {\n let app = test_app();\n\n // Create a task first\n send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Task 1\"}\"#),\n ).await;\n\n send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Task 2\"}\"#),\n ).await;\n\n // List them\n let response = send_request(app, Method::GET, \"/tasks\", None).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let body: Vec<Task> = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(body.len() >= 2);\n}\n\n#[tokio::test]\nasync fn test_get_task_not_found() {\n let app = test_app();\n\n let response = send_request(app, Method::GET, \"/tasks/nonexistent-id\", None).await;\n\n assert_eq!(response.status(), StatusCode::NOT_FOUND);\n}\n\n#[tokio::test]\nasync fn test_crud_cycle() {\n let app = test_app();\n\n // Create\n let response = send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Cycle task\"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::CREATED);\n\n let created: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n let task_id = created.id.clone();\n\n // Get by ID\n let response = send_request(app.clone(), Method::GET, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::OK);\n\n // Update\n let response = send_request(\n app.clone(),\n Method::PUT,\n &format!(\"/tasks/{}\", task_id),\n Some(r#\"{\"title\": \"Updated\", \"completed\": true}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let updated: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert_eq!(updated.title, \"Updated\");\n assert!(updated.completed);\n\n // Delete\n let response = send_request(app.clone(), Method::DELETE, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::NO_CONTENT);\n\n // Verify deletion\n let response = send_request(app, Method::GET, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::NOT_FOUND);\n}\n"
| 1 | "use std::sync::{Arc, Mutex};\n\nuse axum::{\n body::Body,\n extract::{Path, Request, State},\n http::{Method, Request as HttpRequest, StatusCode},\n middleware::{self, Next},\n response::{IntoResponse, Json, Response},\n routing::get,\n Router,\n};\nuse chrono::{DateTime, Utc};\nuse serde::{Deserialize, Serialize};\nuse tower::ServiceExt;\nuse uuid::Uuid;\n\n// Reutilizamos los mismos tipos que en main.rs\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\nstruct Task {\n id: String,\n title: String,\n description: String,\n completed: bool,\n created_at: DateTime<Utc>,\n updated_at: DateTime<Utc>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct CreateTaskRequest {\n title: String,\n description: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct UpdateTaskRequest {\n title: Option<String>,\n description: Option<String>,\n completed: Option<bool>,\n}\n\ntype TaskDb = Arc<Mutex<Vec<Task>>>;\n\n#[derive(Clone)]\nstruct AppState {\n db: TaskDb,\n}\n\n#[derive(Debug)]\nenum AppError {\n NotFound(String),\n BadRequest(String),\n Internal(String),\n}\n\nimpl IntoResponse for AppError {\n fn into_response(self) -> Response {\n let (status, message) = match self {\n AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),\n AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),\n AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),\n };\n\n let body = Json(serde_json::json!({\n \"error\": message,\n \"code\": status.as_u16(),\n }));\n\n (status, body).into_response()\n }\n}\n\n// ─── Handlers ──────────────────────────────────────────────────────\n\nasync fn create_task(\n State(state): State<AppState>,\n Json(payload): Json<CreateTaskRequest>,\n) -> Result<(StatusCode, Json<Task>), AppError> {\n let title = payload.title.trim().to_string();\n if title.is_empty() {\n return Err(AppError::BadRequest(\n \"El campo 'title' es obligatorio y no puede estar vacío\".into(),\n ));\n }\n\n let task = Task {\n id: Uuid::new_v4().to_string(),\n title,\n description: payload.description.unwrap_or_default(),\n completed: false,\n created_at: Utc::now(),\n updated_at: Utc::now(),\n };\n\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let response = task.clone();\n db.push(task);\n\n Ok((StatusCode::CREATED, Json(response)))\n}\n\nasync fn list_tasks(\n State(state): State<AppState>,\n) -> Result<Json<Vec<Task>>, AppError> {\n let db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n Ok(Json(db.clone()))\n}\n\nasync fn get_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n) -> Result<Json<Task>, AppError> {\n let db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let task = db\n .iter()\n .find(|t| t.id == id)\n .cloned()\n .ok_or_else(|| AppError::NotFound(format!(\"Tarea {} no encontrada\", id)))?;\n\n Ok(Json(task))\n}\n\nasync fn update_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n Json(payload): Json<UpdateTaskRequest>,\n) -> Result<Json<Task>, AppError> {\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let task = db\n .iter_mut()\n .find(|t| t.id == id)\n .ok_or_else(|| AppError::NotFound(format!(\"Tarea {} no encontrada\", id)))?;\n\n if let Some(title) = payload.title {\n let trimmed = title.trim().to_string();\n if trimmed.is_empty() {\n return Err(AppError::BadRequest(\n \"El campo 'title' no puede estar vacío\".into(),\n ));\n }\n task.title = trimmed;\n }\n\n if let Some(description) = payload.description {\n task.description = description;\n }\n\n if let Some(completed) = payload.completed {\n task.completed = completed;\n }\n\n task.updated_at = Utc::now();\n\n Ok(Json(task.clone()))\n}\n\nasync fn delete_task(\n State(state): State<AppState>,\n Path(id): Path<String>,\n) -> Result<StatusCode, AppError> {\n let mut db = state.db.lock().map_err(|e| {\n AppError::Internal(format!(\"Error de concurrencia: {}\", e))\n })?;\n\n let len_before = db.len();\n db.retain(|t| t.id != id);\n\n if db.len() == len_before {\n return Err(AppError::NotFound(format!(\"Tarea {} no encontrada\", id)));\n }\n\n Ok(StatusCode::NO_CONTENT)\n}\n\n// ─── Tests ─────────────────────────────────────────────────────────\n\nfn test_app() -> Router {\n let state = AppState {\n db: Arc::new(Mutex::new(Vec::new())),\n };\n\n Router::new()\n .route(\"/tasks\", get(list_tasks).post(create_task))\n .route(\"/tasks/{id}\", get(get_task).put(update_task).delete(delete_task))\n .with_state(state)\n}\n\nasync fn send_request(app: Router, method: Method, path: &str, body: Option<&str>) -> Response {\n let mut builder = HttpRequest::builder()\n .method(method)\n .uri(path)\n .header(\"Content-Type\", \"application/json\");\n\n let req = if let Some(b) = body {\n builder.body(Body::from(b.to_string())).unwrap()\n } else {\n builder.body(Body::empty()).unwrap()\n };\n\n app.oneshot(req).await.unwrap()\n}\n\n#[tokio::test]\nasync fn test_create_task() {\n let app = test_app();\n\n let response = send_request(\n app,\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Test task\", \"description\": \"A test\"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::CREATED);\n\n let body: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(!body.id.is_empty());\n assert_eq!(body.title, \"Test task\");\n assert_eq!(body.description, \"A test\");\n assert!(!body.completed);\n}\n\n#[tokio::test]\nasync fn test_create_task_empty_title() {\n let app = test_app();\n\n let response = send_request(\n app,\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \" \"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::BAD_REQUEST);\n}\n\n#[tokio::test]\nasync fn test_list_tasks_empty() {\n let app = test_app();\n\n let response = send_request(app, Method::GET, \"/tasks\", None).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let body: Vec<Task> = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(body.is_empty());\n}\n\n#[tokio::test]\nasync fn test_list_tasks_with_data() {\n let app = test_app();\n\n // Create a task first\n send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Task 1\"}\"#),\n ).await;\n\n send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Task 2\"}\"#),\n ).await;\n\n // List them\n let response = send_request(app, Method::GET, \"/tasks\", None).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let body: Vec<Task> = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert!(body.len() >= 2);\n}\n\n#[tokio::test]\nasync fn test_get_task_not_found() {\n let app = test_app();\n\n let response = send_request(app, Method::GET, \"/tasks/nonexistent-id\", None).await;\n\n assert_eq!(response.status(), StatusCode::NOT_FOUND);\n}\n\n#[tokio::test]\nasync fn test_crud_cycle() {\n let app = test_app();\n\n // Create\n let response = send_request(\n app.clone(),\n Method::POST,\n \"/tasks\",\n Some(r#\"{\"title\": \"Cycle task\"}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::CREATED);\n\n let created: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n let task_id = created.id.clone();\n\n // Get by ID\n let response = send_request(app.clone(), Method::GET, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::OK);\n\n // Update\n let response = send_request(\n app.clone(),\n Method::PUT,\n &format!(\"/tasks/{}\", task_id),\n Some(r#\"{\"title\": \"Updated\", \"completed\": true}\"#),\n ).await;\n\n assert_eq!(response.status(), StatusCode::OK);\n\n let updated: Task = serde_json::from_slice(\n &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()\n ).unwrap();\n\n assert_eq!(updated.title, \"Updated\");\n assert!(updated.completed);\n\n // Delete\n let response = send_request(app.clone(), Method::DELETE, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::NO_CONTENT);\n\n // Verify deletion\n let response = send_request(app, Method::GET, &format!(\"/tasks/{}\", task_id), None).await;\n assert_eq!(response.status(), StatusCode::NOT_FOUND);\n}\n" |