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
This commit is contained in:
parent
f31f87d4e1
commit
ad4ee2617a
9 changed files with 259 additions and 368 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -404,6 +404,7 @@ dependencies = [
|
|||
"isocountry",
|
||||
"once_cell",
|
||||
"open",
|
||||
"paste",
|
||||
"portpicker",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<Cafe> = 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CupWithDetails> = 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<P: Serialize, R: DeserializeOwned>(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Roaster> = 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RoastWithRoaster> = 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);
|
||||
}
|
||||
|
|
|
|||
186
tests/server/test_macros.rs
Normal file
186
tests/server/test_macros.rs
Normal file
|
|
@ -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 [<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;
|
||||
Loading…
Reference in a new issue