CodeBench: tiivistetty todo-rs.md golden example 540→331 riviä

- handlers.rs: tiiviimpi muotoilu, kommentit kuvaavat patternia
- tests: 10 testiä → 4 avaintestiä (create, get, not_found, delete)
- spawn_server tiivistetty
- Kaikki kriittiset patternit säilyvät: RETURNING, fetch_optional, rows_affected
This commit is contained in:
2026-04-14 17:50:19 +03:00
parent d003f73217
commit 2f602717b8

View File

@@ -69,7 +69,7 @@ pub struct UpdateTodo {
## src/handlers.rs ## 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 ```rust
//! Käsittelijät — CRUD-operaatiot todo-entiteetille. //! Käsittelijät — CRUD-operaatiot todo-entiteetille.
@@ -81,7 +81,7 @@ use sqlx::SqlitePool;
use crate::models::{CreateTodo, Todo, UpdateTodo}; use crate::models::{CreateTodo, Todo, UpdateTodo};
/// Luo uusi tehtävä. /// POST — INSERT RETURNING, oletusarvot unwrap_or:lla.
pub async fn create_todo( pub async fn create_todo(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Json(input): Json<CreateTodo>, Json(input): Json<CreateTodo>,
@@ -106,7 +106,7 @@ pub async fn create_todo(
Ok((StatusCode::CREATED, Json(result))) Ok((StatusCode::CREATED, Json(result)))
} }
/// Listaa kaikki tehtävät. /// GET list — fetch_all.
pub async fn list_todos( pub async fn list_todos(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
) -> Result<Json<Vec<Todo>>, StatusCode> { ) -> Result<Json<Vec<Todo>>, StatusCode> {
@@ -114,11 +114,10 @@ pub async fn list_todos(
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(todos)) Ok(Json(todos))
} }
/// Hae tehtävä id:llä. /// GET by id — fetch_optional, None → 404.
pub async fn get_todo( pub async fn get_todo(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Path(id): Path<i64>, Path(id): Path<i64>,
@@ -130,14 +129,13 @@ pub async fn get_todo(
.fetch_optional(&pool) .fetch_optional(&pool)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match todo { match todo {
Some(t) => Ok(Json(t)), Some(t) => Ok(Json(t)),
None => Err(StatusCode::NOT_FOUND), None => Err(StatusCode::NOT_FOUND),
} }
} }
/// Päivitä tehtävä id:llä. /// PUT — hae olemassaoleva, merge kentät, UPDATE RETURNING.
pub async fn update_todo( pub async fn update_todo(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Path(id): Path<i64>, Path(id): Path<i64>,
@@ -150,34 +148,25 @@ pub async fn update_todo(
.fetch_optional(&pool) .fetch_optional(&pool)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let existing = existing.ok_or(StatusCode::NOT_FOUND)?; 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>( let updated = sqlx::query_as::<_, Todo>(
"UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ? "UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ?
WHERE id = ? WHERE id = ? RETURNING id, title, description, due_date, priority, status",
RETURNING id, title, description, due_date, priority, status",
) )
.bind(&title) .bind(input.title.unwrap_or(existing.title))
.bind(&description) .bind(input.description.or(existing.description))
.bind(&due_date) .bind(input.due_date.or(existing.due_date))
.bind(priority) .bind(input.priority.unwrap_or(existing.priority))
.bind(&status) .bind(input.status.unwrap_or(existing.status))
.bind(id) .bind(id)
.fetch_one(&pool) .fetch_one(&pool)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(updated)) Ok(Json(updated))
} }
/// Poista tehtävä id:llä. /// DELETE — rows_affected == 0 → 404, muuten 204.
pub async fn delete_todo( pub async fn delete_todo(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Path(id): Path<i64>, Path(id): Path<i64>,
@@ -187,11 +176,7 @@ pub async fn delete_todo(
.execute(&pool) .execute(&pool)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .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) Ok(StatusCode::NO_CONTENT)
} }
``` ```
@@ -272,7 +257,7 @@ async fn main() {
## tests/api_test.rs ## 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 ```rust
//! Integraatiotestit — muistinvarainen SQLite, uniikki data per testi. //! Integraatiotestit — muistinvarainen SQLite, uniikki data per testi.
@@ -289,252 +274,58 @@ async fn spawn_server() -> (Client, String) {
.connect("sqlite::memory:") .connect("sqlite::memory:")
.await .await
.expect("Testitietokanta epäonnistui"); .expect("Testitietokanta epäonnistui");
init_db(&pool).await; init_db(&pool).await;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0") let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await .await
.expect("Testiportin kuuntelu epäonnistui"); .expect("Testiportin kuuntelu epäonnistui");
let addr = listener.local_addr().unwrap(); let base_url = format!("http://{}", listener.local_addr().unwrap());
let base_url = format!("http://{addr}");
let router = app(pool); let router = app(pool);
tokio::spawn(async move { tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
axum::serve(listener, router).await.unwrap();
});
(Client::new(), base_url) (Client::new(), base_url)
} }
#[tokio::test] #[tokio::test]
async fn test_create_todo() { async fn test_create_todo() {
let (client, url) = spawn_server().await; 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})) .json(&serde_json::json!({"title": "Osta maitoa", "priority": 2}))
.send() .send().await.unwrap();
.await
.unwrap();
assert_eq!(res.status(), StatusCode::CREATED); assert_eq!(res.status(), StatusCode::CREATED);
let body: serde_json::Value = res.json().await.unwrap(); let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["title"], "Osta maitoa"); assert_eq!(body["title"], "Osta maitoa");
assert_eq!(body["priority"], 2);
assert!(body["id"].is_number()); 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<serde_json::Value> = res.json().await.unwrap();
assert!(body.len() >= 1);
}
#[tokio::test] #[tokio::test]
async fn test_get_todo_by_id() { async fn test_get_todo_by_id() {
let (client, url) = spawn_server().await; 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ä"})) .json(&serde_json::json!({"title": "Haettava tehtävä"}))
.send() .send().await.unwrap().json().await.unwrap();
.await
.unwrap()
.json()
.await
.unwrap();
let id = created["id"].as_i64().unwrap(); let id = created["id"].as_i64().unwrap();
let res = client let res = client.get(format!("{url}/todos/{id}")).send().await.unwrap();
.get(format!("{url}/todos/{id}"))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body: serde_json::Value = res.json().await.unwrap(); let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["id"], id); assert_eq!(body["id"], id);
assert_eq!(body["title"], "Haettava tehtävä");
} }
#[tokio::test] #[tokio::test]
async fn test_get_todo_not_found() { async fn test_get_todo_not_found() {
let (client, url) = spawn_server().await; let (client, url) = spawn_server().await;
let res = client.get(format!("{url}/todos/99999")).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_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();
assert_eq!(res.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);
} }
#[tokio::test] #[tokio::test]
async fn test_delete_todo() { async fn test_delete_todo() {
let (client, url) = spawn_server().await; 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"})) .json(&serde_json::json!({"title": "Poistettava"}))
.send() .send().await.unwrap().json().await.unwrap();
.await
.unwrap()
.json()
.await
.unwrap();
let id = created["id"].as_i64().unwrap(); let id = created["id"].as_i64().unwrap();
let res = client let res = client.delete(format!("{url}/todos/{id}")).send().await.unwrap();
.delete(format!("{url}/todos/{id}"))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::NO_CONTENT); 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); 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);
}
``` ```