Compare commits
No commits in common. "8936222367fc9f11dd04fdc92fc0b8e8d04d7519" and "d32f6be813fe0dd938d662beabbf5fd556647d6a" have entirely different histories.
8936222367
...
d32f6be813
5 changed files with 3 additions and 457 deletions
|
|
@ -13,7 +13,6 @@ pub enum AppError {
|
||||||
InternalError(anyhow::Error),
|
InternalError(anyhow::Error),
|
||||||
JsonExtractError(JsonRejection),
|
JsonExtractError(JsonRejection),
|
||||||
PathError(PathRejection),
|
PathError(PathRejection),
|
||||||
Unprocessable(String),
|
|
||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,11 +36,6 @@ impl IntoResponse for AppError {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
Self::Unprocessable(msg) => (
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
Json(ErrorJson { error: msg }),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
Self::PathError(rej) => (
|
Self::PathError(rej) => (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
Json(ErrorJson {
|
Json(ErrorJson {
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,14 @@ use serde::Deserialize;
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{TaskModel, TaskStatus};
|
use crate::models::TaskModel;
|
||||||
|
|
||||||
use super::AppError;
|
use super::AppError;
|
||||||
|
|
||||||
pub fn create_task_router() -> Router<Pool<Sqlite>> {
|
pub fn create_task_router() -> Router<Pool<Sqlite>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", post(create_task).get(list_tasks))
|
.route("/", post(create_task))
|
||||||
.route(
|
.route("/{task_id}", get(get_task))
|
||||||
"/{task_id}",
|
|
||||||
get(get_task).put(update_task).delete(delete_task),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -40,14 +37,6 @@ pub async fn create_task(
|
||||||
Ok((StatusCode::CREATED, Json(model)))
|
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(
|
pub async fn get_task(
|
||||||
State(pool): State<Pool<Sqlite>>,
|
State(pool): State<Pool<Sqlite>>,
|
||||||
WithRejection(Path(task_id), _): WithRejection<Path<Uuid>, AppError>,
|
WithRejection(Path(task_id), _): WithRejection<Path<Uuid>, AppError>,
|
||||||
|
|
@ -56,51 +45,3 @@ pub async fn get_task(
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(model)))
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
# 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}}"
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue