test: add integration and E2E tests for edit functionality
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.
This commit is contained in:
parent
1a1b28559c
commit
931859938a
6 changed files with 819 additions and 4 deletions
|
|
@ -497,3 +497,149 @@ async fn deleting_a_brew_returns_204() {
|
|||
|
||||
assert_eq!(get_response.status(), 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_brew_returns_200_and_updates_data() {
|
||||
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 02").await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_brew = 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,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/brews"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_brew)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create brew");
|
||||
|
||||
let brew: Brew = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update = serde_json::json!({
|
||||
"water_temp": 94.0,
|
||||
"grind_setting": 22.0,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/brews/{}", brew.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update brew");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
let updated: Brew = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(updated.water_temp, 94.0);
|
||||
assert_eq!(updated.grind_setting, 22.0);
|
||||
assert_eq!(updated.coffee_weight, 15.0); // unchanged
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_brew_without_auth_returns_401() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"water_temp": 94.0,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url("/brews/1"))
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_brew_with_no_changes_returns_400() {
|
||||
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 02").await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_brew = 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,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/brews"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_brew)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create brew");
|
||||
|
||||
let brew: Brew = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update = serde_json::json!({});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/brews/{}", brew.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update brew");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_nonexistent_brew_returns_404() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"water_temp": 94.0,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url("/brews/99999"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 404);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::helpers::{
|
||||
create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
create_cafe_with_payload, 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};
|
||||
|
|
@ -188,3 +189,144 @@ async fn deleting_a_cup_returns_a_204_for_valid_id() {
|
|||
|
||||
assert_eq!(get_response.status(), 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_cup_returns_a_200_for_valid_data() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe1 = create_default_cafe(&app).await;
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe1.id,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/cups"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_cup)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create cup");
|
||||
|
||||
let cup: Cup = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
// Create a second cafe to update to
|
||||
let cafe2 = create_cafe_with_payload(
|
||||
&app,
|
||||
brewlog::domain::cafes::NewCafe {
|
||||
name: "Updated Cafe".to_string(),
|
||||
city: "Tokyo".to_string(),
|
||||
country: "JP".to_string(),
|
||||
latitude: 35.6762,
|
||||
longitude: 139.6503,
|
||||
website: None,
|
||||
created_at: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let update = serde_json::json!({
|
||||
"cafe_id": cafe2.id,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cups/{}", cup.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update cup");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
let updated: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(updated.cafe_id, cafe2.id);
|
||||
assert_eq!(updated.roast_id, roast.id); // unchanged
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_cup_without_auth_returns_401() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"cafe_id": 2,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url("/cups/1"))
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_cup_with_no_changes_returns_400() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/cups"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_cup)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create cup");
|
||||
|
||||
let cup: Cup = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update = serde_json::json!({});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cups/{}", cup.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_nonexistent_cup_returns_404() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"cafe_id": 1,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url("/cups/99999"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 404);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
use crate::helpers::{
|
||||
TestApp, assert_datastar_headers, assert_datastar_headers_with_mode, assert_full_page,
|
||||
assert_html_fragment, create_default_bag, create_default_cafe, create_default_gear,
|
||||
create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
assert_html_fragment, create_default_bag, create_default_brew, create_default_cafe,
|
||||
create_default_cup, create_default_gear, create_default_roast, create_default_roaster,
|
||||
spawn_app_with_auth,
|
||||
};
|
||||
use crate::test_macros::define_datastar_entity_tests;
|
||||
use brewlog::domain::bags::UpdateBag;
|
||||
|
|
@ -584,3 +585,210 @@ async fn cafes_create_with_datastar_header_returns_fragment() {
|
|||
"Fragment should include created cafe"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Roasters (update with/without datastar)
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn roasters_update_with_datastar_header_returns_redirect_script() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let client = Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"name": "Updated Roaster",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/roasters/{}", roaster.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update roaster");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn roasters_update_without_datastar_header_returns_json() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let client = Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"name": "JSON Updated Roaster",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/roasters/{}", roaster.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update roaster");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert!(response.headers().get("datastar-selector").is_none());
|
||||
|
||||
let updated: brewlog::domain::roasters::Roaster =
|
||||
response.json().await.expect("failed to parse JSON");
|
||||
assert_eq!(updated.name, "JSON Updated Roaster");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Roasts (update with/without datastar)
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn roasts_update_with_datastar_header_returns_redirect_script() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let client = Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"name": "Updated Roast",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/roasts/{}", roast.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update roast");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cafes (update with/without datastar)
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn cafes_update_with_datastar_header_returns_redirect_script() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
let client = Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"name": "Updated Cafe",
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cafes/{}", cafe.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update cafe");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Brews (update with/without datastar)
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn brews_update_with_datastar_header_returns_redirect_script() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let brew = create_default_brew(&app).await;
|
||||
let client = Client::new();
|
||||
|
||||
let update = serde_json::json!({
|
||||
"water_temp": 94.0,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/brews/{}", brew.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update brew");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cups (update with/without datastar)
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn cups_update_with_datastar_header_returns_redirect_script() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let cup = create_default_cup(&app).await;
|
||||
let client = Client::new();
|
||||
|
||||
// Create a new cafe to update to
|
||||
let cafe2 = crate::helpers::create_cafe_with_payload(
|
||||
&app,
|
||||
brewlog::domain::cafes::NewCafe {
|
||||
name: "Updated Test Cafe".to_string(),
|
||||
city: "Tokyo".to_string(),
|
||||
country: "JP".to_string(),
|
||||
latitude: 35.6762,
|
||||
longitude: 139.6503,
|
||||
website: None,
|
||||
created_at: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let update = serde_json::json!({
|
||||
"cafe_id": cafe2.id,
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cups/{}", cup.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update cup");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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;
|
||||
use crate::test_macros::{define_form_create_tests, define_form_update_tests};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup functions: create prerequisites + return form fields
|
||||
|
|
@ -93,6 +93,117 @@ async fn checkin_form_fields(app: &TestApp) -> Vec<(String, 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -153,6 +264,59 @@ define_form_create_tests!(
|
|||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -383,6 +383,48 @@ pub async fn create_cafe_with_payload(app: &TestApp, payload: NewCafe) -> Cafe {
|
|||
create_entity(app, "/cafes", &payload).await
|
||||
}
|
||||
|
||||
pub async fn create_default_brew(app: &TestApp) -> brewlog::domain::brews::Brew {
|
||||
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 02").await;
|
||||
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
|
||||
}
|
||||
|
||||
pub async fn create_default_cup(app: &TestApp) -> brewlog::domain::cups::Cup {
|
||||
let roaster = create_default_roaster(app).await;
|
||||
let roast = create_default_roast(app, roaster.id).await;
|
||||
let cafe = create_default_cafe(app).await;
|
||||
create_entity(
|
||||
app,
|
||||
"/cups",
|
||||
&brewlog::domain::cups::NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
created_at: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a session for the authenticated user and returns the raw session token
|
||||
/// to use as a `brewlog_session` cookie value.
|
||||
pub async fn create_session(app: &TestApp) -> String {
|
||||
|
|
@ -464,6 +506,50 @@ pub async fn post_form_datastar(
|
|||
.expect("failed to POST form with datastar")
|
||||
}
|
||||
|
||||
/// PUT a form-encoded payload with session cookie auth.
|
||||
/// Uses `redirect::Policy::none()` so tests can assert the 303 redirect itself.
|
||||
pub async fn put_form(
|
||||
app: &TestApp,
|
||||
path: &str,
|
||||
form_body: &[(impl AsRef<str> + Serialize, impl AsRef<str> + Serialize)],
|
||||
) -> reqwest::Response {
|
||||
let session_token = create_session(app).await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.put(app.api_url(path))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.form(form_body)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to PUT form")
|
||||
}
|
||||
|
||||
/// PUT a form-encoded payload with session cookie auth and Datastar headers.
|
||||
pub async fn put_form_datastar(
|
||||
app: &TestApp,
|
||||
path: &str,
|
||||
form_body: &[(impl AsRef<str> + Serialize, impl AsRef<str> + Serialize)],
|
||||
) -> reqwest::Response {
|
||||
let session_token = create_session(app).await;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.put(app.api_url(path))
|
||||
.header("Cookie", format!("brewlog_session={session_token}"))
|
||||
.header("datastar-request", "true")
|
||||
.form(form_body)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to PUT form with datastar")
|
||||
}
|
||||
|
||||
pub async fn spawn_app_with_openrouter_mock() -> TestApp {
|
||||
let mock_server = wiremock::MockServer::start().await;
|
||||
let openrouter_url = format!("{}/api/v1/chat/completions", mock_server.uri());
|
||||
|
|
|
|||
|
|
@ -253,3 +253,72 @@ macro_rules! define_form_create_tests {
|
|||
}
|
||||
|
||||
pub(crate) use define_form_create_tests;
|
||||
|
||||
/// Generates form submission tests for updating a given entity.
|
||||
/// The `setup_and_form` function creates the entity and returns
|
||||
/// `(entity_id_string, Vec<(String, String)>)`.
|
||||
///
|
||||
/// Generated tests:
|
||||
/// - `updating_{entity}_via_form_returns_redirect` — PUT form → 303 redirect
|
||||
/// - `updating_{entity}_via_form_with_datastar_returns_redirect_script` — PUT form + datastar → 200
|
||||
macro_rules! define_form_update_tests {
|
||||
(
|
||||
entity: $entity:ident,
|
||||
api_path: $api_path:expr,
|
||||
redirect_prefix: $redirect_prefix:expr,
|
||||
setup_and_form: $setup_fn:expr
|
||||
) => {
|
||||
paste::paste! {
|
||||
#[tokio::test]
|
||||
async fn [<updating_ $entity _via_form_returns_redirect>]() {
|
||||
let app = crate::helpers::spawn_app_with_auth().await;
|
||||
let (entity_id, form_fields) = $setup_fn(&app).await;
|
||||
let path = format!("{}/{}", $api_path, entity_id);
|
||||
let response = crate::helpers::put_form(&app, &path, &form_fields).await;
|
||||
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
303,
|
||||
"expected 303 See Other, got {}",
|
||||
response.status()
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.expect("missing Location header");
|
||||
assert!(
|
||||
location.starts_with($redirect_prefix),
|
||||
"expected redirect to start with '{}', got '{}'",
|
||||
$redirect_prefix,
|
||||
location
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn [<updating_ $entity _via_form_with_datastar_returns_redirect_script>]() {
|
||||
let app = crate::helpers::spawn_app_with_auth().await;
|
||||
let (entity_id, form_fields) = $setup_fn(&app).await;
|
||||
let path = format!("{}/{}", $api_path, entity_id);
|
||||
let response =
|
||||
crate::helpers::put_form_datastar(&app, &path, &form_fields).await;
|
||||
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
200,
|
||||
"expected 200 OK for datastar form update, got {}",
|
||||
response.status()
|
||||
);
|
||||
crate::helpers::assert_datastar_headers_with_mode(&response, "body", "append");
|
||||
|
||||
let body = response.text().await.expect("failed to read body");
|
||||
assert!(
|
||||
body.contains("window.location"),
|
||||
"Expected redirect script in body"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use define_form_update_tests;
|
||||
|
|
|
|||
Loading…
Reference in a new issue