Create a plan for implementing projects and also create the backend API. (#19)
Creates projects to organize tasks under. (Note the migration also contains the tables for creating folders for projects as well because adding foreign keys is a PITA in sqlite apparently). Reviewed-on: #19 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
parent
69f4a6f1ca
commit
4552c347c6
17 changed files with 957 additions and 12 deletions
|
|
@ -1,9 +1,15 @@
|
|||
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 {
|
||||
tracing_subscriber::fmt::try_init();
|
||||
use backend::database::{DatabaseConfig, create_pool};
|
||||
use backend::services;
|
||||
|
||||
|
|
@ -24,6 +30,8 @@ async fn create_app() -> Router {
|
|||
Router::new()
|
||||
.route("/health", get(health))
|
||||
.nest("/api/tasks", services::create_task_router())
|
||||
.nest("/api/projects", services::create_project_router())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(pool)
|
||||
}
|
||||
|
||||
|
|
@ -49,11 +57,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 +86,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;
|
||||
}
|
||||
|
|
|
|||
387
backend/tests/api/projects.hurl
Normal file
387
backend/tests/api/projects.hurl
Normal file
|
|
@ -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}}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue