feat: test helper, snapshot testing

This commit is contained in:
Max Hohlfeld 2024-12-12 18:40:35 +01:00
parent ed7ce14990
commit 931b7e159d
10 changed files with 283 additions and 73 deletions

43
Cargo.lock generated
View File

@ -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"

View File

@ -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 = "." }

View File

@ -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<User>,
@ -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());
}

View File

@ -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<User>) -> impl Responder {
@ -11,8 +22,25 @@ async fn get(user: web::ReqData<User>) -> 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);
}

View File

@ -36,3 +36,9 @@ impl TryFrom<u8> for Function {
}
}
}
impl Default for Function {
fn default() -> Self {
Self::Posten
}
}

View File

@ -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"));
}

View File

@ -23,3 +23,9 @@ impl TryFrom<u8> for Role {
}
}
}
impl Default for Role {
fn default() -> Self {
Self::Staff
}
}

View File

@ -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;

View File

@ -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)]

View File

@ -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<Postgres>,
app: &T,
config: &RequestConfig,
) -> Cookie<'a>
where
T: Service<Request, Response = ServiceResponse<R>, 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<T, R>(
pool: &Pool<Postgres>,
app: T,
config: &RequestConfig,
) -> ServiceResponse<R>
where
T: Service<Request, Response = ServiceResponse<R>, 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<T, R, F>(
pool: &Pool<Postgres>,
app: T,
config: &RequestConfig,
form: F,
) -> ServiceResponse<R>
where
T: Service<Request, Response = ServiceResponse<R>, 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<T, R>(
pool: &Pool<Postgres>,
app: T,
config: &RequestConfig,
) -> ServiceResponse<R>
where
T: Service<Request, Response = ServiceResponse<R>, 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
}