Add form-based PUT update tests via define_form_update_tests macro for all 7 entities. Add hand-written datastar update tests for roasters, roasts, cafes, brews, and cups. Add brew/cup update API tests (CRUD + auth). Add put_form, put_form_datastar, create_default_brew, create_default_cup test helpers.
389 lines
12 KiB
Rust
389 lines
12 KiB
Rust
use crate::helpers::{
|
|
TestApp, create_default_bag, create_default_cafe, create_default_gear, create_default_roast,
|
|
create_default_roaster, post_form, spawn_app_with_auth,
|
|
};
|
|
use crate::test_macros::{define_form_create_tests, define_form_update_tests};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Setup functions: create prerequisites + return form fields
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async fn roaster_form_fields(_app: &TestApp) -> Vec<(String, String)> {
|
|
vec![
|
|
("name".into(), "Form Roasters".into()),
|
|
("country".into(), "UK".into()),
|
|
]
|
|
}
|
|
|
|
async fn roast_form_fields(app: &TestApp) -> Vec<(String, String)> {
|
|
let roaster = create_default_roaster(app).await;
|
|
vec![
|
|
("roaster_id".into(), roaster.id.into_inner().to_string()),
|
|
("name".into(), "Form Roast".into()),
|
|
("origin".into(), "Ethiopia".into()),
|
|
("region".into(), "Yirgacheffe".into()),
|
|
("producer".into(), "Coop".into()),
|
|
("tasting_notes".into(), "Blueberry, Jasmine".into()),
|
|
("process".into(), "Washed".into()),
|
|
]
|
|
}
|
|
|
|
async fn bag_form_fields(app: &TestApp) -> Vec<(String, String)> {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
vec![
|
|
("roast_id".into(), roast.id.into_inner().to_string()),
|
|
("roast_date".into(), "2023-06-15".into()),
|
|
("amount".into(), "250".into()),
|
|
]
|
|
}
|
|
|
|
async fn brew_form_fields(app: &TestApp) -> Vec<(String, String)> {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let bag = create_default_bag(app, roast.id).await;
|
|
let grinder = create_default_gear(app, "grinder", "Comandante", "C40 MK4").await;
|
|
let brewer = create_default_gear(app, "brewer", "Hario", "V60").await;
|
|
vec![
|
|
("bag_id".into(), bag.id.into_inner().to_string()),
|
|
("coffee_weight".into(), "15.0".into()),
|
|
("grinder_id".into(), grinder.id.into_inner().to_string()),
|
|
("grind_setting".into(), "24.0".into()),
|
|
("brewer_id".into(), brewer.id.into_inner().to_string()),
|
|
("water_volume".into(), "250".into()),
|
|
("water_temp".into(), "96.0".into()),
|
|
]
|
|
}
|
|
|
|
async fn cup_form_fields(app: &TestApp) -> Vec<(String, String)> {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let cafe = create_default_cafe(app).await;
|
|
vec![
|
|
("roast_id".into(), roast.id.into_inner().to_string()),
|
|
("cafe_id".into(), cafe.id.into_inner().to_string()),
|
|
]
|
|
}
|
|
|
|
async fn gear_form_fields(_app: &TestApp) -> Vec<(String, String)> {
|
|
vec![
|
|
("category".into(), "grinder".into()),
|
|
("make".into(), "Comandante".into()),
|
|
("model".into(), "C40 MK4".into()),
|
|
]
|
|
}
|
|
|
|
async fn cafe_form_fields(_app: &TestApp) -> Vec<(String, String)> {
|
|
vec![
|
|
("name".into(), "Form Cafe".into()),
|
|
("city".into(), "London".into()),
|
|
("country".into(), "UK".into()),
|
|
("latitude".into(), "51.5074".into()),
|
|
("longitude".into(), "-0.1278".into()),
|
|
]
|
|
}
|
|
|
|
async fn checkin_form_fields(app: &TestApp) -> Vec<(String, String)> {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let cafe = create_default_cafe(app).await;
|
|
vec![
|
|
("cafe_id".into(), cafe.id.into_inner().to_string()),
|
|
("roast_id".into(), roast.id.into_inner().to_string()),
|
|
]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Update setup functions: create entity + return (id, update form fields)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async fn roaster_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let roaster = create_default_roaster(app).await;
|
|
(
|
|
roaster.id.into_inner().to_string(),
|
|
vec![("name".into(), "Updated Roasters".into())],
|
|
)
|
|
}
|
|
|
|
async fn roast_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
(
|
|
roast.id.into_inner().to_string(),
|
|
vec![("name".into(), "Updated Roast Name".into())],
|
|
)
|
|
}
|
|
|
|
async fn bag_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let bag = create_default_bag(app, roast.id).await;
|
|
(
|
|
bag.id.into_inner().to_string(),
|
|
vec![("amount".into(), "300".into())],
|
|
)
|
|
}
|
|
|
|
async fn brew_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let bag = create_default_bag(app, roast.id).await;
|
|
let grinder = create_default_gear(app, "grinder", "Comandante", "C40 MK4").await;
|
|
let brewer = create_default_gear(app, "brewer", "Hario", "V60").await;
|
|
let brew: brewlog::domain::brews::Brew = crate::helpers::create_entity(
|
|
app,
|
|
"/brews",
|
|
&brewlog::domain::brews::NewBrew {
|
|
bag_id: bag.id,
|
|
coffee_weight: 15.0,
|
|
grinder_id: grinder.id,
|
|
grind_setting: 24.0,
|
|
brewer_id: brewer.id,
|
|
filter_paper_id: None,
|
|
water_volume: 250,
|
|
water_temp: 92.0,
|
|
quick_notes: Vec::new(),
|
|
brew_time: None,
|
|
created_at: None,
|
|
},
|
|
)
|
|
.await;
|
|
(
|
|
brew.id.into_inner().to_string(),
|
|
vec![("water_temp".into(), "94.0".into())],
|
|
)
|
|
}
|
|
|
|
async fn cup_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let roaster = create_default_roaster(app).await;
|
|
let roast = create_default_roast(app, roaster.id).await;
|
|
let cafe = create_default_cafe(app).await;
|
|
let cup: brewlog::domain::cups::Cup = crate::helpers::create_entity(
|
|
app,
|
|
"/cups",
|
|
&brewlog::domain::cups::NewCup {
|
|
roast_id: roast.id,
|
|
cafe_id: cafe.id,
|
|
created_at: None,
|
|
},
|
|
)
|
|
.await;
|
|
// Update the cafe to a different one
|
|
let cafe2 = crate::helpers::create_cafe_with_payload(
|
|
app,
|
|
brewlog::domain::cafes::NewCafe {
|
|
name: "Form Update Cafe".to_string(),
|
|
city: "London".to_string(),
|
|
country: "UK".to_string(),
|
|
latitude: 51.5074,
|
|
longitude: -0.1278,
|
|
website: None,
|
|
created_at: None,
|
|
},
|
|
)
|
|
.await;
|
|
(
|
|
cup.id.into_inner().to_string(),
|
|
vec![("cafe_id".into(), cafe2.id.into_inner().to_string())],
|
|
)
|
|
}
|
|
|
|
async fn gear_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let gear = create_default_gear(app, "grinder", "Original", "Model").await;
|
|
(
|
|
gear.id.into_inner().to_string(),
|
|
vec![("make".into(), "Updated Make".into())],
|
|
)
|
|
}
|
|
|
|
async fn cafe_update_form(app: &TestApp) -> (String, Vec<(String, String)>) {
|
|
let cafe = create_default_cafe(app).await;
|
|
(
|
|
cafe.id.into_inner().to_string(),
|
|
vec![("name".into(), "Updated Cafe".into())],
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Macro-generated tests: form create → 303 redirect + datastar variant
|
|
// ---------------------------------------------------------------------------
|
|
|
|
define_form_create_tests!(
|
|
entity: roaster,
|
|
api_path: "/roasters",
|
|
redirect_prefix: "/roasters/",
|
|
setup_and_form: roaster_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: roast,
|
|
api_path: "/roasts",
|
|
redirect_prefix: "/roasters/",
|
|
setup_and_form: roast_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: bag,
|
|
api_path: "/bags",
|
|
redirect_prefix: "/bags/",
|
|
setup_and_form: bag_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: brew,
|
|
api_path: "/brews",
|
|
redirect_prefix: "/brews/",
|
|
setup_and_form: brew_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: cup,
|
|
api_path: "/cups",
|
|
redirect_prefix: "/cups/",
|
|
setup_and_form: cup_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: gear,
|
|
api_path: "/gear",
|
|
redirect_prefix: "/gear/",
|
|
setup_and_form: gear_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: cafe,
|
|
api_path: "/cafes",
|
|
redirect_prefix: "/cafes/",
|
|
setup_and_form: cafe_form_fields
|
|
);
|
|
|
|
define_form_create_tests!(
|
|
entity: checkin,
|
|
api_path: "/check-in",
|
|
redirect_prefix: "/cups/",
|
|
setup_and_form: checkin_form_fields
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Macro-generated tests: form update → 303 redirect + datastar variant
|
|
// ---------------------------------------------------------------------------
|
|
|
|
define_form_update_tests!(
|
|
entity: roaster,
|
|
api_path: "/roasters",
|
|
redirect_prefix: "/roasters/",
|
|
setup_and_form: roaster_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: roast,
|
|
api_path: "/roasts",
|
|
redirect_prefix: "/roasters/",
|
|
setup_and_form: roast_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: bag,
|
|
api_path: "/bags",
|
|
redirect_prefix: "/bags/",
|
|
setup_and_form: bag_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: brew,
|
|
api_path: "/brews",
|
|
redirect_prefix: "/brews/",
|
|
setup_and_form: brew_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: cup,
|
|
api_path: "/cups",
|
|
redirect_prefix: "/cups/",
|
|
setup_and_form: cup_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: gear,
|
|
api_path: "/gear",
|
|
redirect_prefix: "/gear/",
|
|
setup_and_form: gear_update_form
|
|
);
|
|
|
|
define_form_update_tests!(
|
|
entity: cafe,
|
|
api_path: "/cafes",
|
|
redirect_prefix: "/cafes/",
|
|
setup_and_form: cafe_update_form
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hand-written tests: form-specific parsing edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn brew_form_with_empty_filter_paper_and_quick_notes() {
|
|
let app = spawn_app_with_auth().await;
|
|
let roaster = create_default_roaster(&app).await;
|
|
let roast = create_default_roast(&app, roaster.id).await;
|
|
let bag = create_default_bag(&app, roast.id).await;
|
|
let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
|
let brewer = create_default_gear(&app, "brewer", "Hario", "V60").await;
|
|
|
|
let form_fields = vec![
|
|
("bag_id", bag.id.into_inner().to_string()),
|
|
("coffee_weight", "15.0".into()),
|
|
("grinder_id", grinder.id.into_inner().to_string()),
|
|
("grind_setting", "24.0".into()),
|
|
("brewer_id", brewer.id.into_inner().to_string()),
|
|
("water_volume", "250".into()),
|
|
("water_temp", "96.0".into()),
|
|
("filter_paper_id", String::new()), // empty string → None
|
|
("quick_notes", "good,too-fast".into()), // comma-separated → vec
|
|
];
|
|
|
|
let response = post_form(&app, "/brews", &form_fields).await;
|
|
assert_eq!(response.status(), 303);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn roast_form_with_comma_separated_tasting_notes() {
|
|
let app = spawn_app_with_auth().await;
|
|
let roaster = create_default_roaster(&app).await;
|
|
|
|
let form_fields = vec![
|
|
("roaster_id", roaster.id.into_inner().to_string()),
|
|
("name", "Tasting Test".into()),
|
|
("origin", "Kenya".into()),
|
|
("region", "Nyeri".into()),
|
|
("producer", "Smallholder".into()),
|
|
("tasting_notes", "Blackcurrant, Tomato, Brown Sugar".into()),
|
|
("process", "Natural".into()),
|
|
];
|
|
|
|
let response = post_form(&app, "/roasts", &form_fields).await;
|
|
assert_eq!(response.status(), 303);
|
|
|
|
let location = response
|
|
.headers()
|
|
.get("location")
|
|
.and_then(|v| v.to_str().ok())
|
|
.expect("missing Location header");
|
|
assert!(location.starts_with("/roasters/"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bag_form_with_empty_roast_date() {
|
|
let app = spawn_app_with_auth().await;
|
|
let roaster = create_default_roaster(&app).await;
|
|
let roast = create_default_roast(&app, roaster.id).await;
|
|
|
|
let form_fields = vec![
|
|
("roast_id", roast.id.into_inner().to_string()),
|
|
("roast_date", String::new()), // empty string → None
|
|
("amount", "250".into()),
|
|
];
|
|
|
|
let response = post_form(&app, "/bags", &form_fields).await;
|
|
assert_eq!(response.status(), 303);
|
|
}
|