From 931b7e159de97eed148f31067b58541206a8d1ce Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Thu, 12 Dec 2024 18:40:35 +0100 Subject: [PATCH] feat: test helper, snapshot testing --- Cargo.lock | 43 +++++++ web/Cargo.toml | 3 + web/src/endpoints/area/delete.rs | 52 ++++++++ web/src/endpoints/area/get_new.rs | 34 +++++- web/src/models/function.rs | 6 + web/src/models/location.rs | 69 +---------- web/src/models/role.rs | 6 + web/src/utils/mod.rs | 2 + web/src/utils/test_helper/mod.rs | 10 +- web/src/utils/test_helper/test_requests.rs | 131 +++++++++++++++++++++ 10 files changed, 283 insertions(+), 73 deletions(-) create mode 100644 web/src/utils/test_helper/test_requests.rs diff --git a/Cargo.lock b/Cargo.lock index 9f347455..68210a0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,6 +748,7 @@ dependencies = [ "dotenv", "futures-util", "idna 1.0.2", + "insta", "lettre", "pico-args", "quick-xml", @@ -886,6 +887,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1180,6 +1193,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1825,6 +1844,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + [[package]] name = "instant" version = "0.1.13" @@ -1951,6 +1982,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2774,6 +2811,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "slab" version = "0.4.9" diff --git a/web/Cargo.toml b/web/Cargo.toml index 76125ca5..f2270d35 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -41,6 +41,9 @@ actix-http = "3.9.0" built = "0.7.4" static-files = "0.2.1" +[dev-dependencies] +insta = "1.41.1" + # [dev-dependencies] # brass-web = { path = "." } diff --git a/web/src/endpoints/area/delete.rs b/web/src/endpoints/area/delete.rs index e9a8711f..83f58aee 100644 --- a/web/src/endpoints/area/delete.rs +++ b/web/src/endpoints/area/delete.rs @@ -1,4 +1,5 @@ use actix_web::{web, HttpResponse, Responder}; +use brass_macros::db_test; use sqlx::PgPool; use crate::{ @@ -7,6 +8,11 @@ use crate::{ utils::ApplicationError, }; +#[cfg(test)] +use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig}; +#[cfg(test)] +use actix_http::StatusCode; + #[actix_web::delete("/area/delete/{id}")] pub async fn delete( user: web::ReqData, @@ -25,3 +31,49 @@ pub async fn delete( return Ok(HttpResponse::Ok().finish()); } + +#[db_test] +async fn works_when_user_is_admin(context: &DbTestContext) { + Area::create(&context.db_pool, "Area to delete") + .await + .unwrap(); + assert!(Area::read_by_id(&context.db_pool, 2) + .await + .unwrap() + .is_some()); + + let app = context.app().await; + let config = RequestConfig { + uri: "/area/delete/2".to_string(), + role: Role::Admin, + function: crate::models::Function::Posten, + user_area: 1, + }; + let response = test_delete(&context.db_pool, app, &config).await; + + assert_eq!(StatusCode::OK, response.status()); + assert!(Area::read_by_id(&context.db_pool, 2) + .await + .unwrap() + .is_none()); +} + +#[db_test] +async fn does_not_work_when_user_is_not_admin(context: &DbTestContext) { + Area::create(&context.db_pool, "Area to delete") + .await + .unwrap(); + assert!(Area::read_by_id(&context.db_pool, 2) + .await + .unwrap() + .is_some()); + + let app = context.app().await; + let response = test_delete(&context.db_pool, app, &RequestConfig::new("/area/delete/2")).await; + + assert_eq!(StatusCode::UNAUTHORIZED, response.status()); + assert!(Area::read_by_id(&context.db_pool, 2) + .await + .unwrap() + .is_some()); +} diff --git a/web/src/endpoints/area/get_new.rs b/web/src/endpoints/area/get_new.rs index 3e222286..6599bfd3 100644 --- a/web/src/endpoints/area/get_new.rs +++ b/web/src/endpoints/area/get_new.rs @@ -1,7 +1,18 @@ -use actix_web::{web, HttpResponse, Responder}; +use actix_web::{body::MessageBody, test::read_body, web, HttpResponse, Responder}; use askama_actix::TemplateToResponse; +use brass_macros::db_test; +use insta::assert_snapshot; -use crate::{endpoints::area::NewOrEditAreaTemplate, models::{Role, User}}; +use crate::{ + endpoints::area::NewOrEditAreaTemplate, + models::{Role, User}, + utils::test_helper::DbTestContext, +}; + +#[cfg(test)] +use crate::utils::test_helper::{test_get, RequestConfig}; +#[cfg(test)] +use actix_http::StatusCode; #[actix_web::get("/area/new")] async fn get(user: web::ReqData) -> impl Responder { @@ -11,8 +22,25 @@ async fn get(user: web::ReqData) -> impl Responder { let template = NewOrEditAreaTemplate { user: user.into_inner(), - area: None + area: None, }; template.to_response() } + +#[db_test] +async fn produces_template(context: &DbTestContext) { + let app = context.app().await; + let config = RequestConfig { + uri: "/area/new".to_string(), + role: Role::Admin, + function: crate::models::Function::Posten, + user_area: 1, + }; + let response = test_get(&context.db_pool, app, &config).await; + + assert_eq!(StatusCode::OK, response.status()); + + let body = String::from_utf8(read_body(response).await.to_vec()).unwrap(); + assert_snapshot!(body); +} diff --git a/web/src/models/function.rs b/web/src/models/function.rs index 584d99e7..322b9f6e 100644 --- a/web/src/models/function.rs +++ b/web/src/models/function.rs @@ -36,3 +36,9 @@ impl TryFrom for Function { } } } + +impl Default for Function { + fn default() -> Self { + Self::Posten + } +} diff --git a/web/src/models/location.rs b/web/src/models/location.rs index 16d8b125..82c2bcc1 100644 --- a/web/src/models/location.rs +++ b/web/src/models/location.rs @@ -1,26 +1,11 @@ -use actix_http::StatusCode; -use actix_identity::Identity; -use actix_web::test; -use actix_web::test::call_service; -use actix_web::test::TestRequest; -use actix_web::HttpMessage; -use brass_macros::db_test; use sqlx::{query, PgPool}; -use crate::auth::utils::generate_salt_and_hash_plain_password; -use crate::auth::utils::hash_plain_password_with_salt; -use crate::endpoints::user::post_login::LoginForm; -use crate::models::Function; -use crate::models::Role; -use crate::models::User; -use crate::utils::test_helper::DbTestContext; - use super::Area; use super::Result; #[derive(Debug)] -pub struct Location { +pub struct Location { pub id: i32, pub name: String, pub area_id: i32, @@ -133,55 +118,3 @@ impl Location { Ok(()) } } - -#[db_test] -async fn test_read_all(context: &DbTestContext) { - let app = context.app().await; - - let (hash, salt) = generate_salt_and_hash_plain_password("abc").unwrap(); - User::create_with_password(&context.db_pool, "abc", "abc", &hash, &salt, Role::Admin, Function::Wachhabender, 1).await.unwrap(); - - Location::create(&context.db_pool, "wundervolle location", 1).await.unwrap(); - - //let locations = Location::read_all(&context.db_pool).await.unwrap(); - // - //assert_eq!(1, locations.len()); - - let login_form = LoginForm { - email: "abc".to_string(), - password: "abc".to_string() - }; - - let login_req = TestRequest::post() - .uri("/login") - .set_form(login_form) - .to_request(); - - let login_resp = call_service(&app, login_req).await; - //println!("{:?}", log); - let cookie = login_resp.response().cookies().next().unwrap().to_owned(); - - let req = TestRequest::get() - .uri("/locations") - .cookie(cookie) - .to_request(); - - //let http_req = TestRequest::get() - // .uri("/locations") - // .to_http_request(); - //let service_req = TestRequest::get() - // .uri("/locations") - // .to_srv_request(); - - //Identity::login(&req.extensions(), "1".to_string()).unwrap(); - let resp = call_service(&context.app().await, req).await; - //let resp = test::call_and_read_body(&context.app().await, http_req).await; - - println!("{:?}", resp.headers()); - assert_eq!(StatusCode::OK, resp.status()); - - - let body = test::read_body(resp).await; - let body_string = String::from_utf8(body.to_vec()).unwrap(); - assert!(body_string.contains("wundervolle location")); -} diff --git a/web/src/models/role.rs b/web/src/models/role.rs index 48201961..3d5fd182 100644 --- a/web/src/models/role.rs +++ b/web/src/models/role.rs @@ -23,3 +23,9 @@ impl TryFrom for Role { } } } + +impl Default for Role { + fn default() -> Self { + Self::Staff + } +} diff --git a/web/src/utils/mod.rs b/web/src/utils/mod.rs index 600b027b..6facfc53 100644 --- a/web/src/utils/mod.rs +++ b/web/src/utils/mod.rs @@ -4,6 +4,8 @@ pub mod password_help; pub mod token_generation; pub mod event_planning_template; mod application_error; + +#[cfg(test)] pub mod test_helper; pub use application_error::ApplicationError; diff --git a/web/src/utils/test_helper/mod.rs b/web/src/utils/test_helper/mod.rs index 0ee23450..8d5c2d6e 100644 --- a/web/src/utils/test_helper/mod.rs +++ b/web/src/utils/test_helper/mod.rs @@ -7,14 +7,20 @@ use actix_web::{ test::init_service, }; use brass_config::{load_config, Config, Environment}; -use lettre::{transport::stub::StubTransport, Transport}; +use lettre::transport::stub::StubTransport; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::{Captures, Regex}; use sqlx::{ postgres::{PgConnectOptions, PgPoolOptions}, - Connection, Executor, PgConnection, PgPool, + Connection, Executor, PgConnection, PgPool, Pool, Postgres, }; +mod test_requests; +pub use test_requests::test_delete; +pub use test_requests::test_get; +pub use test_requests::test_post; +pub use test_requests::RequestConfig; + use crate::create_app; #[derive(Debug)] diff --git a/web/src/utils/test_helper/test_requests.rs b/web/src/utils/test_helper/test_requests.rs new file mode 100644 index 00000000..2546aade --- /dev/null +++ b/web/src/utils/test_helper/test_requests.rs @@ -0,0 +1,131 @@ +use crate::{ + endpoints::user::post_login::LoginForm, + models::{Function, Role, User}, +}; +use actix_http::Request; +use actix_web::{ + body::MessageBody, + cookie::Cookie, + dev::{Service, ServiceResponse}, + error::Error, + test, +}; +use serde::Serialize; +use sqlx::{Pool, Postgres}; + +pub struct RequestConfig { + pub uri: String, + pub role: Role, + pub function: Function, + pub user_area: i32, +} + +impl RequestConfig { + pub fn new(uri: &str) -> Self { + Self { + uri: uri.to_string(), + role: Role::Staff, + function: Function::Posten, + user_area: 1, + } + } +} + +async fn create_user_and_get_login_cookie<'a, T, R>( + pool: &Pool, + app: &T, + config: &RequestConfig, +) -> Cookie<'a> +where + T: Service, Error = Error>, + R: MessageBody + 'a, +{ + const HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$IPiLaPCFZOK69MA1a6GUzw$ZZinpbkP7pXhN7g7dGkh87kGTeuFd/2er1U+y+4IKWo"; + const SALT: &str = "IPiLaPCFZOK69MA1a6GUzw"; + + User::create_with_password( + pool, + "abc", + "abc", + &HASH, + &SALT, + config.role, + config.function, + config.user_area, + ) + .await + .unwrap(); + + let login_form = LoginForm { + email: "abc".to_string(), + password: "abc".to_string(), + }; + + let login_req = test::TestRequest::post() + .uri("/login") + .set_form(login_form) + .to_request(); + + let login_resp = test::call_service(&app, login_req).await; + login_resp.response().cookies().next().unwrap().into_owned() +} + +pub async fn test_get( + pool: &Pool, + app: T, + config: &RequestConfig, +) -> ServiceResponse +where + T: Service, Error = Error>, + R: MessageBody, +{ + let cookie = create_user_and_get_login_cookie(&pool, &app, &config).await; + + let get_request = test::TestRequest::get() + .uri(&config.uri) + .cookie(cookie) + .to_request(); + + test::call_service(&app, get_request).await +} + +pub async fn test_post( + pool: &Pool, + app: T, + config: &RequestConfig, + form: F, +) -> ServiceResponse +where + T: Service, Error = Error>, + R: MessageBody, + F: Serialize, +{ + let cookie = create_user_and_get_login_cookie(&pool, &app, config).await; + + let post_request = test::TestRequest::post() + .uri(&config.uri) + .cookie(cookie) + .set_form(form) + .to_request(); + + test::call_service(&app, post_request).await +} + +pub async fn test_delete( + pool: &Pool, + app: T, + config: &RequestConfig, +) -> ServiceResponse +where + T: Service, Error = Error>, + R: MessageBody, +{ + let cookie = create_user_and_get_login_cookie(&pool, &app, config).await; + + let delete_request = test::TestRequest::delete() + .uri(&config.uri) + .cookie(cookie) + .to_request(); + + test::call_service(&app, delete_request).await +}