Compare commits

...

6 commits

5 changed files with 457 additions and 3 deletions

View file

@ -13,6 +13,7 @@ pub enum AppError {
InternalError(anyhow::Error),
JsonExtractError(JsonRejection),
PathError(PathRejection),
Unprocessable(String),
NotFound,
}
@ -36,6 +37,11 @@ impl IntoResponse for AppError {
}),
)
.into_response(),
Self::Unprocessable(msg) => (
StatusCode::UNPROCESSABLE_ENTITY,
Json(ErrorJson { error: msg }),
)
.into_response(),
Self::PathError(rej) => (
StatusCode::BAD_REQUEST,
Json(ErrorJson {

View file

@ -10,14 +10,17 @@ use serde::Deserialize;
use sqlx::{Pool, Sqlite};
use uuid::Uuid;
use crate::models::TaskModel;
use crate::models::{TaskModel, TaskStatus};
use super::AppError;
pub fn create_task_router() -> Router<Pool<Sqlite>> {
Router::new()
.route("/", post(create_task))
.route("/{task_id}", get(get_task))
.route("/", post(create_task).get(list_tasks))
.route(
"/{task_id}",
get(get_task).put(update_task).delete(delete_task),
)
}
#[derive(Deserialize)]
@ -37,6 +40,14 @@ pub async fn create_task(
Ok((StatusCode::CREATED, Json(model)))
}
pub async fn list_tasks(
State(pool): State<Pool<Sqlite>>,
) -> Result<(StatusCode, Json<Vec<TaskModel>>), AppError> {
let tasks = TaskModel::list_all(&pool).await?;
Ok((StatusCode::OK, Json(tasks)))
}
pub async fn get_task(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(task_id), _): WithRejection<Path<Uuid>, AppError>,
@ -45,3 +56,51 @@ pub async fn get_task(
Ok((StatusCode::OK, Json(model)))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateTaskRequest {
title: Option<String>,
description: Option<String>,
status: Option<TaskStatus>,
}
pub async fn update_task(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(task_id), _): WithRejection<Path<Uuid>, AppError>,
WithRejection(Json(input), _): WithRejection<Json<UpdateTaskRequest>, AppError>,
) -> Result<(StatusCode, Json<TaskModel>), AppError> {
let mut model = TaskModel::get_by_id(&pool, task_id).await?;
if let Some(title) = input.title {
if title.len() == 0 {
return Err(AppError::Unprocessable(
"Title must not be empty".to_string(),
));
}
model.title = title;
}
if let Some(description) = input.description {
model.description = Some(description);
}
if let Some(status) = input.status {
model.status = status;
}
let model = model.update(&pool).await?;
Ok((StatusCode::OK, Json(model)))
}
pub async fn delete_task(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(task_id), _): WithRejection<Path<Uuid>, AppError>,
) -> Result<StatusCode, AppError> {
// Ensure that the task exists.
TaskModel::get_by_id(&pool, task_id).await?;
TaskModel::delete(&pool, task_id).await?;
Ok(StatusCode::NO_CONTENT)
}

View file

@ -0,0 +1,92 @@
# Task Delete API Tests
# Setup: Create a task to delete
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Task to Delete",
"description": "This task will be deleted"
}
HTTP 201
[Captures]
delete_task_id: jsonpath "$.id"
# Test: Delete task successfully
DELETE {{host}}/api/tasks/{{delete_task_id}}
HTTP 204
# Test: Verify task is deleted (should return 404)
GET {{host}}/api/tasks/{{delete_task_id}}
HTTP 404
[Asserts]
jsonpath "$.error" exists
# Setup: Create another task for additional tests
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Another Task to Delete"
}
HTTP 201
[Captures]
another_task_id: jsonpath "$.id"
# Test: Verify task exists before deletion
GET {{host}}/api/tasks/{{another_task_id}}
HTTP 200
[Asserts]
jsonpath "$.title" == "Another Task to Delete"
# Test: Delete the task
DELETE {{host}}/api/tasks/{{another_task_id}}
HTTP 204
# Test: Confirm task no longer exists
GET {{host}}/api/tasks/{{another_task_id}}
HTTP 404
[Asserts]
jsonpath "$.error" exists
# Test: Delete non-existent task
DELETE {{host}}/api/tasks/00000000-0000-0000-0000-000000000000
HTTP 404
[Asserts]
jsonpath "$.error" exists
# Test: Delete with invalid UUID format
DELETE {{host}}/api/tasks/invalid-uuid-format
HTTP 400
[Asserts]
jsonpath "$.error" exists
# Test: Multiple deletions of same task (idempotency test)
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Task for Idempotency Test"
}
HTTP 201
[Captures]
idempotent_task_id: jsonpath "$.id"
# First deletion should succeed
DELETE {{host}}/api/tasks/{{idempotent_task_id}}
HTTP 204
# Second deletion should return 404 (task already gone)
DELETE {{host}}/api/tasks/{{idempotent_task_id}}
HTTP 404
[Asserts]
jsonpath "$.error" exists

