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:
Jon Seager 2026-02-10 18:36:21 +00:00
parent 1a1b28559c
commit 931859938a
No known key found for this signature in database
6 changed files with 819 additions and 4 deletions

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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"
);
}

View file

@ -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
// ---------------------------------------------------------------------------

View file

@ -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());

View file

@ -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;