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
|
## 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);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user