View file

@ -0,0 +1,143 @@
# Task List API Tests
# Test: Get task list (may or may not be empty)
GET {{host}}/api/tasks
HTTP 200
[Captures]
initial_count: jsonpath "$" count
[Asserts]
jsonpath "$" isCollection
# Setup: Create multiple tasks with different properties
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Test Task Alpha",
"description": "Alpha task description"
}
HTTP 201
[Captures]
alpha_task_id: jsonpath "$.id"
alpha_created_at: jsonpath "$.created_at"
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Test Task Beta"
}
HTTP 201
[Captures]
beta_task_id: jsonpath "$.id"
beta_created_at: jsonpath "$.created_at"
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Test Task Gamma",
"description": "Gamma task with description"
}
HTTP 201
[Captures]
gamma_task_id: jsonpath "$.id"
gamma_created_at: jsonpath "$.created_at"
# Test: List includes our newly created tasks
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$" isCollection
jsonpath "$" count >= {{initial_count}}
jsonpath "$[*].id" includes "{{alpha_task_id}}"
jsonpath "$[*].id" includes "{{beta_task_id}}"
jsonpath "$[*].id" includes "{{gamma_task_id}}"
jsonpath "$[*].title" includes "Test Task Alpha"
jsonpath "$[*].title" includes "Test Task Beta"
jsonpath "$[*].title" includes "Test Task Gamma"
# Test: Verify all tasks have required fields
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$[?(@.id=='{{alpha_task_id}}')].title" exists
jsonpath "$[?(@.id=='{{alpha_task_id}}')].status" exists
jsonpath "$[?(@.id=='{{alpha_task_id}}')].created_at" exists
jsonpath "$[?(@.id=='{{alpha_task_id}}')].updated_at" exists
jsonpath "$[?(@.id=='{{beta_task_id}}')].title" exists
jsonpath "$[?(@.id=='{{beta_task_id}}')].status" exists
jsonpath "$[?(@.id=='{{beta_task_id}}')].created_at" exists
jsonpath "$[?(@.id=='{{beta_task_id}}')].updated_at" exists
# Test: Verify our tasks have correct initial values
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$[?(@.id=='{{alpha_task_id}}')].title" nth 0 == "Test Task Alpha"
jsonpath "$[?(@.id=='{{alpha_task_id}}')].description" nth 0 == "Alpha task description"
jsonpath "$[?(@.id=='{{alpha_task_id}}')].status" nth 0 == "todo"
jsonpath "$[?(@.id=='{{beta_task_id}}')].title" nth 0 == "Test Task Beta"
jsonpath "$[?(@.id=='{{beta_task_id}}')].description" nth 0 == null
jsonpath "$[?(@.id=='{{beta_task_id}}')].status" nth 0 == "todo"
# Setup: Update tasks to test mixed statuses
PUT {{host}}/api/tasks/{{beta_task_id}}
Content-Type: application/json
{
"status": "done"
}
HTTP 200
PUT {{host}}/api/tasks/{{gamma_task_id}}
Content-Type: application/json
{
"status": "backlog"
}
HTTP 200
# Test: List shows updated statuses
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$[?(@.id=='{{alpha_task_id}}')].status" nth 0 == "todo"
jsonpath "$[?(@.id=='{{beta_task_id}}')].status" nth 0 == "done"
jsonpath "$[?(@.id=='{{gamma_task_id}}')].status" nth 0 == "backlog"
# Setup: Delete one task
DELETE {{host}}/api/tasks/{{alpha_task_id}}
HTTP 204
# Test: Deleted task no longer appears in list
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$[*].id" not includes "{{alpha_task_id}}"
jsonpath "$[*].id" includes "{{beta_task_id}}"
jsonpath "$[*].id" includes "{{gamma_task_id}}"
# Cleanup: Delete remaining test tasks
DELETE {{host}}/api/tasks/{{beta_task_id}}
HTTP 204
DELETE {{host}}/api/tasks/{{gamma_task_id}}
HTTP 204
# Test: Our test tasks are gone
GET {{host}}/api/tasks
HTTP 200
[Asserts]
jsonpath "$[*].id" not includes "{{beta_task_id}}"
jsonpath "$[*].id" not includes "{{gamma_task_id}}"

