diff --git a/Cargo.lock b/Cargo.lock index 06f2ed7..3e52103 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn", + "syn 2.0.101", ] [[package]] @@ -184,7 +184,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -195,7 +195,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -306,7 +306,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -351,6 +351,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "2.9.0" @@ -707,7 +722,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -873,6 +888,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -939,7 +964,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -1149,6 +1174,20 @@ dependencies = [ "windows-link", ] +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "http" version = "1.3.1" @@ -1601,6 +1640,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maik" version = "0.2.0" @@ -1615,6 +1660,32 @@ dependencies = [ "wg", ] +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1689,6 +1760,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "8.0.0" @@ -1805,7 +1882,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -1944,7 +2021,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -1958,6 +2035,63 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1975,7 +2109,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -2068,6 +2202,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2468,6 +2608,17 @@ dependencies = [ "libc", ] +[[package]] +name = "select" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5910c1d91bd7e6e178c0f8eb9e4ad01f814064b4a1c0ae3c906224a3cbf12879" +dependencies = [ + "bit-set", + "html5ever", + "markup5ever_rcdom", +] + [[package]] name = "serde" version = "1.0.219" @@ -2485,7 +2636,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -2518,7 +2669,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -2615,6 +2766,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2721,7 +2884,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.101", ] [[package]] @@ -2744,7 +2907,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.101", "tempfile", "tokio", "url", @@ -2878,6 +3041,31 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -2895,6 +3083,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.101" @@ -2923,7 +3122,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -2971,6 +3170,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "test-log" version = "0.2.17" @@ -2989,7 +3199,7 @@ checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3009,7 +3219,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3113,7 +3323,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3325,7 +3535,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3461,6 +3671,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3547,7 +3763,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -3582,7 +3798,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3681,7 +3897,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3692,7 +3908,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -3985,6 +4201,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml5ever" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "yaml-rust2" version = "0.10.1" @@ -4025,7 +4252,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", "synstructure", ] @@ -4051,8 +4278,10 @@ dependencies = [ "rand 0.9.1", "regex", "reqwest", + "select", "serde", "serde_json", + "serde_urlencoded", "sqlx", "tar", "test-log", @@ -4085,7 +4314,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] [[package]] @@ -4105,7 +4334,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", "synstructure", ] @@ -4145,5 +4374,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.101", ] diff --git a/Cargo.toml b/Cargo.toml index 66bf9a6..34b9865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ http-body-util = "0.1.3" maik = "0.2.0" regex = "1.11.1" reqwest = { version = "0.12", features = ["cookies"] } +select = "0.6.1" +serde_urlencoded = "0.7.1" test-log = { version = "0.2.12", default-features = false, features = ["trace"] } [profile.dev.package.sqlx-macros] diff --git a/src/server/routes/auth/mod.rs b/src/server/routes/auth/mod.rs index 692106c..93a7357 100644 --- a/src/server/routes/auth/mod.rs +++ b/src/server/routes/auth/mod.rs @@ -63,6 +63,7 @@ impl fmt::Debug for SignupForm { f.debug_struct("SignupForm") .field("email", &self.email) .field("password", &"REDACTED") + .field("csrf-token", &self.csrf_token) .finish() } } diff --git a/tests/auth.rs b/tests/auth.rs index 5cbd29e..754c987 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -7,13 +7,13 @@ use test_log::test as traced; #[traced(tokio::test)] async fn login_succeeds_with_valid_credentials() -> Result<()> { let server = TestServer::spawn().await; - let client = reqwest::Client::builder().cookie_store(true).build()?; + let mut client = server.browser_client(); + client.get_csrf_token().await?; // Signup let resp = client - .post(server.url("/auth/signup")) - .header("Content-Type", "application/x-www-form-urlencoded") - .body("email=admin&password=hunter2") + .post("/auth/signup") + .csrf_form(&[("email", "admin"), ("password", "hunter2")]) .send() .await?; @@ -21,16 +21,19 @@ async fn login_succeeds_with_valid_credentials() -> Result<()> { // Login let resp = client - .post(server.url("/auth/login")) - .header("Content-Type", "application/x-www-form-urlencoded") - .body("email=admin&password=hunter2") + .post("/auth/login") + .csrf_form(&[("email", "admin"), ("password", "hunter2")]) .send() .await?; assert_eq!(resp.status(), 200, "login succeeds"); // Logout - let resp = client.post(server.url("/auth/logout")).send().await?; + let resp = client + .post("/auth/logout") + .csrf_form::<[(&str, &str)]>(&[]) + .send() + .await?; assert_eq!(resp.status(), 200, "logout succeeds"); @@ -40,13 +43,13 @@ async fn login_succeeds_with_valid_credentials() -> Result<()> { #[traced(tokio::test)] async fn login_fails_with_invalid_credentials() -> Result<()> { let server = TestServer::spawn().await; - let client = reqwest::Client::new(); + let mut client = server.browser_client(); + client.get_csrf_token().await?; // Signup let resp = client - .post(server.url("/auth/signup")) - .header("Content-Type", "application/x-www-form-urlencoded") - .body("email=admin&password=hunter2") + .post("/auth/signup") + .csrf_form(&[("email", "admin"), ("password", "hunter2")]) .send() .await?; @@ -54,20 +57,15 @@ async fn login_fails_with_invalid_credentials() -> Result<()> { // Login let resp = client - .post(server.url("/auth/login")) - .header("Content-Type", "application/x-www-form-urlencoded") - .body("email=admin&password=hunter3") + .post("/auth/login") + .csrf_form(&[("email", "admin"), ("password", "hunter3")]) .send() .await?; - assert_ne!( + assert_eq!( resp.status(), - 200, - "login suceeded with invalid credentials" - ); - assert!( - resp.headers().get("Set-Cookie").is_none(), - "auth cookie was set for invalid crednetials" + 401, + "login didn't reject invalid credentials" ); server.shutdown().await @@ -76,11 +74,12 @@ async fn login_fails_with_invalid_credentials() -> Result<()> { #[traced(tokio::test)] async fn login_rejects_missing_credentials() -> Result<()> { let server = TestServer::spawn().await; - let client = reqwest::Client::new(); + let mut client = server.browser_client(); + client.get_csrf_token().await?; + let resp = client - .post(server.url("/auth/login")) - .header("Content-Type", "application/x-www-form-urlencoded") - .body("email=&password=") + .post("/auth/login") + .csrf_form(&[("email", ""), ("password", "")]) .send() .await?; @@ -89,10 +88,6 @@ async fn login_rejects_missing_credentials() -> Result<()> { 401, "login didn't reject missing credentials" ); - assert!( - resp.headers().get("Set-Cookie").is_none(), - "auth cookie was set for missing crednetials" - ); server.shutdown().await } diff --git a/tests/fixture/mod.rs b/tests/fixture/mod.rs index d09ade5..118a9a0 100644 --- a/tests/fixture/mod.rs +++ b/tests/fixture/mod.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; +use axum::http; use bollard::query_parameters::{CreateImageOptionsBuilder, ListContainersOptionsBuilder}; use bollard::secret::{ ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health, @@ -8,6 +9,11 @@ use bollard::Docker; use futures_util::{FutureExt, StreamExt as _}; use rand::distr::slice::Choose; use rand::{rng, Rng}; +use reqwest::header::{HeaderName, HeaderValue}; +use reqwest::Body; +use select::document::Document; +use select::predicate::Attr; +use serde::Serialize; use sqlx::migrate::MigrateDatabase; use sqlx::{Connection, PgConnection}; use std::net::SocketAddr; @@ -74,6 +80,11 @@ impl TestServer { format!("http://{}{path}", self.addr) } + /// Construct a browser-like client. + pub fn browser_client(&self) -> BrowserClient { + BrowserClient::new(self.addr) + } + /// Request a graceful shutdown and return imideately. pub async fn start_shutdown(&self) -> Result<()> { self.server_task_handle.abort(); @@ -90,6 +101,115 @@ impl TestServer { } } +pub struct BrowserClient { + inner: reqwest::Client, + server: SocketAddr, + csrf_token: Option, +} + +impl BrowserClient { + fn new(server: SocketAddr) -> BrowserClient { + let inner = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("build reqwest client"); + BrowserClient { + inner, + server, + csrf_token: None, + } + } + + /// format a URL for the given path + fn url(&self, path: &str) -> String { + println!("url: http://{}{path}", self.server); + format!("http://{}{path}", self.server) + } + + pub fn post>(&self, url: U) -> RequestBuilder { + RequestBuilder::new( + self.inner.post(self.url(url.as_ref())), + self.csrf_token.clone(), + ) + } + + pub async fn get_csrf_token(&mut self) -> Result { + // Any page with a CSRF-cookie will do, tokens are valid for a session. + let resp = self.inner.get(&self.url("/auth/signup")).send().await?; + + assert_eq!(resp.status(), 200, "get CSRF page"); + + let signup_page = resp.text().await.context("recv signup page body")?; + + let document = Document::from(signup_page.as_str()); + let csrf_node = document + .find(Attr("name", "csrf-token")) + .next() + .context("find csrf node")?; + let csrf_token = csrf_node + .attr("value") + .context("get csrf token from node")? + .to_string(); + + self.csrf_token = Some(csrf_token.clone()); + + Ok(csrf_token) + } +} + +pub struct RequestBuilder { + inner: reqwest::RequestBuilder, + csrf_token: Option, +} + +impl RequestBuilder { + fn new(request: reqwest::RequestBuilder, csrf_token: Option) -> RequestBuilder { + RequestBuilder { + inner: request, + csrf_token, + } + } + + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + self.inner = self.inner.header(key, value); + self + } + + pub fn form(mut self, form: &T) -> RequestBuilder { + self.inner = self.inner.form(form); + self + } + + pub fn csrf_form(mut self, form: &T) -> RequestBuilder { + let body_str = serde_urlencoded::to_string(form).unwrap(); + let full_body_str = format!( + "{body_str}&csrf-token={}", + self.csrf_token.as_ref().unwrap() + ); + + self.inner = self + .inner + .body(full_body_str) + .header("Content-Type", "application/x-www-form-urlencoded"); + self + } + + pub fn body>(mut self, body: T) -> Self { + self.inner = self.inner.body(body); + self + } + + pub async fn send(self) -> Result { + self.inner.send().await + } +} + const TEST_DB_IMAGE_NAME: &str = "postgres"; const TEST_DB_SUPERUSER: &str = "postgres"; const TEST_DB_SUPERUSER_PASS: &str = "password";