diff --git a/backend/.gitignore b/backend/.gitignore index eb88735..34d5ca5 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,6 @@ target coverage +reports *.db *.db-shm diff --git a/backend/tests/api.rs b/backend/tests/api.rs index df2913d..857457d 100644 --- a/backend/tests/api.rs +++ b/backend/tests/api.rs @@ -1,7 +1,12 @@ +use std::path::Path; + use axum::{Router, routing::get}; use axum_test::{TestServer, TestServerConfig, Transport}; +use hurl::report::html::{Testcase, write_report}; use hurl::runner::{self, RunnerOptions, Value, VariableSet}; use hurl::util::logger::LoggerOptionsBuilder; +use hurl_core::input::Input; +use tower_http::trace::TraceLayer; async fn create_app() -> Router { use backend::database::{DatabaseConfig, create_pool}; @@ -49,11 +54,14 @@ async fn run_hurl_test(hurl_file_path: &str) { let logger_opts = LoggerOptionsBuilder::new().build(); let result = runner::run(&content, None, &runner_opts, &variables, &logger_opts).unwrap(); - assert!( - result.success, - "Hurl test failed for {}: {:?}", - hurl_file_path, result - ); + let input = Input::new(hurl_file_path); + let test_case = Testcase::from(&result, &input); + test_case + .write_html(&content, &result.entries, Path::new("reports/store"), &[]) + .expect("Failed to write html files"); + write_report(Path::new("reports/"), &vec![test_case]).expect("Failed to write report"); + + assert!(result.success, "Hurl test failed for {}", hurl_file_path); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -75,3 +83,8 @@ async fn test_update_tasks_api() { async fn test_delete_tasks_api() { run_hurl_test("./tests/api/delete_tasks.hurl").await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_projects_api() { + run_hurl_test("./tests/api/projects.hurl").await; +} diff --git a/backend/tests/api/projects.hurl b/backend/tests/api/projects.hurl new file mode 100644 index 0000000..d2d8676 --- /dev/null +++ b/backend/tests/api/projects.hurl @@ -0,0 +1,387 @@ +# Project API Tests + +# Test: Create a new project with all fields (POST /api/projects) +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Test Project Alpha", + "color": "#1976d2" +} + +HTTP 201 +[Captures] +project_id: jsonpath "$.id" +[Asserts] +jsonpath "$.title" == "Test Project Alpha" +jsonpath "$.color" == "#1976d2" +jsonpath "$.status" == "active" +jsonpath "$.folder_id" == null +jsonpath "$.sort_order" == 0 +jsonpath "$.id" exists +jsonpath "$.created_at" exists +jsonpath "$.updated_at" exists + +# Test: Create another new project +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Test Project Beta", + "color": "#ffffff" +} + +HTTP 201 +[Captures] +project_id_beta: jsonpath "$.id" +[Asserts] +jsonpath "$.title" == "Test Project Beta" +jsonpath "$.color" == "#ffffff" +jsonpath "$.status" == "active" +jsonpath "$.folder_id" == null +jsonpath "$.sort_order" == 1000 +jsonpath "$.id" exists +jsonpath "$.created_at" exists +jsonpath "$.updated_at" exists + + +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Missing Color" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + + +# Test: Create project with invalid data (missing title) +POST {{host}}/api/projects +Content-Type: application/json +{ + "color": "#ffffff" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + +# Test: Create project with invalid data (invalid color) +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Invalid Color", + "color": "#ffffffasdf" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + + +# Test: Get a specific project by ID (GET /api/projects/{id}) +GET {{host}}/api/projects/{{project_id}} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.title" == "Test Project Alpha" +jsonpath "$.color" == "#1976d2" +jsonpath "$.status" == "active" +jsonpath "$.folder_id" == null +jsonpath "$.created_at" exists +jsonpath "$.updated_at" exists + +# Test: Get a minimal project by ID +GET {{host}}/api/projects/{{project_id_beta}} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id_beta}}" +jsonpath "$.title" == "Test Project Beta" +jsonpath "$.color" == "#ffffff" +jsonpath "$.status" == "active" + +# Test: Get non-existent project +GET {{host}}/api/projects/00000000-0000-0000-0000-000000000000 + +HTTP 404 +[Asserts] +jsonpath "$.error" exists + +# Test: Get project with invalid UUID format +GET {{host}}/api/projects/invalid-uuid + +HTTP 400 +[Asserts] +jsonpath "$.error" exists + +# Test: List all projects (GET /api/projects) +GET {{host}}/api/projects + +HTTP 200 +[Captures] +initial_count: jsonpath "$" count +[Asserts] +jsonpath "$" isCollection +jsonpath "$[*].id" contains "{{project_id}}" +jsonpath "$[*].id" contains "{{project_id_beta}}" +jsonpath "$[*].title" contains "Test Project Alpha" +jsonpath "$[*].title" contains "Test Project Beta" + +# Test: Verify all projects have required fields +GET {{host}}/api/projects + +HTTP 200 +[Asserts] +jsonpath "$[?(@.id=='{{project_id}}')].title" exists +jsonpath "$[?(@.id=='{{project_id}}')].status" exists +jsonpath "$[?(@.id=='{{project_id}}')].created_at" exists +jsonpath "$[?(@.id=='{{project_id}}')].updated_at" exists +jsonpath "$[?(@.id=='{{project_id}}')].sort_order" exists + +# Test: Update project title only (PUT /api/projects/{id}) +PUT {{host}}/api/projects/{{project_id}} +Content-Type: application/json +{ + "title": "Updated Project Alpha" +} + +HTTP 200 +[Captures] +first_updated_at: jsonpath "$.updated_at" +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.title" == "Updated Project Alpha" +jsonpath "$.color" == "#1976d2" +jsonpath "$.status" == "active" + + +# Test: Update status to done +PUT {{host}}/api/projects/{{project_id}} +Content-Type: application/json +{ + "status": "done" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.status" == "done" + +# Test: Update status to backlog +PUT {{host}}/api/projects/{{project_id}} +Content-Type: application/json +{ + "status": "backlog" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.status" == "backlog" + +# Test: Update color +PUT {{host}}/api/projects/{{project_id}} +Content-Type: application/json +{ + "color": "#388e3c" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.color" == "#388e3c" + + +# Test: Update multiple fields together +PUT {{host}}/api/projects/{{project_id}} +Content-Type: application/json +{ + "title": "Completely Updated Project", + "color": "#d32f2f", + "status": "active" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == "{{project_id}}" +jsonpath "$.title" == "Completely Updated Project" +jsonpath "$.color" == "#d32f2f" +jsonpath "$.status" == "active" + + +# Test: Update non-existent project +PUT {{host}}/api/projects/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/projects/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/projects/{{project_id_beta}} +Content-Type: application/json +{ + "title": "" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + +# Test: Update with invalid status +PUT {{host}}/api/projects/{{project_id_beta}} +Content-Type: application/json +{ + "status": "invalid_status" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + +# Test: Update with invalid color format +PUT {{host}}/api/projects/{{project_id_beta}} +Content-Type: application/json +{ + "color": "invalid-color" +} + +HTTP 422 +[Asserts] +jsonpath "$.error" exists + +# Setup: Create a project to delete +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Project to Delete", + "color": "#222222" +} + +HTTP 201 +[Captures] +delete_project_id: jsonpath "$.id" + +# Test: Delete project successfully (DELETE /api/projects/{id}) +DELETE {{host}}/api/projects/{{delete_project_id}} + +HTTP 204 + +# Test: Verify project is deleted (should return 404) +GET {{host}}/api/projects/{{delete_project_id}} + +HTTP 404 +[Asserts] +jsonpath "$.error" exists + +# Setup: Create another project for additional delete tests +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Another Project to Delete", + "color": "#333333" +} + +HTTP 201 +[Captures] +another_delete_project_id: jsonpath "$.id" + +# Test: Verify project exists before deletion +GET {{host}}/api/projects/{{another_delete_project_id}} + +HTTP 200 +[Asserts] +jsonpath "$.title" == "Another Project to Delete" + +# Test: Delete the project +DELETE {{host}}/api/projects/{{another_delete_project_id}} + +HTTP 204 + +# Test: Confirm project no longer exists +GET {{host}}/api/projects/{{another_delete_project_id}} + +HTTP 404 +[Asserts] +jsonpath "$.error" exists + +# Test: Delete non-existent project +DELETE {{host}}/api/projects/00000000-0000-0000-0000-000000000000 + +HTTP 404 +[Asserts] +jsonpath "$.error" exists + +# Test: Delete with invalid UUID format +DELETE {{host}}/api/projects/invalid-uuid-format + +HTTP 400 +[Asserts] +jsonpath "$.error" exists + +# Test: Multiple deletions of same project (idempotency test) +POST {{host}}/api/projects +Content-Type: application/json +{ + "title": "Project for Idempotency Test", + "color": "#234567" +} + +HTTP 201 +[Captures] +idempotent_project_id: jsonpath "$.id" + +# First deletion should succeed +DELETE {{host}}/api/projects/{{idempotent_project_id}} + +HTTP 204 + +# Second deletion should return 404 (project already gone) +DELETE {{host}}/api/projects/{{idempotent_project_id}} + +HTTP 404 +[Asserts] +jsonpath "$.error" exists + +# Test: List shows remaining projects after deletions +GET {{host}}/api/projects + +HTTP 200 +[Asserts] +jsonpath "$[*].id" contains "{{project_id}}" +jsonpath "$[*].id" contains "{{project_id_beta}}" +jsonpath "$[*].id" not contains "{{delete_project_id}}" +jsonpath "$[*].id" not contains "{{another_delete_project_id}}" +jsonpath "$[*].id" not contains "{{idempotent_project_id}}" + +# Cleanup: Delete remaining test projects +DELETE {{host}}/api/projects/{{project_id}} + +HTTP 204 + +DELETE {{host}}/api/projects/{{project_id_beta}} + +HTTP 204 + +# Test: Verify all test projects are cleaned up +GET {{host}}/api/projects + +HTTP 200 +[Asserts] +jsonpath "$[*].id" not contains "{{project_id}}" +jsonpath "$[*].id" not contains "{{project_id_beta}}"