View file

@ -0,0 +1,154 @@
# Task Update API Tests
# Setup: Create a task to update
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Original Task",
"description": "Original description"
}
HTTP 201
[Captures]
task_id: jsonpath "$.id"
created_at: jsonpath "$.created_at"
# Test: Update task title only
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"title": "Updated Task Title"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.title" == "Updated Task Title"
jsonpath "$.description" == "Original description"
jsonpath "$.status" == "todo"
jsonpath "$.updated_at" != "{{created_at}}"
# Test: Update description only
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"description": "Updated description"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.title" == "Updated Task Title"
jsonpath "$.description" == "Updated description"
jsonpath "$.status" == "todo"
# Test: Update status to done
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"status": "done"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.status" == "done"
jsonpath "$.completed_at" exists
# Test: Update status to backlog
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"status": "backlog"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.status" == "backlog"
jsonpath "$.completed_at" == null
# Test: Update multiple fields together
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"title": "Completely Updated Task",
"description": "Completely updated description",
"status": "done"
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.title" == "Completely Updated Task"
jsonpath "$.description" == "Completely updated description"
jsonpath "$.status" == "done"
jsonpath "$.completed_at" exists
# Test: Clear description (set to null)
PUT {{host}}/api/tasks/{{task_id}}
Content-Type: application/json
{
"description": ""
}
HTTP 200
[Asserts]
jsonpath "$.id" == "{{task_id}}"
jsonpath "$.description" == ""
# Setup: Create another task for error tests
POST {{host}}/api/tasks
Content-Type: application/json
{
"title": "Task for Error Tests"
}
HTTP 201
[Captures]
error_test_task_id: jsonpath "$.id"
# Test: Update non-existent task
PUT {{host}}/api/tasks/00000000-0000-0000-0000-000000000000
Content-Type: application/json
{
"title": "This should fail"
}
HTTP 404
[Asserts]
jsonpath "$.error" exists
# Test: Update with invalid UUID format
PUT {{host}}/api/tasks/invalid-uuid-format
Content-Type: application/json
{
"title": "This should also fail"
}
HTTP 400
[Asserts]
jsonpath "$.error" exists
# Test: Update with empty title
PUT {{host}}/api/tasks/{{error_test_task_id}}
Content-Type: application/json
{
"title": ""
}
HTTP 422
[Asserts]
jsonpath "$.error" exists
# Test: Update with invalid status
PUT {{host}}/api/tasks/{{error_test_task_id}}
Content-Type: application/json
{
"status": "invalid_status"
}
HTTP 422
[Asserts]
jsonpath "$.error" exists