From f7036a18a0d1767e2d732bffe79ced752090665a Mon Sep 17 00:00:00 2001 From: Martin Berg Alstad <git@martials.no> Date: Tue, 17 Dec 2024 17:05:43 +0000 Subject: [PATCH] Tests --- .idea/dataSources.xml | 2 +- Cargo.lock | 19 +++-- Cargo.toml | 1 + src/database.rs | 35 ++------ src/error.rs | 22 ++++- src/main.rs | 11 ++- src/routes/auth.rs | 68 ++++++++++----- src/services/reservation_service.rs | 93 ++++++++++----------- src/services/room_service.rs | 5 +- src/test.rs | 124 ++-------------------------- 10 files changed, 145 insertions(+), 235 deletions(-) diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index cf1fc59..75f69bb 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -5,7 +5,7 @@ <driver-ref>postgresql</driver-ref> <synchronize>true</synchronize> <jdbc-driver>org.postgresql.Driver</jdbc-driver> - <jdbc-url>jdbc:postgresql://localhost:32784/postgres</jdbc-url> + <jdbc-url>jdbc:postgresql://localhost:32769/postgres</jdbc-url> <working-dir>$ProjectFileDir$</working-dir> </data-source> </component> diff --git a/Cargo.lock b/Cargo.lock index 46554eb..ea4671d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,18 +441,19 @@ dependencies = [ [[package]] name = "bon" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8256e3cff531086cc3faf94c1649930ff64bceb2d0e8cc84fc0356d7ee9806" +checksum = "811d7882589e047896e5974d039dd8823a67973a63d559e6ad1e87ff5c42ed4f" dependencies = [ "bon-macros", + "rustversion", ] [[package]] name = "bon-macros" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99838f77c5073bc7846ecce92b64e7e5a5bd152a8ec392facf90ee4d90b4b35" +checksum = "d8e745a763e579a5ce70130e66f9dd35abf77cfeb9f418f305aeab8d1ae54c43" dependencies = [ "darling", "ident_case", @@ -1509,7 +1510,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" name = "lib" version = "1.4.3" dependencies = [ + "async-trait", "axum", + "bon", "chrono", "deadpool-diesel", "derive_more", @@ -1517,9 +1520,12 @@ dependencies = [ "diesel-async", "diesel-crud-derive", "diesel-crud-trait", + "diesel_async_migrations", "into-response-derive", "mime", "serde", + "serde_json", + "testcontainers-modules", "thiserror", "tokio", "tower 0.5.0", @@ -2841,15 +2847,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" dependencies = [ "bitflags 2.6.0", "bytes", "http", "http-body", - "http-body-util", "pin-project-lite", "tower-layer", "tower-service", diff --git a/Cargo.toml b/Cargo.toml index 8996b1f..a51d48f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ lib = { path = "../lib", features = ["axum", "serde", "derive", "diesel", "time" #lib = { git = "https://github.com/emberal/rust-lib", tag = "1.4.3", features = ["axum", "serde", "derive"] } [dev-dependencies] +lib = { path = "../lib", features = ["test"] } rstest = "0.22.0" testcontainers-modules = { version = "0.10.0", features = ["postgres"] } async-std = { version = "1.12.0", features = ["attributes"] } diff --git a/src/database.rs b/src/database.rs index b559a62..f48ef8e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,35 +1,10 @@ use crate::config; -use crate::error::AppError; -use axum::async_trait; use deadpool_diesel::postgres::BuildError; -use deadpool_diesel::Status; -use diesel_async::pooled_connection::deadpool::{Object, Pool}; -use diesel_async::pooled_connection::AsyncDieselConnectionManager; -use diesel_async::AsyncPgConnection; - -pub type PgPool = Pool<AsyncPgConnection>; - -#[async_trait] -pub trait GetConnection: Clone + Send + Sync { - async fn get(&self) -> Result<Object<AsyncPgConnection>, AppError>; - fn status(&self) -> Status; -} - -#[async_trait] -impl GetConnection for PgPool { - async fn get(&self) -> Result<Object<AsyncPgConnection>, AppError> { - self.get().await.map_err(Into::into) - } - fn status(&self) -> Status { - self.status() - } -} +use lib::diesel::pool::PgPool; pub(crate) fn create_pool() -> Result<PgPool, BuildError> { - create_pool_from_url(config::DATABASE_URL) -} - -pub(crate) fn create_pool_from_url(url: impl Into<String>) -> Result<PgPool, BuildError> { - let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(url); - Pool::builder(config).max_size(config::POOL_SIZE).build() + lib::diesel::pool::create_pool() + .url(config::DATABASE_URL) + .size(config::POOL_SIZE) + .call() } diff --git a/src/error.rs b/src/error.rs index 75deff2..da1dc2b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,11 @@ +use crate::services::reservation_service::ReservationError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum_login::AuthnBackend; use derive_more::Constructor; use diesel::result::DatabaseErrorKind; use diesel_async::pooled_connection::deadpool; - -use crate::services::reservation_service::ReservationError; +use lib::diesel::get_connection::GetConnectionError; use lib::diesel_crud_trait::CrudError; use lib::into_response_derive::IntoResponse; use serde::{Deserialize, Serialize}; @@ -96,6 +96,15 @@ impl IntoResponse for ResponseError { } } +impl From<GetConnectionError> for ResponseError { + fn from(value: GetConnectionError) -> Self { + match value { + GetConnectionError::PoolError(error) => Self::InternalServerError(error.to_string()), + GetConnectionError::DieselError(error) => Self::InternalServerError(error.to_string()), + } + } +} + impl From<CrudError> for ResponseError { fn from(value: CrudError) -> Self { match &value { @@ -156,6 +165,15 @@ impl IntoResponse for AppError { } } +impl From<GetConnectionError> for AppError { + fn from(value: GetConnectionError) -> Self { + match value { + GetConnectionError::PoolError(error) => Self::PoolError(error), + GetConnectionError::DieselError(error) => Self::DatabaseError(error), + } + } +} + impl From<base64ct::Error> for AppError { fn from(value: base64ct::Error) -> Self { Self::Base64Error(value.to_string()) diff --git a/src/main.rs b/src/main.rs index aadd46b..539deb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,16 @@ #[macro_use] extern crate rstest; -use crate::database::{create_pool, GetConnection}; +use crate::database::create_pool; use crate::services::session_service::SessionService; use crate::services::user_service::UserService; +use axum::middleware::{from_fn, Next}; +use axum::response::IntoResponse; use axum::Router; use axum_login::tower_sessions::SessionManagerLayer; use axum_login::AuthManagerLayerBuilder; use lib::axum::app::AppBuilder; +use lib::diesel::get_connection::GetConnection; use tower_sessions::cookie::time::Duration; use tower_sessions::Expiry; @@ -73,7 +76,7 @@ async fn main() { trait LoginRequired { fn login_required<Pool>(self) -> Self where - Pool: GetConnection + Send + Sync + 'static; + Pool: GetConnection + 'static; } impl<S> LoginRequired for Router<S> @@ -84,10 +87,6 @@ where where Pool: GetConnection + Send + Sync + 'static, { - use axum_login::axum::{ - middleware::{from_fn, Next}, - response::IntoResponse, - }; self.route_layer(from_fn( |auth_session: axum_login::AuthSession<UserService<Pool>>, req, next: Next| async move { if auth_session.user.is_some() { diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 72cef36..33f69ff 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,5 +1,5 @@ use crate::auth::AuthSession; -use crate::database::GetConnection; +use crate::error::ResponseError; use crate::models::user::{CreateUser, User, UserCredentials}; use crate::result::{ResponseResult, Success}; use crate::services::user_service::UserService; @@ -7,17 +7,17 @@ use crate::{bad_request, internal_server_error, ok}; use axum::extract::State; use axum::Json; use axum_valid::Valid; +use lib::diesel::get_connection::GetConnection; // router!( // "/auth", // routes!( // post "/login" => login::<Pool>, // post "/register" => register::<Pool> // ), -// Pool: Clone, Send, Sync, GetConnection -> UserService +// Pool: GetConnection -> UserService // ); -pub fn router<Pool: Clone + Send + Sync + GetConnection + 'static>( -) -> axum::Router<UserService<Pool>> { +pub fn router<Pool: GetConnection + 'static>() -> axum::Router<UserService<Pool>> { axum::Router::new().nest( "/auth", axum::Router::new() @@ -53,7 +53,7 @@ where .insert(create_user) .await .map(Success::Created) - .map_err(Into::into) + .map_err(ResponseError::from) } #[cfg(test)] @@ -61,26 +61,51 @@ mod tests { use crate::create_app; use crate::error::ErrorResponseBody; use crate::models::user::{CreateUser, User, UserRole}; - use crate::test::{create_test_containers_pool, create_test_pool, BuildJson, DeserializeInto}; + use crate::test::create_test_containers_pool; use axum::http::request::Builder; use axum::http::{Request, StatusCode}; + use axum::Router; + use futures::executor::block_on; + use lib::axum::traits::BuildJson; + use lib::serde::traits::DeserializeInto; use secrecy::ExposeSecret; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; + use testcontainers_modules::postgres::Postgres; + use testcontainers_modules::testcontainers::ContainerAsync; use tower::ServiceExt; fn register() -> Builder { Request::builder().uri("/auth/register").method("POST") } - #[tokio::test] - async fn test_register_created() { - let pool = create_test_pool().await.unwrap(); - let app = create_app(pool); - let create_user = CreateUser::new("test@email.com", "password"); + #[fixture] + #[once] + fn setup() -> Setup { + block_on(async { + let test_container = create_test_containers_pool().await.unwrap(); + let app = create_app(test_container.pool); + Setup { + _container: test_container.container, + app, + } + }) + } + + struct Setup { + _container: ContainerAsync<Postgres>, + app: Router, + } + + #[rstest] + #[tokio::test(flavor = "multi_thread")] + async fn test_register_created(setup: &Setup) { + let create_user = CreateUser::new("test_register_created@email.com", "password"); let create_email = create_user.email.clone(); - let response = app + let response = setup + .app + .clone() .oneshot(register().json(create_user).unwrap()) .await .unwrap(); @@ -95,15 +120,18 @@ mod tests { assert!(!user.salt.is_empty()); } - #[tokio::test] - async fn test_register_email_already_registered() { - let test_container = create_test_containers_pool().await.unwrap(); - let app = create_app(test_container.pool); - - let create_user = CreateUser::new("test@email.com", "password"); + #[rstest] + #[tokio::test(flavor = "multi_thread")] + async fn test_register_email_already_registered(setup: &Setup) { + let create_user = CreateUser::new( + "test_register_email_already_registered@email.com", + "password", + ); let call = || async { - app.clone() + setup + .app + .clone() .oneshot(register().json(create_user.clone()).unwrap()) .await .unwrap() @@ -129,7 +157,7 @@ mod tests { where S: Serializer, { - let mut s = serializer.serialize_struct("CreateUser", 2)?; + let mut s = serializer.serialize_struct("CreateUser", 3)?; s.serialize_field("email", &self.email)?; s.serialize_field("password", &self.password.expose_secret())?; s.serialize_field("role", &self.role)?; diff --git a/src/services/reservation_service.rs b/src/services/reservation_service.rs index ac5b7b4..b073b38 100644 --- a/src/services/reservation_service.rs +++ b/src/services/reservation_service.rs @@ -115,28 +115,27 @@ mod tests { use crate::models::room::Room; use crate::models::user::{CreateUser, User}; use crate::schema::{hotel, room, user}; - use crate::test::setup_test_transaction; + use crate::test::create_test_containers_pool; use chrono::{Duration, Utc}; use diesel::dsl::insert_into; use diesel_async::AsyncPgConnection; + use futures::executor::block_on; + use lib::diesel::pool::PgPool; use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudRead}; use secrecy::SecretString; + use testcontainers_modules::postgres::Postgres; + use testcontainers_modules::testcontainers::ContainerAsync; #[rstest] - #[tokio::test] - async fn test_check_in(#[future] setup: Setup) { - let Setup { - mut conn, - reservation, - .. - } = setup.await; - - Reservation::check_in(reservation.id, &mut conn) + #[tokio::test(flavor = "multi_thread")] + async fn test_check_in(setup: &Setup) { + let mut conn = setup.pool.get().await.unwrap(); + Reservation::check_in(setup.reservation.id, &mut conn) .await .unwrap(); assert!( - Reservation::read(reservation.id, &mut conn) + Reservation::read(setup.reservation.id, &mut conn) .await .unwrap() .checked_in @@ -144,20 +143,15 @@ mod tests { } #[rstest] - #[tokio::test] - async fn test_check_out(#[future] setup: Setup) { - let Setup { - mut conn, - reservation, - .. - } = setup.await; - - Reservation::check_out(reservation.id, &mut conn) + #[tokio::test(flavor = "multi_thread")] + async fn test_check_out(setup: &Setup) { + let mut conn = setup.pool.get().await.unwrap(); + Reservation::check_out(setup.reservation.id, &mut conn) .await .unwrap(); assert!( - !Reservation::read(reservation.id, &mut conn) + !Reservation::read(setup.reservation.id, &mut conn) .await .unwrap() .checked_in @@ -190,40 +184,34 @@ mod tests { Duration::days(11), Err(ReservationError::RoomNotAvailable) )] - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_room_available( - #[future] setup: Setup, + setup: &Setup, #[case] start: Duration, #[case] end: Duration, #[case] expected: Result<(), ReservationError>, ) { - let Setup { - mut conn, - reservation, - .. - } = setup.await; + let mut conn = setup.pool.get().await.unwrap(); let now = Utc::now().naive_utc(); let start = now + start; let end = now + end; assert_eq!( - Reservation::room_available(reservation.room_id, (start, end).into(), &mut conn).await, + Reservation::room_available(setup.reservation.room_id, (start, end).into(), &mut conn) + .await, expected ); } #[rstest] - #[tokio::test] - async fn test_room_available_no_reservations(#[future] setup: Setup) { - let Setup { - mut conn, hotel, .. - } = setup.await; - + #[tokio::test(flavor = "multi_thread")] + async fn test_room_available_no_reservations(setup: &Setup) { + let mut conn = setup.pool.get().await.unwrap(); let room = Room::insert( Room { id: 2, - hotel_id: hotel.id, + hotel_id: setup.hotel.id, beds: 1, size: 1, }, @@ -243,21 +231,30 @@ mod tests { } #[fixture] - async fn setup() -> Setup { - let mut conn = setup_test_transaction().await.unwrap(); - let hotel = insert_hotel(&mut conn).await; - let user = insert_user(&mut conn).await; - let room = insert_room(&mut conn, hotel.id).await; - let reservation = insert_reservation(&mut conn, room.id, user.email).await; - Setup { - conn, - hotel, - reservation, - } + #[once] + fn setup() -> Setup { + block_on(async { + let test_container = create_test_containers_pool().await.unwrap(); + let pool = test_container.pool; + let mut conn = pool.get().await.unwrap(); + + let hotel = insert_hotel(&mut conn).await; + let user = insert_user(&mut conn).await; + let room = insert_room(&mut conn, hotel.id).await; + let reservation = insert_reservation(&mut conn, room.id, user.email).await; + + Setup { + _container: test_container.container, + pool, + hotel, + reservation, + } + }) } struct Setup { - conn: AsyncPgConnection, + _container: ContainerAsync<Postgres>, + pool: PgPool, hotel: Hotel, reservation: Reservation, } diff --git a/src/services/room_service.rs b/src/services/room_service.rs index 5a60c45..450bab7 100644 --- a/src/services/room_service.rs +++ b/src/services/room_service.rs @@ -55,13 +55,14 @@ impl Room { #[cfg(test)] mod tests { use super::*; + use crate::config; use crate::models::hotel::{CreateHotel, Hotel}; use crate::models::reservation::{CreateReservation, Reservation}; use crate::models::room::Room; use crate::models::user::{CreateUser, User}; - use crate::test::setup_test_transaction; use diesel_async::AsyncPgConnection; use lib::diesel_crud_trait::DieselCrudCreate; + use lib::test::diesel_pool::setup_test_transaction; use serde_json::json; #[rstest] @@ -148,7 +149,7 @@ mod tests { #[fixture] async fn setup() -> Setup { - let mut conn = setup_test_transaction().await.unwrap(); + let mut conn = setup_test_transaction(config::DATABASE_URL).await.unwrap(); let hotels = insert_hotels(&mut conn).await; let rooms = insert_rooms(&mut conn, &hotels).await; let _reservation = insert_reservation(&mut conn, rooms[0].id).await; diff --git a/src/test.rs b/src/test.rs index eda1ac6..4c45958 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,78 +1,11 @@ use crate::config; -use crate::database::{create_pool, create_pool_from_url, GetConnection, PgPool}; -use crate::error::AppError; -use axum::async_trait; -use axum::body::{to_bytes, Body}; -use axum::http::header::CONTENT_TYPE; -use axum::http::Request; -use axum::response::Response; -use deadpool_diesel::postgres::BuildError; -use deadpool_diesel::Status; -use derive_more::Constructor; -use diesel_async::pooled_connection::deadpool::{Object, PoolError}; -use diesel_async::{AsyncConnection, AsyncPgConnection}; -use mime::APPLICATION_JSON; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use testcontainers_modules::postgres::Postgres; -use testcontainers_modules::testcontainers::runners::AsyncRunner; -use testcontainers_modules::testcontainers::{ContainerAsync, TestcontainersError}; -use thiserror::Error; - -#[derive(Debug, PartialEq, Error)] -pub enum Error { - #[error(transparent)] - Connection(#[from] diesel::ConnectionError), - #[error(transparent)] - Database(#[from] diesel::result::Error), -} - -pub async fn setup_test_transaction() -> Result<AsyncPgConnection, Error> { - let mut conn = AsyncPgConnection::establish(config::DATABASE_URL).await?; - conn.begin_test_transaction().await?; - Ok(conn) -} - -pub(crate) async fn create_test_pool() -> Result<PoolStub, BuildError> { - let pool = create_pool()?; - Ok(PoolStub(pool)) -} - -#[derive(Debug, Error)] -pub(crate) enum ContainerError { - #[error(transparent)] - TestContainers(#[from] TestcontainersError), - #[error(transparent)] - BuildError(#[from] BuildError), - #[error(transparent)] - PoolError(#[from] PoolError), - #[error(transparent)] - DieselError(#[from] diesel::result::Error), -} - -/// When the TestContainer is dropped, the container will be removed. -/// # Panics -/// If destructed and the container field is dropped, the container will be removed, and using the pool will cause panic. -#[derive(Constructor)] -pub(crate) struct TestContainer { - pub _container: ContainerAsync<Postgres>, - pub pool: PgPool, -} +use diesel_async::AsyncPgConnection; +use lib::test::test_containers::{ContainerError, TestContainer}; pub(crate) async fn create_test_containers_pool<'a>() -> Result<TestContainer, ContainerError> { - let container = create_postgres_container().await?; - let connection_string = format!( - "postgres://postgres:postgres@127.0.0.1:{}/postgres", - container.get_host_port_ipv4(5432).await? - ); - let pool = create_pool_from_url(connection_string)?; - run_migrations(pool.get().await?.as_mut()).await?; - Ok(TestContainer::new(container, pool)) -} - -pub(crate) async fn create_postgres_container( -) -> Result<ContainerAsync<Postgres>, TestcontainersError> { - Postgres::default().start().await + let test_container = lib::test::test_containers::create_test_containers_pool().await?; + run_migrations(test_container.pool.get().await?.as_mut()).await?; + Ok(test_container) } pub(crate) async fn run_migrations( @@ -80,50 +13,3 @@ pub(crate) async fn run_migrations( ) -> Result<(), diesel::result::Error> { config::MIGRATIONS.run_pending_migrations(conn).await } - -#[derive(Clone)] -pub(crate) struct PoolStub(PgPool); - -#[async_trait] -impl GetConnection for PoolStub { - async fn get(&self) -> Result<Object<AsyncPgConnection>, AppError> { - let mut conn = self.0.get().await?; - conn.begin_test_transaction().await?; - Ok(conn) - } - fn status(&self) -> Status { - unimplemented!("PoolStub does not support status") - } -} - -pub trait BuildJson { - fn json<T: Serialize>(self, body: T) -> Result<Request<Body>, axum::http::Error>; -} - -impl BuildJson for axum::http::request::Builder { - fn json<T: Serialize>(self, body: T) -> Result<Request<Body>, axum::http::Error> { - self.header(CONTENT_TYPE, APPLICATION_JSON.as_ref()) - .body(Body::new(json!(body).to_string())) - } -} - -#[derive(Debug, Error)] -pub(crate) enum DeserializeError { - #[error(transparent)] - SerdeError(#[from] serde_json::Error), - #[error(transparent)] - AxumError(#[from] axum::Error), -} - -#[async_trait] -pub trait DeserializeInto { - async fn deserialize_into<T: for<'de> Deserialize<'de>>(self) -> Result<T, DeserializeError>; -} - -#[async_trait] -impl DeserializeInto for Response { - async fn deserialize_into<T: for<'de> Deserialize<'de>>(self) -> Result<T, DeserializeError> { - let body = to_bytes(self.into_body(), usize::MAX).await?; - serde_json::from_slice(&body).map_err(Into::into) - } -}