From ad4ee2617a13abb86f4fbda0b2dd54cfbd23a31e Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Fri, 6 Feb 2026 14:26:54 +0000 Subject: [PATCH] refactor(tests): add generic server helpers and CRUD test macros - Add `paste` dev-dependency for macro identifier concatenation - Add `create_entity()` generic helper, convert per-entity creation helpers to thin wrappers - Add `define_crud_tests!` macro generating nonexistent-GET/DELETE-404, empty-list-200, malformed-JSON-400, and missing-fields-400 tests - Apply macro to roasters, cafes, cups, and roasts API tests --- Cargo.lock | 1 + Cargo.toml | 1 + tests/server/cafes_api.rs | 89 ++--------------- tests/server/cups_api.rs | 53 ++-------- tests/server/helpers.rs | 109 +++++++------------- tests/server/main.rs | 1 + tests/server/roasters_api.rs | 104 ++------------------ tests/server/roasts_api.rs | 83 ++-------------- tests/server/test_macros.rs | 186 +++++++++++++++++++++++++++++++++++ 9 files changed, 259 insertions(+), 368 deletions(-) create mode 100644 tests/server/test_macros.rs diff --git a/Cargo.lock b/Cargo.lock index 8935256..964f98e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,7 @@ dependencies = [ "isocountry", "once_cell", "open", + "paste", "portpicker", "rand 0.8.5", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index d5bee96..2e61840 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tempfile = "3.8" once_cell = "1.19" webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } wiremock = "0.6" +paste = "1.0.15" [[test]] name = "cli" diff --git a/tests/server/cafes_api.rs b/tests/server/cafes_api.rs index 8ee2b0c..8ce5d24 100644 --- a/tests/server/cafes_api.rs +++ b/tests/server/cafes_api.rs @@ -1,6 +1,15 @@ use crate::helpers::{create_default_cafe, spawn_app_with_auth}; +use crate::test_macros::define_crud_tests; use brewlog::domain::cafes::{Cafe, NewCafe, UpdateCafe}; +define_crud_tests!( + entity: cafe, + path: "/cafes", + list_type: Cafe, + malformed_json: r#"{"name": "Test", "city": }"#, + missing_fields: r#"{"name": "Test Cafe"}"# +); + #[tokio::test] async fn creating_a_cafe_returns_a_201_for_valid_data() { let app = spawn_app_with_auth().await; @@ -96,57 +105,6 @@ async fn creating_a_cafe_requires_authentication() { assert_eq!(response.status(), 401); } -#[tokio::test] -async fn creating_a_cafe_with_missing_required_fields_returns_a_400() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .post(app.api_url("/cafes")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .header("content-type", "application/json") - .body(r#"{"name": "Test Cafe"}"#) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 400); -} - -#[tokio::test] -async fn creating_a_cafe_with_malformed_json_returns_a_400() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .post(app.api_url("/cafes")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .header("content-type", "application/json") - .body(r#"{"name": "Test", "city": }"#) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 400); -} - -#[tokio::test] -async fn listing_cafes_returns_a_200_with_empty_list() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .get(app.api_url("/cafes")) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 200); - - let cafes: Vec = response.json().await.expect("Failed to parse response"); - assert_eq!(cafes.len(), 0); -} - #[tokio::test] async fn listing_cafes_returns_a_200_with_multiple_cafes() { let app = spawn_app_with_auth().await; @@ -217,20 +175,6 @@ async fn getting_a_cafe_returns_a_200_for_valid_id() { assert_eq!(fetched.name, "Blue Bottle"); } -#[tokio::test] -async fn getting_a_nonexistent_cafe_returns_a_404() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .get(app.api_url("/cafes/999999")) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 404); -} - #[tokio::test] async fn updating_a_cafe_returns_a_200_for_valid_data() { let app = spawn_app_with_auth().await; @@ -337,18 +281,3 @@ async fn deleting_a_cafe_returns_a_204_for_valid_id() { assert_eq!(get_response.status(), 404); } - -#[tokio::test] -async fn deleting_a_nonexistent_cafe_returns_a_404() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .delete(app.api_url("/cafes/999999")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 404); -} diff --git a/tests/server/cups_api.rs b/tests/server/cups_api.rs index 1d46fd3..6bc45cb 100644 --- a/tests/server/cups_api.rs +++ b/tests/server/cups_api.rs @@ -1,9 +1,16 @@ use crate::helpers::{ create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth, }; +use crate::test_macros::define_crud_tests; use brewlog::domain::cups::{Cup, CupWithDetails, NewCup}; use brewlog::domain::ids::{CafeId, RoastId}; +define_crud_tests!( + entity: cup, + path: "/cups", + list_type: CupWithDetails +); + #[tokio::test] async fn creating_a_cup_returns_a_201_for_valid_data() { let app = spawn_app_with_auth().await; @@ -53,23 +60,6 @@ async fn creating_a_cup_requires_authentication() { assert_eq!(response.status(), 401); } -#[tokio::test] -async fn listing_cups_returns_a_200_with_empty_list() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .get(app.api_url("/cups")) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 200); - - let cups: Vec = response.json().await.expect("Failed to parse response"); - assert_eq!(cups.len(), 0); -} - #[tokio::test] async fn listing_cups_returns_a_200_with_enriched_data() { let app = spawn_app_with_auth().await; @@ -148,20 +138,6 @@ async fn getting_a_cup_returns_a_200_with_details() { assert_eq!(fetched.cafe_name, "Blue Bottle"); } -#[tokio::test] -async fn getting_a_nonexistent_cup_returns_a_404() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .get(app.api_url("/cups/999999")) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 404); -} - #[tokio::test] async fn deleting_a_cup_returns_a_204_for_valid_id() { let app = spawn_app_with_auth().await; @@ -207,18 +183,3 @@ async fn deleting_a_cup_returns_a_204_for_valid_id() { assert_eq!(get_response.status(), 404); } - -#[tokio::test] -async fn deleting_a_nonexistent_cup_returns_a_404() { - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - let response = client - .delete(app.api_url("/cups/999999")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .send() - .await - .expect("Failed to execute request"); - - assert_eq!(response.status(), 404); -} diff --git a/tests/server/helpers.rs b/tests/server/helpers.rs index a668523..8286535 100644 --- a/tests/server/helpers.rs +++ b/tests/server/helpers.rs @@ -11,6 +11,7 @@ use brewlog::domain::roasters::{NewRoaster, Roaster}; use brewlog::domain::users::NewUser; use brewlog::infrastructure::database::Database; use reqwest::Client; +use serde::{Serialize, de::DeserializeOwned}; use tokio::net::TcpListener; use tokio::task::AbortHandle; use webauthn_rs::prelude::*; @@ -188,11 +189,16 @@ async fn add_auth_to_app(mut app: TestApp) -> TestApp { app } -pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) -> Roaster { +/// Generic helper: POST a JSON payload and deserialize the response. +/// Automatically attaches the auth token if the test app has one. +pub async fn create_entity( + app: &TestApp, + path: &str, + payload: &P, +) -> R { let client = Client::new(); - let mut request = client.post(app.api_url("/roasters")).json(&payload); + let mut request = client.post(app.api_url(path)).json(payload); - // Add auth token if available if let Some(token) = &app.auth_token { request = request.bearer_auth(token); } @@ -200,34 +206,23 @@ pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) -> let response = request .send() .await - .expect("failed to create roaster via API"); + .unwrap_or_else(|e| panic!("failed to create entity at {path}: {e}")); response .json() .await - .expect("failed to deserialize roaster from response") + .unwrap_or_else(|e| panic!("failed to deserialize entity from {path}: {e}")) +} + +pub async fn create_roaster_with_payload(app: &TestApp, payload: NewRoaster) -> Roaster { + create_entity(app, "/roasters", &payload).await } pub async fn create_roast_with_payload( app: &TestApp, payload: brewlog::domain::roasts::NewRoast, ) -> brewlog::domain::roasts::Roast { - let client = Client::new(); - let mut request = client.post(app.api_url("/roasts")).json(&payload); - - if let Some(token) = &app.auth_token { - request = request.bearer_auth(token); - } - - let response = request - .send() - .await - .expect("failed to create roast via API"); - - response - .json() - .await - .expect("failed to deserialize roast from response") + create_entity(app, "/roasts", &payload).await } pub async fn create_default_roaster(app: &TestApp) -> Roaster { @@ -270,25 +265,16 @@ pub async fn create_default_bag( app: &TestApp, roast_id: brewlog::domain::ids::RoastId, ) -> brewlog::domain::bags::Bag { - let client = Client::new(); - let new_bag = brewlog::domain::bags::NewBag { - roast_id, - roast_date: Some(chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()), - amount: 250.0, - }; - - let mut request = client.post(app.api_url("/bags")).json(&new_bag); - - if let Some(token) = &app.auth_token { - request = request.bearer_auth(token); - } - - let response = request.send().await.expect("failed to create bag via API"); - - response - .json() - .await - .expect("failed to deserialize bag from response") + create_entity( + app, + "/bags", + &brewlog::domain::bags::NewBag { + roast_id, + roast_date: Some(chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()), + amount: 250.0, + }, + ) + .await } /// Asserts that the response has valid Datastar fragment headers @@ -352,31 +338,22 @@ pub async fn create_default_gear( make: &str, model: &str, ) -> brewlog::domain::gear::Gear { - let client = Client::new(); let gear_category = match category { "grinder" => brewlog::domain::gear::GearCategory::Grinder, "brewer" => brewlog::domain::gear::GearCategory::Brewer, "filter_paper" => brewlog::domain::gear::GearCategory::FilterPaper, _ => panic!("Unknown gear category: {}", category), }; - let new_gear = brewlog::domain::gear::NewGear { - category: gear_category, - make: make.to_string(), - model: model.to_string(), - }; - - let mut request = client.post(app.api_url("/gear")).json(&new_gear); - - if let Some(token) = &app.auth_token { - request = request.bearer_auth(token); - } - - let response = request.send().await.expect("failed to create gear via API"); - - response - .json() - .await - .expect("failed to deserialize gear from response") + create_entity( + app, + "/gear", + &brewlog::domain::gear::NewGear { + category: gear_category, + make: make.to_string(), + model: model.to_string(), + }, + ) + .await } pub async fn create_default_cafe(app: &TestApp) -> Cafe { @@ -395,19 +372,7 @@ pub async fn create_default_cafe(app: &TestApp) -> Cafe { } pub async fn create_cafe_with_payload(app: &TestApp, payload: NewCafe) -> Cafe { - let client = Client::new(); - let mut request = client.post(app.api_url("/cafes")).json(&payload); - - if let Some(token) = &app.auth_token { - request = request.bearer_auth(token); - } - - let response = request.send().await.expect("failed to create cafe via API"); - - response - .json() - .await - .expect("failed to deserialize cafe from response") + create_entity(app, "/cafes", &payload).await } /// Creates a session for the authenticated user and returns the raw session token diff --git a/tests/server/main.rs b/tests/server/main.rs index daffe05..d864078 100644 --- a/tests/server/main.rs +++ b/tests/server/main.rs @@ -14,4 +14,5 @@ pub mod pages; pub mod roasters_api; pub mod roasts_api; pub mod scan_api; +pub mod test_macros; pub mod timeline; diff --git a/tests/server/roasters_api.rs b/tests/server/roasters_api.rs index c7001da..d4c776b 100644 --- a/tests/server/roasters_api.rs +++ b/tests/server/roasters_api.rs @@ -1,6 +1,15 @@ use crate::helpers::spawn_app_with_auth; +use crate::test_macros::define_crud_tests; use brewlog::domain::roasters::{NewRoaster, Roaster, UpdateRoaster}; +define_crud_tests!( + entity: roaster, + path: "/roasters", + list_type: Roaster, + malformed_json: r#"{"name": "Test", "country": }"#, + missing_fields: r#"{"name": "Test Roasters"}"# +); + #[tokio::test] async fn creating_a_roaster_returns_a_201_for_valid_data() { // Arrange @@ -110,43 +119,6 @@ async fn getting_a_roaster_returns_a_200_for_valid_id() { assert_eq!(roaster.name, "Fetchable Roasters"); } -#[tokio::test] -async fn getting_a_nonexistent_roaster_returns_a_404() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .get(app.api_url("/roasters/999999")) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 404); -} - -#[tokio::test] -async fn listing_roasters_returns_a_200_with_empty_list() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .get(app.api_url("/roasters")) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 200); - - let roasters: Vec = response.json().await.expect("Failed to parse response"); - assert_eq!(roasters.len(), 0); -} - #[tokio::test] async fn listing_roasters_returns_a_200_with_multiple_roasters() { // Arrange @@ -372,24 +344,6 @@ async fn deleting_a_roaster_returns_a_204_for_valid_id() { assert_eq!(get_response.status(), 404); } -#[tokio::test] -async fn deleting_a_nonexistent_roaster_returns_a_404() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .delete(app.api_url("/roasters/999999")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 404); -} - #[tokio::test] async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization() { // Arrange @@ -416,43 +370,3 @@ async fn creating_a_roaster_with_empty_name_returns_a_201_after_normalization() // Assert - The API accepts this but normalizes to empty string assert_eq!(response.status(), 201); } - -#[tokio::test] -async fn creating_a_roaster_with_malformed_json_returns_a_400() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .post(app.api_url("/roasters")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .header("content-type", "application/json") - .body(r#"{"name": "Test", "country": }"#) // Invalid JSON - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 400); -} - -#[tokio::test] -async fn creating_a_roaster_with_missing_required_fields_returns_a_400() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - Missing 'country' field - let response = client - .post(app.api_url("/roasters")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .header("content-type", "application/json") - .body(r#"{"name": "Test Roasters"}"#) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 400); -} diff --git a/tests/server/roasts_api.rs b/tests/server/roasts_api.rs index 2c28c83..7243622 100644 --- a/tests/server/roasts_api.rs +++ b/tests/server/roasts_api.rs @@ -1,7 +1,15 @@ use crate::helpers::{create_default_roaster, create_roaster_with_name, spawn_app_with_auth}; +use crate::test_macros::define_crud_tests; use brewlog::domain::ids::RoasterId; use brewlog::domain::roasts::{NewRoast, Roast, RoastWithRoaster}; +define_crud_tests!( + entity: roast, + path: "/roasts", + list_type: RoastWithRoaster, + malformed_json: r#"{"name": "Test", "roaster_id": }"# +); + #[tokio::test] async fn creating_a_roast_returns_a_201_for_valid_data() { // Arrange @@ -159,43 +167,6 @@ async fn getting_a_roast_returns_a_200_for_valid_id() { assert_eq!(roast.name, "Kenyan AA"); } -#[tokio::test] -async fn getting_a_nonexistent_roast_returns_a_404() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .get(app.api_url("/roasts/999999")) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 404); -} - -#[tokio::test] -async fn listing_roasts_returns_a_200_with_empty_list() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .get(app.api_url("/roasts")) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 200); - - let roasts: Vec = response.json().await.expect("Failed to parse response"); - assert_eq!(roasts.len(), 0); -} - #[tokio::test] async fn listing_roasts_returns_a_200_with_multiple_roasts() { // Arrange @@ -366,24 +337,6 @@ async fn deleting_a_roast_returns_a_204_for_valid_id() { assert_eq!(get_response.status(), 404); } -#[tokio::test] -async fn deleting_a_nonexistent_roast_returns_a_404() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .delete(app.api_url("/roasts/999999")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 404); -} - #[tokio::test] async fn creating_a_roast_with_empty_name_returns_a_400() { // Arrange @@ -478,23 +431,3 @@ async fn creating_a_roast_with_empty_tasting_notes_returns_a_400() { // Assert assert_eq!(response.status(), 400); } - -#[tokio::test] -async fn creating_a_roast_with_malformed_json_returns_a_400() { - // Arrange - let app = spawn_app_with_auth().await; - let client = reqwest::Client::new(); - - // Act - let response = client - .post(app.api_url("/roasts")) - .bearer_auth(app.auth_token.as_ref().unwrap()) - .header("content-type", "application/json") - .body(r#"{"name": "Test", "roaster_id": }"#) // Invalid JSON - .send() - .await - .expect("Failed to execute request"); - - // Assert - assert_eq!(response.status(), 400); -} diff --git a/tests/server/test_macros.rs b/tests/server/test_macros.rs new file mode 100644 index 0000000..591c89a --- /dev/null +++ b/tests/server/test_macros.rs @@ -0,0 +1,186 @@ +/// Generates mechanical CRUD tests that are identical across entities. +/// Entity-specific tests remain hand-written in each file. +macro_rules! define_crud_tests { + ( + entity: $entity:ident, + path: $path:expr, + list_type: $list_type:ty + $(, malformed_json: $malformed:expr)? + $(, missing_fields: $missing:expr)? + ) => { + paste::paste! { + #[tokio::test] + async fn []() { + let app = crate::helpers::spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let response = client + .get(app.api_url(&format!("{}/999999", $path))) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(response.status(), 404); + } + + #[tokio::test] + async fn []() { + let app = crate::helpers::spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let response = client + .delete(app.api_url(&format!("{}/999999", $path))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(response.status(), 404); + } + + #[tokio::test] + async fn []() { + let app = crate::helpers::spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let response = client + .get(app.api_url($path)) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(response.status(), 200); + + let items: Vec<$list_type> = + response.json().await.expect("Failed to parse response"); + assert_eq!(items.len(), 0); + } + + $( + #[tokio::test] + async fn []() { + let app = crate::helpers::spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let response = client + .post(app.api_url($path)) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("content-type", "application/json") + .body($malformed) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(response.status(), 400); + } + )? + + $( + #[tokio::test] + async fn []() { + let app = crate::helpers::spawn_app_with_auth().await; + let client = reqwest::Client::new(); + + let response = client + .post(app.api_url($path)) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("content-type", "application/json") + .body($missing) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(response.status(), 400); + } + )? + } + }; +} + +pub(crate) use define_crud_tests; + +/// Generates list (with/without datastar header) and delete (with datastar header) +/// tests for a given entity. The setup function creates an entity and returns its +/// ID as a String (used for the delete test; ignored for list tests). +macro_rules! define_datastar_entity_tests { + ( + entity: $entity:ident, + type_param: $type_param:expr, + api_path: $api_path:expr, + list_element: $list_element:expr, + selector: $selector:expr, + setup: $setup:expr + ) => { + paste::paste! { + #[tokio::test] + async fn [<$entity _list_with_datastar_header_returns_fragment>]() { + let app = crate::helpers::spawn_app_with_auth().await; + $setup(&app).await; + let client = reqwest::Client::new(); + + let response = client + .get(format!("{}/data?type={}", app.address, $type_param)) + .header("datastar-request", "true") + .send() + .await + .expect(concat!("failed to fetch ", stringify!($entity))); + + assert_eq!(response.status(), 200); + crate::helpers::assert_datastar_headers_with_mode( + &response, + "#data-content", + "inner", + ); + + let body = response.text().await.expect("failed to read body"); + crate::helpers::assert_html_fragment(&body); + assert!( + body.contains($list_element), + "Fragment should contain the selector element" + ); + } + + #[tokio::test] + async fn [<$entity _list_without_datastar_header_returns_full_page>]() { + let app = crate::helpers::spawn_app_with_auth().await; + $setup(&app).await; + let client = reqwest::Client::new(); + + let response = client + .get(format!("{}/data?type={}", app.address, $type_param)) + .send() + .await + .expect(concat!("failed to fetch ", stringify!($entity))); + + assert_eq!(response.status(), 200); + assert!(response.headers().get("datastar-selector").is_none()); + + let body = response.text().await.expect("failed to read body"); + crate::helpers::assert_full_page(&body); + } + + #[tokio::test] + async fn [<$entity _delete_with_datastar_header_returns_fragment>]() { + let app = crate::helpers::spawn_app_with_auth().await; + let entity_id = $setup(&app).await; + let client = reqwest::Client::new(); + + let response = client + .delete(app.api_url(&format!("{}/{}", $api_path, entity_id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .header("datastar-request", "true") + .send() + .await + .expect(concat!("failed to delete ", stringify!($entity))); + + assert_eq!(response.status(), 200); + crate::helpers::assert_datastar_headers(&response, $selector); + + let body = response.text().await.expect("failed to read body"); + crate::helpers::assert_html_fragment(&body); + } + } + }; +} + +pub(crate) use define_datastar_entity_tests;