diff --git a/Cargo.lock b/Cargo.lock index c286f7ce..f425479b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2573,6 +2573,8 @@ dependencies = [ "diesel", "diesel-async", "dotenvy", + "env_logger", + "log", "serde", "uuid", ] diff --git a/databases/diesel-async/Cargo.toml b/databases/diesel-async/Cargo.toml index 8443b577..fbd7827b 100644 --- a/databases/diesel-async/Cargo.toml +++ b/databases/diesel-async/Cargo.toml @@ -5,8 +5,10 @@ edition = "2021" [dependencies] actix-web.workspace = true -diesel = { version = "*", default-features = false, features = ["uuid"] } -diesel-async = { version = "*", features = ["postgres", "bb8", "async-connection-wrapper"] } +diesel = { version = "2", default-features = false, features = ["uuid"] } +diesel-async = { version = "0.5", features = ["postgres", "bb8", "async-connection-wrapper"] } serde.workspace = true uuid.workspace = true dotenvy.workspace = true +env_logger.workspace = true +log.workspace = true diff --git a/databases/diesel-async/src/actions.rs b/databases/diesel-async/src/actions.rs index 17ecb31d..2bbb2c94 100644 --- a/databases/diesel-async/src/actions.rs +++ b/databases/diesel-async/src/actions.rs @@ -18,8 +18,7 @@ pub async fn find_item_by_uid( let item = items .filter(id.eq(uid)) .select(models::Item::as_select()) - // execute the query via the provided - // async `diesel_async::RunQueryDsl` + // execute the query via the provided async `diesel_async::RunQueryDsl` .first::(conn) .await .optional()?; diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index 84d36691..b6cb829f 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -1,9 +1,11 @@ #[macro_use] extern crate diesel; -use actix_web::{error, get, post, web, App, HttpResponse, HttpServer, Responder}; -use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}; -use diesel_async::AsyncPgConnection; +use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder}; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, + AsyncPgConnection, +}; use dotenvy::dotenv; use std::{env, io}; use uuid::Uuid; @@ -72,15 +74,20 @@ async fn add_item( #[actix_web::main] async fn main() -> io::Result<()> { dotenv().ok(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + // initialize DB pool outside `HttpServer::new` so that it is shared across all workers + let pool = initialize_db_pool().await; - let mgr = AsyncDieselConnectionManager::::new(db_url); - let pool = Pool::builder().build(mgr).await.unwrap(); + log::info!("starting HTTP server at http://localhost:8080"); HttpServer::new(move || { App::new() + // add DB pool handle to app data; enables use of `web::Data` extractor .app_data(web::Data::new(pool.clone())) + // add request logger middleware + .wrap(middleware::Logger::default()) + // add route handlers .service(add_item) .service(get_item) }) @@ -88,3 +95,88 @@ async fn main() -> io::Result<()> { .run() .await } + +/// Initialize database connection pool based on `DATABASE_URL` environment variable. +/// +/// See more: . +async fn initialize_db_pool() -> DbPool { + let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + + let connection_manager = AsyncDieselConnectionManager::::new(db_url); + Pool::builder().build(connection_manager).await.unwrap() +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + use super::*; + + #[actix_web::test] + async fn item_routes() { + dotenv().ok(); + env_logger::try_init_from_env(env_logger::Env::new().default_filter_or("info")).ok(); + + let pool = initialize_db_pool().await; + + let app = test::init_service( + App::new() + .app_data(web::Data::new(pool.clone())) + .wrap(middleware::Logger::default()) + .service(get_item) + .service(add_item), + ) + .await; + + // send something that isn't a UUID to `get_item` + let req = test::TestRequest::get().uri("/item/123").to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"UUID parsing failed"), + "unexpected body: {body:?}", + ); + + // try to find a non-existent item + let req = test::TestRequest::get() + .uri(&format!("/item/{}", Uuid::nil())) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"No item found"), + "unexpected body: {body:?}", + ); + + // create new item + let req = test::TestRequest::post() + .uri("/item") + .set_json(models::NewItem::new("Test item")) + .to_request(); + let res: models::Item = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test item"); + + // get an item + let req = test::TestRequest::get() + .uri(&format!("/item/{}", res.id)) + .to_request(); + let res: models::Item = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test item"); + + // delete new item from table + use crate::schema::items::dsl::*; + diesel::delete(items.filter(id.eq(res.id))) + .execute( + &mut pool + .get() + .await + .expect("couldn't get db connection from pool"), + ) + .await + .expect("couldn't delete test item from table"); + } +}