diff --git a/Cargo.toml b/Cargo.toml index 5a92b54..9493693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,18 @@ edition = "2021" [dependencies] axum = { version = "0.7.4", features = ["query"] } -cargo-manifest = "0.17.0" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" -serde_yml = "0.0.12" shuttle-axum = "0.49.0" shuttle-runtime = "0.49.0" tokio = "1.28.2" -toml = "0.8.19" +cargo-manifest = { version = "0.17.0", optional = true } +serde_yml = { version = "0.0.12", optional = true } +toml = { version = "0.8.19", optional = true } [dev-dependencies] axum-test = "16.4.0" + +[features] +default = [] +task1-9 = ["cargo-manifest", "serde_yml", "toml"] diff --git a/src/lib.rs b/src/lib.rs index a1ce688..11d9414 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,23 @@ mod routes; -use axum::routing::post; -pub use routes::hello_world; -use routes::{hello_bird, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, minus_one}; +#[cfg(feature = "task1-9")] +use routes::{ + hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one, + refill, MilkFactory, +}; pub fn router() -> axum::Router { + #[cfg(feature = "task1-9")] use axum::routing::get; + #[cfg(feature = "task1-9")] + use axum::routing::post; use axum::Router; - Router::new() + #[cfg(feature = "task1-9")] + let milk_factory = MilkFactory::new(); + + #[cfg(feature = "task1-9")] + return Router::new() .route("/hello_world", get(hello_world)) .route("/-1/seek", get(minus_one)) .route("/2/dest", get(ipv4_dest)) @@ -16,338 +25,11 @@ pub fn router() -> axum::Router { .route("/2/v6/dest", get(ipv6_dest)) .route("/2/v6/key", get(ipv6_key)) .route("/5/manifest", post(manifest)) - .route("/", get(hello_bird)) -} - -#[cfg(test)] -mod test { - use super::*; - use axum::http::StatusCode; - use axum_test::TestServer; - - fn test_server() -> TestServer { - TestServer::new(router()).unwrap() - } - - #[tokio::test] - async fn test_hello_world() { - let server = test_server(); - - let response = server.get("/hello_world").await; - response.assert_status_ok(); - response.assert_text("Hello, world!"); - } - - #[tokio::test] - async fn test_hello_bird() { - let server = test_server(); - - let response = server.get("/").await; - response.assert_status_ok(); - response.assert_text("Hello, bird!"); - } - - #[tokio::test] - async fn test_minus_one() { - let server = test_server(); - - let response = server.get("/-1/seek").await; - response.assert_header("location", "https://www.youtube.com/watch?v=9Gc4QTqslN4"); - response.assert_status(StatusCode::FOUND); - } - - #[tokio::test] - async fn test_ipv4_dest() { - let server = test_server(); - - let response = server.get("/2/dest?from=10.0.0.0&key=1.2.3.255").await; - response.assert_status_ok(); - response.assert_text("11.2.3.255"); - - let response = server.get("/2/dest?from=invalid-from&key=1.2.3.255").await; - response.assert_status_bad_request(); - - let response = server.get("/2/dest?from=10.0.0.0&key=invalid-key").await; - response.assert_status_bad_request(); - - let response = server - .get("/2/dest?from=128.128.33.0&key=255.0.255.33") - .await; - response.assert_status_ok(); - response.assert_text("127.128.32.33"); - } - - #[tokio::test] - async fn test_ipv4_key() { - let server = test_server(); - - let response = server.get("/2/key?from=10.0.0.0&to=11.2.3.255").await; - response.assert_status_ok(); - response.assert_text("1.2.3.255"); - - let response = server.get("/2/key?from=invalid-from&to=1.2.3.255").await; - response.assert_status_bad_request(); - - let response = server.get("/2/key?from=10.0.0.0&to=invalid-to").await; - response.assert_status_bad_request(); - - let response = server - .get("/2/key?from=128.128.33.0&to=127.128.32.33") - .await; - response.assert_status_ok(); - response.assert_text("255.0.255.33"); - } - - #[tokio::test] - async fn test_ipv6_dest() { - let server = test_server(); - - let response = server.get("/2/v6/dest?from=fe80::1&key=5:6:7::3333").await; - response.assert_status_ok(); - response.assert_text("fe85:6:7::3332"); - - let response = server - .get("/2/v6/dest?from=invalid-from&key=5:6:7::3333") - .await; - response.assert_status_bad_request(); - - let response = server.get("/2/v6/dest?from=fe80::1&key=invalid-key").await; - response.assert_status_bad_request(); - } - - #[tokio::test] - async fn test_ipv6_key() { - let server = test_server(); - - let response = server - .get("/2/v6/key?from=aaaa::aaaa&to=5555:ffff:c:0:0:c:1234:5555") - .await; - response.assert_status_ok(); - response.assert_text("ffff:ffff:c::c:1234:ffff"); - - let response = server - .get("/2/v6/dest?from=invalid-from&to=5:6:7::3333") - .await; - response.assert_status_bad_request(); - - let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await; - response.assert_status_bad_request(); - } - - #[tokio::test] - async fn test_manifest_1() { - let server = test_server(); - - let want = "Toy car: 2\nLego brick: 230"; - - let payload = r#"[package] -name = "not-a-gift-order" -authors = ["Not Santa"] -keywords = ["Christmas 2024"] - -[[package.metadata.orders]] -item = "Toy car" -quantity = 2 - -[[package.metadata.orders]] -item = "Lego brick" -quantity = 230"#; - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - - response.assert_status_ok(); - response.assert_text(want); - - let payload = r#"[package] -name = "coal-in-a-bowl" -authors = ["H4CK3R_13E7"] -keywords = ["Christmas 2024"] - -[[package.metadata.orders]] -item = "Coal" -quantity = "Hahaha get rekt""#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - response.assert_status(StatusCode::NO_CONTENT); - } - - #[tokio::test] - async fn test_manifest_2() { - let server = test_server(); - - let payload = r#"[package] -name = false -authors = ["Not Santa"] -keywords = ["Christmas 2024"]"#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - response.assert_status(StatusCode::BAD_REQUEST); - - let payload = r#"[package] -name = "not-a-gift-order" -authors = ["Not Santa"] -keywords = ["Christmas 2024"] - -[profile.release] -incremental = "stonks""#; - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - response.assert_status(StatusCode::BAD_REQUEST); - } - - #[tokio::test] - async fn test_manifest_3() { - let server = test_server(); - - let want = "Toy car: 2\nLego brick: 230"; - - let payload = r#"[package] -name = "not-a-gift-order" -authors = ["Not Santa"] -keywords = ["Christmas 2024"] - -[[package.metadata.orders]] -item = "Toy car" -quantity = 2 - -[[package.metadata.orders]] -item = "Lego brick" -quantity = 230"#; - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - - response.assert_status_ok(); - response.assert_text(want); - - let payload = r#"[package] -name = "not-a-gift-order" -authors = ["Not Santa"] - -[[package.metadata.orders]] -item = "Toy car" -quantity = 2 - -[[package.metadata.orders]] -item = "Lego brick" -quantity = 230"#; - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - - response.assert_status(StatusCode::BAD_REQUEST); - response.assert_text("Magic keyword not provided"); - - let payload = r#"[package] -name = "grass" -authors = ["A vegan cow"] -keywords = ["Moooooo"]"#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - response.assert_status(StatusCode::BAD_REQUEST); - response.assert_text("Magic keyword not provided"); - - let payload = r#"[package] -name = "grass" -authors = ["A vegan cow"] -keywords = ["Christmas 2024"]"#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/toml") - .await; - response.assert_status(StatusCode::NO_CONTENT); - } - - #[tokio::test] - async fn test_manifest_4() { - let server = test_server(); - - let payload = r#"[package] -name = "grass" -authors = ["A vegan cow"] -keywords = ["Christmas 2024"]"#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("text/html") - .await; - response.assert_status(StatusCode::UNSUPPORTED_MEDIA_TYPE); - - let want = "Toy train: 5"; - - let payload = r#"package: - name: big-chungus-sleigh - version: "2.0.24" - metadata: - orders: - - item: "Toy train" - quantity: 5 - rust-version: "1.69" - keywords: - - "Christmas 2024""#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/yaml") - .await; - - response.assert_status_ok(); - response.assert_text(want); - - let want = "Toy train: 5"; - - let payload = r#"{ - "package": { - "name": "big-chungus-sleigh", - "version": "2.0.24", - "metadata": { - "orders": [ - { - "item": "Toy train", - "quantity": 5 - } - ] - }, - "rust-version": "1.69", - "keywords": [ - "Christmas 2024" - ] - } -}"#; - - let response = server - .post("/5/manifest") - .text(payload) - .content_type("application/json") - .await; - - response.assert_status_ok(); - response.assert_text(want); - } + .route("/9/milk", post(milk)) + .route("/9/refill", post(refill)) + .with_state(milk_factory) + .route("/", get(hello_bird)); + + // #[cfg(feature="task12")] + Router::new() } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 02f96e8..318bbb4 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,7 +1,9 @@ +#![cfg(feature = "task1-9")] mod hello_bird; mod hello_world; mod minus_one; mod task_five; +mod task_nine; mod task_two; pub use hello_bird::hello_bird; @@ -12,3 +14,5 @@ pub use task_two::ipv4_dest; pub use task_two::ipv4_key; pub use task_two::ipv6_dest; pub use task_two::ipv6_key; + +pub use task_nine::{milk, refill, MilkFactory}; diff --git a/src/routes/task_nine/milk.rs b/src/routes/task_nine/milk.rs new file mode 100644 index 0000000..b4850fa --- /dev/null +++ b/src/routes/task_nine/milk.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::State, + http::{header::CONTENT_TYPE, HeaderMap, StatusCode}, + response::IntoResponse, +}; + +use super::{MilkFactory, Unit}; + +#[allow(clippy::unused_async)] +pub async fn refill(State(milk_factory): State) -> impl IntoResponse { + milk_factory.magic_refill(); + String::new().into_response() +} + +#[allow(clippy::unused_async)] +pub async fn milk( + State(milk_factory): State, + headers: HeaderMap, + payload: String, +) -> Result { + headers.get(CONTENT_TYPE).map_or_else( + || match milk_factory.withdraw() { + Ok(message) => Ok(message.to_string()), + Err(message) => Err(message.into_response()), + }, + |content_type| match content_type.to_str().unwrap() { + "application/json" => Unit::from_json(&payload).map_or_else( + |_| { + let _ = milk_factory.withdraw(); + Err((StatusCode::BAD_REQUEST, "").into_response()) + }, + |unit| match unit { + Unit::Liters(_) | Unit::Gallons(_) | Unit::Litres(_) | Unit::Pints(_) => { + match milk_factory.withdraw() { + Ok(_) => Ok(unit.json().unwrap()), + Err(message) => Err(message.into_response()), + } + } + }, + ), + _ => match milk_factory.withdraw() { + Ok(message) => Ok(message.to_string()), + Err(message) => Err(message.into_response()), + }, + }, + ) +} diff --git a/src/routes/task_nine/milk_factory.rs b/src/routes/task_nine/milk_factory.rs new file mode 100644 index 0000000..547e748 --- /dev/null +++ b/src/routes/task_nine/milk_factory.rs @@ -0,0 +1,153 @@ +use std::{ + fmt::{Display, Formatter}, + sync::{atomic::AtomicU32, Arc}, + thread, +}; + +use axum::{http::StatusCode, response::IntoResponse}; +use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; + +#[derive(Debug, Clone)] +pub struct MilkFactory { + stock: Arc, +} + +impl MilkFactory { + pub fn new() -> Self { + let mf = Self { + stock: Arc::new(AtomicU32::new(5)), + }; + mf.run(); + mf + } + + pub fn run(&self) { + thread::spawn({ + let stock = self.stock.clone(); + move || { + let mut start = std::time::Instant::now(); + loop { + if start.elapsed().as_millis() >= 990 { + let current_stock = stock.load(std::sync::atomic::Ordering::Relaxed); + if current_stock < 5 { + stock.store(current_stock + 1, std::sync::atomic::Ordering::Relaxed); + } + start = std::time::Instant::now(); + } + } + } + }); + } + + pub fn magic_refill(&self) { + self.stock.store(5, std::sync::atomic::Ordering::Relaxed); + } + + pub fn withdraw(&self) -> Result { + let current_stock = self.stock.load(std::sync::atomic::Ordering::Relaxed); + if current_stock < 1 { + return Err(MilkMessage::NoMilkAvailable); + } + self.stock + .store(current_stock - 1, std::sync::atomic::Ordering::Relaxed); + Ok(MilkMessage::Withdraw) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum MilkMessage { + Withdraw, + Refill, + NoMilkAvailable, + WithdrawUnit(Unit), +} + +#[derive(Debug, Deserialize, Copy, Clone, PartialEq)] +pub enum Unit { + #[serde(rename(deserialize = "liters", serialize = "gallons"))] + Liters(f32), + #[serde(rename(deserialize = "gallons", serialize = "liters"))] + Gallons(f32), + #[serde(rename(deserialize = "litres", serialize = "pints"))] + Litres(f32), + #[serde(rename(deserialize = "pints", serialize = "litres"))] + Pints(f32), +} + +impl Unit { + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } + + pub fn json(self) -> Result { + serde_json::to_string(&self) + } +} + +impl Serialize for Unit { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Unit", 1)?; + match self { + Self::Liters(value) => { + state.serialize_field::("gallons", &(*value * 0.264_172_06))?; + } + Self::Gallons(value) => { + state.serialize_field::("liters", &(*value * 3.785_412))?; + } + Self::Litres(value) => { + state.serialize_field::("pints", &(*value * 1.759_754))?; + } + Self::Pints(value) => { + state.serialize_field::("litres", &(*value * 0.568_261))?; + } + } + state.end() + } +} + +#[test] +fn test_unit() { + let unit = Unit::Liters(5.0); + assert_eq!( + serde_json::to_string(&unit).unwrap(), + r#"{"gallons":1.3208603}"# + ); + + let unit = Unit::from_json(serde_json::json!({"liters": 5.0}).to_string().as_str()).unwrap(); + assert_eq!(unit, Unit::Liters(5.0)); + assert_eq!(unit.json().unwrap(), r#"{"gallons":1.3208603}"#); + + let unit = Unit::from_json( + serde_json::json!({"gallons": -2.000_000_000_000_001}) + .to_string() + .as_str(), + ) + .unwrap(); + assert_eq!(unit, Unit::Gallons(-2.000_000_000_000_001)); + assert_eq!(unit.json().unwrap(), r#"{"liters":-7.570824}"#); +} + +impl Display for MilkMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Withdraw => writeln!(f, "Milk withdrawn"), + Self::Refill => writeln!(f, "Refilled milk"), + Self::NoMilkAvailable => writeln!(f, "No milk available"), + Self::WithdrawUnit(u) => write!(f, "{}", u.json().unwrap()), + } + } +} + +impl IntoResponse for MilkMessage { + fn into_response(self) -> axum::response::Response { + match self { + Self::Withdraw | Self::WithdrawUnit(_) => (StatusCode::OK, self.to_string()), + Self::Refill => (StatusCode::OK, String::new()), + Self::NoMilkAvailable => (StatusCode::TOO_MANY_REQUESTS, self.to_string()), + } + .into_response() + } +} diff --git a/src/routes/task_nine/mod.rs b/src/routes/task_nine/mod.rs new file mode 100644 index 0000000..ad450e0 --- /dev/null +++ b/src/routes/task_nine/mod.rs @@ -0,0 +1,6 @@ +mod milk; +mod milk_factory; + +pub use milk::{milk, refill}; + +pub use milk_factory::{MilkFactory, Unit}; diff --git a/tests/minus_one/main.rs b/tests/minus_one/main.rs new file mode 100644 index 0000000..dc6789a --- /dev/null +++ b/tests/minus_one/main.rs @@ -0,0 +1,38 @@ +#[cfg(all(test, feature = "task1-9"))] +mod minus_one { + + use axum::http::StatusCode; + use axum_test::TestServer; + use itsscb_shuttlings_cch24::router; + + fn test_server() -> TestServer { + TestServer::new(router()).unwrap() + } + + #[tokio::test] + async fn test_hello_world() { + let server = test_server(); + + let response = server.get("/hello_world").await; + response.assert_status_ok(); + response.assert_text("Hello, world!"); + } + + #[tokio::test] + async fn test_hello_bird() { + let server = test_server(); + + let response = server.get("/").await; + response.assert_status_ok(); + response.assert_text("Hello, bird!"); + } + + #[tokio::test] + async fn test_minus_one() { + let server = test_server(); + + let response = server.get("/-1/seek").await; + response.assert_header("location", "https://www.youtube.com/watch?v=9Gc4QTqslN4"); + response.assert_status(StatusCode::FOUND); + } +} diff --git a/tests/task_five/main.rs b/tests/task_five/main.rs new file mode 100644 index 0000000..26be06a --- /dev/null +++ b/tests/task_five/main.rs @@ -0,0 +1,121 @@ +#[cfg(all(test, feature = "task1-9"))] +mod task_five { + use std::thread; + use std::time::Duration; + + use axum::http::StatusCode; + use axum_test::TestServer; + use itsscb_shuttlings_cch24::router; + + fn test_server() -> TestServer { + TestServer::new(router()).unwrap() + } + + #[tokio::test] + async fn test_milk() { + let sever = test_server(); + + let response = sever.post("/9/milk").await; + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + let response = sever.post("/9/milk").await; + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + let response = sever.post("/9/milk").await; + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + let response = sever.post("/9/milk").await; + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + let response = sever.post("/9/milk").await; + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + let response = sever.post("/9/milk").await; + response.assert_status(StatusCode::TOO_MANY_REQUESTS); + + let sever = test_server(); + + for i in 0..=10 { + let response = sever.post("/9/milk").await; + match i { + 0..=4 | 6 | 8 | 9 => { + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + } + 5 | 7 | 10 => { + response.assert_status(StatusCode::TOO_MANY_REQUESTS); + response.assert_text("No milk available\n"); + match i { + 5 => thread::sleep(Duration::from_secs(1)), + 7 => thread::sleep(Duration::from_secs(2)), + _ => (), + } + } + _ => { + response.assert_status(StatusCode::SERVICE_UNAVAILABLE); + response.assert_text("No milk available\n"); + } + } + } + + let sever = test_server(); + + let response = sever + .post("/9/milk") + .text(r#"{"liters":5}"#) + .content_type("application/json") + .await; + response.assert_status_ok(); + response.assert_text(r#"{"gallons":1.3208603}"#); + + let sever = test_server(); + + let response = sever + .post("/9/milk") + .text(r#"{"gallons":5}"#) + .content_type("application/json") + .await; + + response.assert_status_ok(); + response.assert_text(r#"{"liters":18.927061}"#); + + let sever = test_server(); + + let response = sever + .post("/9/milk") + .text(r#"{"liters":1, "gallons":5}"#) + .content_type("application/json") + .await; + response.assert_status_bad_request(); + + let response = sever + .post("/9/milk") + .text(r#"{"litres":2}"#) + .content_type("application/json") + .await; + response.assert_status_ok(); + response.assert_text(r#"{"pints":3.519508}"#); + + let sever = test_server(); + + for i in 0..=11 { + let response = sever.post("/9/milk").await; + match i { + 0..=4 | 6..=10 => { + response.assert_status_ok(); + response.assert_text("Milk withdrawn\n"); + } + 5 | 11 => { + response.assert_status(StatusCode::TOO_MANY_REQUESTS); + response.assert_text("No milk available\n"); + let response = sever.post("/9/refill").await; + response.assert_status_ok(); + } + _ => { + response.assert_status(StatusCode::SERVICE_UNAVAILABLE); + response.assert_text("No milk available\n"); + } + } + } + } +} diff --git a/tests/task_two/main.rs b/tests/task_two/main.rs new file mode 100644 index 0000000..d2c3fc7 --- /dev/null +++ b/tests/task_two/main.rs @@ -0,0 +1,87 @@ +#[cfg(all(test, feature = "task1-9"))] +mod task_two { + use axum_test::TestServer; + use itsscb_shuttlings_cch24::router; + + fn test_server() -> TestServer { + TestServer::new(router()).unwrap() + } + + #[tokio::test] + async fn test_ipv4_dest() { + let server = test_server(); + + let response = server.get("/2/dest?from=10.0.0.0&key=1.2.3.255").await; + response.assert_status_ok(); + response.assert_text("11.2.3.255"); + + let response = server.get("/2/dest?from=invalid-from&key=1.2.3.255").await; + response.assert_status_bad_request(); + + let response = server.get("/2/dest?from=10.0.0.0&key=invalid-key").await; + response.assert_status_bad_request(); + + let response = server + .get("/2/dest?from=128.128.33.0&key=255.0.255.33") + .await; + response.assert_status_ok(); + response.assert_text("127.128.32.33"); + } + + #[tokio::test] + async fn test_ipv4_key() { + let server = test_server(); + + let response = server.get("/2/key?from=10.0.0.0&to=11.2.3.255").await; + response.assert_status_ok(); + response.assert_text("1.2.3.255"); + + let response = server.get("/2/key?from=invalid-from&to=1.2.3.255").await; + response.assert_status_bad_request(); + + let response = server.get("/2/key?from=10.0.0.0&to=invalid-to").await; + response.assert_status_bad_request(); + + let response = server + .get("/2/key?from=128.128.33.0&to=127.128.32.33") + .await; + response.assert_status_ok(); + response.assert_text("255.0.255.33"); + } + + #[tokio::test] + async fn test_ipv6_dest() { + let server = test_server(); + + let response = server.get("/2/v6/dest?from=fe80::1&key=5:6:7::3333").await; + response.assert_status_ok(); + response.assert_text("fe85:6:7::3332"); + + let response = server + .get("/2/v6/dest?from=invalid-from&key=5:6:7::3333") + .await; + response.assert_status_bad_request(); + + let response = server.get("/2/v6/dest?from=fe80::1&key=invalid-key").await; + response.assert_status_bad_request(); + } + + #[tokio::test] + async fn test_ipv6_key() { + let server = test_server(); + + let response = server + .get("/2/v6/key?from=aaaa::aaaa&to=5555:ffff:c:0:0:c:1234:5555") + .await; + response.assert_status_ok(); + response.assert_text("ffff:ffff:c::c:1234:ffff"); + + let response = server + .get("/2/v6/dest?from=invalid-from&to=5:6:7::3333") + .await; + response.assert_status_bad_request(); + + let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await; + response.assert_status_bad_request(); + } +}