diff --git a/Cargo.lock b/Cargo.lock index 645af33..621664d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,6 +1073,7 @@ dependencies = [ "railwind", "serde", "sqlx", + "test-log", "thiserror", "tokio", "tower", @@ -2174,6 +2175,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "test-log" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6159ab4116165c99fc88cce31f99fa2c9dbe08d3691cb38da02fc3b45f357d2b" +dependencies = [ + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba277e77219e9eea169e8508942db1bf5d8a41ff2db9b20aab5a5aadc9fa25d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "thiserror" version = "1.0.52" diff --git a/Cargo.toml b/Cargo.toml index f91ffdd..70b5f31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,4 @@ walkdir = "2" [dev-dependencies] http-body-util = "0.1.0" +test-log = { version = "0.2.14", features = ["trace"], default-features = false } diff --git a/src/tests/index.rs b/src/tests/index.rs index dd0cd7a..1b5382d 100644 --- a/src/tests/index.rs +++ b/src/tests/index.rs @@ -1,20 +1,16 @@ -use axum::{ - body::Body, - http::{Request, StatusCode}, -}; +use axum::http::StatusCode; use sqlx::{Pool, Postgres}; -use tower::ServiceExt; // for `call`, `oneshot`, and `ready` -#[sqlx::test] -async fn index(pool: Pool<Postgres>) -> anyhow::Result<()> { - let app = crate::server::app(pool).await.unwrap(); +use super::util::TestApp; - let response = app - .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) - .await - .unwrap(); +#[test_log::test(sqlx::test)] +async fn index(pool: Pool<Postgres>) -> anyhow::Result<()> { + let mut app = TestApp::new(pool).await; - assert_eq!(response.status(), StatusCode::SEE_OTHER); + app.req() + .expect_status(StatusCode::SEE_OTHER) + .get("/") + .await; Ok(()) } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a8d63d9..c9176e5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,3 +2,4 @@ //! for information on why our tests are inside the `src` folder. mod index; mod users; +mod util; diff --git a/src/tests/users.rs b/src/tests/users.rs index e8a3147..9361125 100644 --- a/src/tests/users.rs +++ b/src/tests/users.rs @@ -1,24 +1,13 @@ -use axum::{ - body::Body, - http::{Request, StatusCode}, - response::IntoResponse, - Router, -}; -use http_body_util::BodyExt; +use axum::http::StatusCode; use sqlx::{Pool, Postgres}; -use tower::Service; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use visdom::Vis; -use crate::schemas::users::{CreateUser, Credentials}; // for `call`, `oneshot`, and `ready` +use crate::{ + schemas::users::{CreateUser, Credentials}, + tests::util::TestApp, +}; -#[sqlx::test] +#[test_log::test(sqlx::test)] async fn can_login(pool: Pool<Postgres>) -> anyhow::Result<()> { - tracing_subscriber::registry() - .with(EnvFilter::from_default_env()) - .with(tracing_subscriber::fmt::layer()) - .init(); - let mut tx = pool.begin().await?; crate::db::users::create_user_if_not_exists( &mut tx, @@ -30,46 +19,29 @@ async fn can_login(pool: Pool<Postgres>) -> anyhow::Result<()> { .await?; tx.commit().await?; - let mut app = crate::server::app(pool).await.unwrap(); - - let response = <Router as tower::ServiceExt<Request<Body>>>::ready(&mut app) - .await? - .call(Request::builder().uri("/login").body(Body::empty())?) - .await - .unwrap(); + let mut app = TestApp::new(pool).await; - assert_eq!(response.status(), StatusCode::OK); + let login_page = app.req().get("/login").await.dom().await; - let body = String::from_utf8(response.into_body().collect().await?.to_bytes().to_vec())?; - let dom = Vis::load(body).expect("Failed to parse HTML"); - let form = dom.find("form"); + let form = login_page.find("form"); let username = form.find("input[name='username'][type='text'][required]"); assert!(!username.is_empty()); let password = form.find("input[name='password'][type='password'][required]"); assert!(!password.is_empty()); - let creds = axum::Form(Credentials { + let creds = Credentials { username: "test".to_string(), password: "test".to_string(), - }) - .into_response() - .into_body(); + }; - let response = <Router as tower::ServiceExt<Request<Body>>>::ready(&mut app) - .await? - .call( - Request::builder() - .method("POST") - .uri("/login") - .header("Content-Type", "application/x-www-form-urlencoded") - .body(creds)?, - ) - .await - .unwrap(); + let login_response = app + .req() + .expect_status(StatusCode::SEE_OTHER) + .post("/login", &creds) + .await; - assert_eq!(response.status(), StatusCode::SEE_OTHER); - let cookie = response.headers().get("Set-Cookie").unwrap(); - dbg!(cookie); + let cookie = login_response.headers().get("Set-Cookie").unwrap(); + assert!(!cookie.is_empty()); Ok(()) } diff --git a/src/tests/util/mod.rs b/src/tests/util/mod.rs new file mode 100644 index 0000000..9dcf986 --- /dev/null +++ b/src/tests/util/mod.rs @@ -0,0 +1,23 @@ +use axum::Router; +use sqlx::{Pool, Postgres}; + +use crate::server::app; + +use self::request_builder::RequestBuilder; + +pub mod request_builder; + +pub struct TestApp { + router: Router, +} + +impl TestApp { + pub async fn new(pool: Pool<Postgres>) -> Self { + TestApp { + router: app(pool).await.unwrap(), + } + } + pub fn req(&mut self) -> RequestBuilder { + RequestBuilder::new(&mut self.router) + } +} diff --git a/src/tests/util/request_builder.rs b/src/tests/util/request_builder.rs new file mode 100644 index 0000000..24a9716 --- /dev/null +++ b/src/tests/util/request_builder.rs @@ -0,0 +1,94 @@ +use askama_axum::IntoResponse; +use axum::{ + body::Body, + http::{self, HeaderMap, Request, Response, StatusCode}, + Form, Router, +}; +use http_body_util::BodyExt; +use mime_guess::mime; +use serde::Serialize; +use tower::{Service, ServiceExt}; +use visdom::Vis; + +pub struct RequestBuilder<'app> { + router: &'app mut axum::Router, + /// This is the HTTP status that we expect the backend to return. + /// If it returns a different status, we'll panic. + expected_status: StatusCode, +} + +impl<'app> RequestBuilder<'app> { + pub fn new(router: &'app mut Router) -> Self { + RequestBuilder { + router: router, + expected_status: StatusCode::OK, + } + } + + pub fn expect_status(mut self, expected: StatusCode) -> Self { + self.expected_status = expected; + self + } + + pub async fn post<Input>(mut self, url: &str, input: &Input) -> TestResponse + where + Input: Serialize, + { + let request = Request::builder() + .method(http::Method::POST) + .uri(url) + .header( + http::header::CONTENT_TYPE, + mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(), + ) + .body(Form(input).into_response().into_body()) + .unwrap(); + + let response = ServiceExt::<Request<Body>>::ready(&mut self.router) + .await + .unwrap() + .call(request) + .await + .unwrap(); + + assert_eq!(response.status(), self.expected_status); + + TestResponse { response } + } + + pub async fn get(mut self, url: &str) -> TestResponse { + let request = Request::builder().uri(url).body(Body::empty()).unwrap(); + + let response = ServiceExt::<Request<Body>>::ready(&mut self.router) + .await + .unwrap() + .call(request) + .await + .unwrap(); + + assert_eq!(response.status(), self.expected_status); + TestResponse { response: response } + } +} + +pub struct TestResponse { + response: Response<Body>, +} + +impl TestResponse { + pub async fn dom(self) -> visdom::types::Elements<'static> { + let body = self + .response + .into_body() + .collect() + .await + .unwrap() + .to_bytes() + .to_vec(); + Vis::load(String::from_utf8(body).unwrap()).unwrap() + } + + pub fn headers(&self) -> &HeaderMap { + self.response.headers() + } +}