brewlog/tests/server/test_macros.rs
Jon Seager ad4ee2617a
refactor(tests): add generic server helpers and CRUD test macros
- Add `paste` dev-dependency for macro identifier concatenation
- Add `create_entity<P, R>()` 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
2026-02-06 14:26:54 +00:00

186 lines
7 KiB
Rust

/// 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 [<getting_a_nonexistent_ $entity _returns_a_404>]() {
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 [<deleting_a_nonexistent_ $entity _returns_a_404>]() {
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 [<listing_ $entity s_returns_a_200_with_empty_list>]() {
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 [<creating_a_ $entity _with_malformed_json_returns_a_400>]() {
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 [<creating_a_ $entity _with_missing_required_fields_returns_a_400>]() {
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;