diff --git a/kipina-codebench/golden-examples/todo-rs.md b/kipina-codebench/golden-examples/todo-rs.md index b28171c..982cad5 100644 --- a/kipina-codebench/golden-examples/todo-rs.md +++ b/kipina-codebench/golden-examples/todo-rs.md @@ -69,7 +69,7 @@ pub struct UpdateTodo { ## src/handlers.rs -CRUD-käsittelijät: POST 201 RETURNING, GET lista, GET by id 404, PUT (merge olemassaolevaan), DELETE 204. +CRUD-käsittelijät. Avainpatternit: INSERT RETURNING, fetch_optional+404, rows_affected+204. ```rust //! Käsittelijät — CRUD-operaatiot todo-entiteetille. @@ -81,7 +81,7 @@ use sqlx::SqlitePool; use crate::models::{CreateTodo, Todo, UpdateTodo}; -/// Luo uusi tehtävä. +/// POST — INSERT RETURNING, oletusarvot unwrap_or:lla. pub async fn create_todo( State(pool): State, Json(input): Json, @@ -106,7 +106,7 @@ pub async fn create_todo( Ok((StatusCode::CREATED, Json(result))) } -/// Listaa kaikki tehtävät. +/// GET list — fetch_all. pub async fn list_todos( State(pool): State, ) -> Result>, StatusCode> { @@ -114,11 +114,10 @@ pub async fn list_todos( .fetch_all(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(todos)) } -/// Hae tehtävä id:llä. +/// GET by id — fetch_optional, None → 404. pub async fn get_todo( State(pool): State, Path(id): Path, @@ -130,14 +129,13 @@ pub async fn get_todo( .fetch_optional(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - match todo { Some(t) => Ok(Json(t)), None => Err(StatusCode::NOT_FOUND), } } -/// Päivitä tehtävä id:llä. +/// PUT — hae olemassaoleva, merge kentät, UPDATE RETURNING. pub async fn update_todo( State(pool): State, Path(id): Path, @@ -150,34 +148,25 @@ pub async fn update_todo( .fetch_optional(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let existing = existing.ok_or(StatusCode::NOT_FOUND)?; - let title = input.title.unwrap_or(existing.title); - let description = input.description.or(existing.description); - let due_date = input.due_date.or(existing.due_date); - let priority = input.priority.unwrap_or(existing.priority); - let status = input.status.unwrap_or(existing.status); - let updated = sqlx::query_as::<_, Todo>( "UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ? - WHERE id = ? - RETURNING id, title, description, due_date, priority, status", + WHERE id = ? RETURNING id, title, description, due_date, priority, status", ) - .bind(&title) - .bind(&description) - .bind(&due_date) - .bind(priority) - .bind(&status) + .bind(input.title.unwrap_or(existing.title)) + .bind(input.description.or(existing.description)) + .bind(input.due_date.or(existing.due_date)) + .bind(input.priority.unwrap_or(existing.priority)) + .bind(input.status.unwrap_or(existing.status)) .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(updated)) } -/// Poista tehtävä id:llä. +/// DELETE — rows_affected == 0 → 404, muuten 204. pub async fn delete_todo( State(pool): State, Path(id): Path, @@ -187,11 +176,7 @@ pub async fn delete_todo( .execute(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - if result.rows_affected() == 0 { - return Err(StatusCode::NOT_FOUND); - } - + if result.rows_affected() == 0 { return Err(StatusCode::NOT_FOUND); } Ok(StatusCode::NO_CONTENT) } ``` @@ -272,7 +257,7 @@ async fn main() { ## tests/api_test.rs -Integraatiotestit: muistinvarainen SQLite, `spawn_server` satunnaisportissa, uniikki suomenkielinen data per testi. +Integraatiotestit: spawn_server (muistinvarainen SQLite, satunnaisportti), CRUD-testit uniikilla datalla. ```rust //! Integraatiotestit — muistinvarainen SQLite, uniikki data per testi. @@ -289,252 +274,58 @@ async fn spawn_server() -> (Client, String) { .connect("sqlite::memory:") .await .expect("Testitietokanta epäonnistui"); - init_db(&pool).await; - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("Testiportin kuuntelu epäonnistui"); - let addr = listener.local_addr().unwrap(); - let base_url = format!("http://{addr}"); - + let base_url = format!("http://{}", listener.local_addr().unwrap()); let router = app(pool); - tokio::spawn(async move { - axum::serve(listener, router).await.unwrap(); - }); - + tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); (Client::new(), base_url) } #[tokio::test] async fn test_create_todo() { let (client, url) = spawn_server().await; - - let res = client - .post(format!("{url}/todos")) + let res = client.post(format!("{url}/todos")) .json(&serde_json::json!({"title": "Osta maitoa", "priority": 2})) - .send() - .await - .unwrap(); - + .send().await.unwrap(); assert_eq!(res.status(), StatusCode::CREATED); let body: serde_json::Value = res.json().await.unwrap(); assert_eq!(body["title"], "Osta maitoa"); - assert_eq!(body["priority"], 2); assert!(body["id"].is_number()); } -#[tokio::test] -async fn test_create_todo_defaults() { - let (client, url) = spawn_server().await; - - let res = client - .post(format!("{url}/todos")) - .json(&serde_json::json!({"title": "Oletusarvotesti"})) - .send() - .await - .unwrap(); - - assert_eq!(res.status(), StatusCode::CREATED); - let body: serde_json::Value = res.json().await.unwrap(); - assert_eq!(body["priority"], 1); - assert_eq!(body["status"], "pending"); - assert!(body["description"].is_null()); -} - -#[tokio::test] -async fn test_list_todos() { - let (client, url) = spawn_server().await; - - client - .post(format!("{url}/todos")) - .json(&serde_json::json!({"title": "Listattava tehtävä"})) - .send() - .await - .unwrap(); - - let res = client.get(format!("{url}/todos")).send().await.unwrap(); - assert_eq!(res.status(), StatusCode::OK); - - let body: Vec = res.json().await.unwrap(); - assert!(body.len() >= 1); -} - #[tokio::test] async fn test_get_todo_by_id() { let (client, url) = spawn_server().await; - - let created: serde_json::Value = client - .post(format!("{url}/todos")) + let created: serde_json::Value = client.post(format!("{url}/todos")) .json(&serde_json::json!({"title": "Haettava tehtävä"})) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - + .send().await.unwrap().json().await.unwrap(); let id = created["id"].as_i64().unwrap(); - let res = client - .get(format!("{url}/todos/{id}")) - .send() - .await - .unwrap(); - + let res = client.get(format!("{url}/todos/{id}")).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let body: serde_json::Value = res.json().await.unwrap(); assert_eq!(body["id"], id); - assert_eq!(body["title"], "Haettava tehtävä"); } #[tokio::test] async fn test_get_todo_not_found() { let (client, url) = spawn_server().await; - - let res = client - .get(format!("{url}/todos/99999")) - .send() - .await - .unwrap(); - - assert_eq!(res.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_update_todo() { - let (client, url) = spawn_server().await; - - let created: serde_json::Value = client - .post(format!("{url}/todos")) - .json(&serde_json::json!({"title": "Vanha otsikko"})) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - - let id = created["id"].as_i64().unwrap(); - let res = client - .put(format!("{url}/todos/{id}")) - .json(&serde_json::json!({"title": "Uusi otsikko"})) - .send() - .await - .unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - let body: serde_json::Value = res.json().await.unwrap(); - assert_eq!(body["title"], "Uusi otsikko"); -} - -#[tokio::test] -async fn test_update_todo_not_found() { - let (client, url) = spawn_server().await; - - let res = client - .put(format!("{url}/todos/99999")) - .json(&serde_json::json!({"title": "Ei löydy"})) - .send() - .await - .unwrap(); - + let res = client.get(format!("{url}/todos/99999")).send().await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_delete_todo() { let (client, url) = spawn_server().await; - - let created: serde_json::Value = client - .post(format!("{url}/todos")) + let created: serde_json::Value = client.post(format!("{url}/todos")) .json(&serde_json::json!({"title": "Poistettava"})) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - + .send().await.unwrap().json().await.unwrap(); let id = created["id"].as_i64().unwrap(); - let res = client - .delete(format!("{url}/todos/{id}")) - .send() - .await - .unwrap(); - + let res = client.delete(format!("{url}/todos/{id}")).send().await.unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); - - let res = client - .get(format!("{url}/todos/{id}")) - .send() - .await - .unwrap(); - + let res = client.get(format!("{url}/todos/{id}")).send().await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); } - -#[tokio::test] -async fn test_delete_todo_not_found() { - let (client, url) = spawn_server().await; - - let res = client - .delete(format!("{url}/todos/99999")) - .send() - .await - .unwrap(); - - assert_eq!(res.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_full_lifecycle() { - let (client, url) = spawn_server().await; - - // Luo - let created: serde_json::Value = client - .post(format!("{url}/todos")) - .json(&serde_json::json!({ - "title": "Elinkaaritesti", - "description": "Testataan koko CRUD-kierto", - "due_date": "2026-12-31", - "priority": 3, - "status": "in_progress" - })) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - - let id = created["id"].as_i64().unwrap(); - assert_eq!(created["title"], "Elinkaaritesti"); - assert_eq!(created["description"], "Testataan koko CRUD-kierto"); - assert_eq!(created["due_date"], "2026-12-31"); - assert_eq!(created["priority"], 3); - assert_eq!(created["status"], "in_progress"); - - // Päivitä - let updated: serde_json::Value = client - .put(format!("{url}/todos/{id}")) - .json(&serde_json::json!({"status": "done"})) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - - assert_eq!(updated["status"], "done"); - assert_eq!(updated["title"], "Elinkaaritesti"); - - // Poista - let res = client - .delete(format!("{url}/todos/{id}")) - .send() - .await - .unwrap(); - - assert_eq!(res.status(), StatusCode::NO_CONTENT); -} ```