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:
@@ -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<SqlitePool>,
|
||||
Json(input): Json<CreateTodo>,
|
||||
@@ -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<SqlitePool>,
|
||||
) -> Result<Json<Vec<Todo>>, 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<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
@@ -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<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
@@ -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<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
@@ -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<serde_json::Value> = 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);
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user