diff --git a/Cargo.lock b/Cargo.lock index d8c3046..5198028 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1221,6 +1221,7 @@ dependencies = [ name = "li7y" version = "0.0.0" dependencies = [ + "actix-http", "actix-identity", "actix-session", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 302e756..d6559b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ time = { version = "0.3.36", features = ["parsing", "serde"] } uuid = { version = "1.9.0", features = ["serde", "v4"] } [dev-dependencies] +actix-http = "3.8.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" diff --git a/tests/auth.rs b/tests/auth.rs new file mode 100644 index 0000000..668bd62 --- /dev/null +++ b/tests/auth.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_http::header; +use actix_web::{cookie::Cookie, test}; +use sqlx::PgPool; + +mod common; + +#[sqlx::test] +async fn protected_route_requires_login(pool: PgPool) { + let srv = test::init_service(li7y::app(&common::config(), &pool)).await; + let req = test::TestRequest::get().uri("/items").to_request(); + let res = test::call_service(&srv, req).await; + + assert!(common::assert_redirect(res.map_into_boxed_body()).starts_with("/login")); +} + +#[sqlx::test] +async fn login(pool: PgPool) { + let srv = test::init_service(li7y::app(&common::config(), &pool)).await; + + // This is identical to common::session_cookie, + // but copied here explicitly to ensure the right functionality is tested. + let req = test::TestRequest::post() + .uri("/login") + .set_form(common::LoginForm::default()) + .to_request(); + + let res = test::call_service(&srv, req).await; + let session = Cookie::parse_encoded( + res.headers() + .clone() + .get(header::SET_COOKIE) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ) + .unwrap(); + + assert!(common::assert_redirect(res.map_into_boxed_body()).starts_with("/")); + + let req = test::TestRequest::get() + .uri("/") + .cookie(session.clone()) + .to_request(); + + let res = test::call_service(&srv, req).await; + + assert!(res.status().is_success()); +} + +#[ignore = "actix_session::CookieSessionStore does not support invalidating sessions"] +#[sqlx::test] +async fn logout(pool: PgPool) { + let srv = test::init_service(li7y::app(&common::config(), &pool)).await; + + let session_cookie = common::session_cookie(&srv).await; + + let req = test::TestRequest::post() + .uri("/logout") + .cookie(session_cookie.clone()) + .to_request(); + + test::call_service(&srv, req).await; + + let req = test::TestRequest::get() + .uri("/items") + .cookie(session_cookie.clone()) + .to_request(); + + let res = test::call_service(&srv, req).await; + + assert!(common::assert_redirect(res.map_into_boxed_body()).starts_with("/login")); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..020f5df --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::env; + +use actix_http::header; +use actix_web::{cookie::Cookie, dev::ServiceResponse, test}; +use clap::Parser; +use serde::Serialize; + +use li7y::Config; + +pub const SUPERUSER_PASSWORD: &str = "correct horse battery staple"; + +#[derive(Serialize)] +pub struct LoginForm { + password: String, +} + +impl Default for LoginForm { + fn default() -> Self { + Self { + password: SUPERUSER_PASSWORD.to_string(), + } + } +} + +pub fn config() -> Config { + env::set_var("SUPERUSER_PASSWORD", SUPERUSER_PASSWORD); + Config::parse_from(Vec::::new().iter()) +} + +#[allow(dead_code)] // for some reason rustc detects this as unused +pub fn assert_redirect(res: ServiceResponse) -> String { + assert!(res.status().is_redirection()); + + res.headers() + .get(header::LOCATION) + .expect("No location header set when expected") + .to_str() + .expect("Location header is not valid UTF-8") + .to_string() +} + +pub async fn session_cookie<'a>( + srv: &impl actix_web::dev::Service< + actix_http::Request, + Response = ServiceResponse>, + Error = actix_web::Error, + >, +) -> Cookie<'a> { + let req = test::TestRequest::post() + .uri("/login") + .set_form(LoginForm::default()) + .to_request(); + + Cookie::parse_encoded( + test::call_service(&srv, req) + .await + .headers() + .get(header::SET_COOKIE) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ) + .unwrap() +} diff --git a/tests/items.rs b/tests/items.rs new file mode 100644 index 0000000..6d30226 --- /dev/null +++ b/tests/items.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::{body::MessageBody, test}; +use sqlx::{query_as, PgPool}; +use uuid::Uuid; + +mod common; + +#[sqlx::test(fixtures("default"))] +async fn list(pool: PgPool) { + let srv = test::init_service(li7y::app(&common::config(), &pool)).await; + + let session_cookie = common::session_cookie(&srv).await; + + let req = test::TestRequest::get() + .uri("/items") + .cookie(session_cookie.clone()) + .to_request(); + + let res = test::call_service(&srv, req).await; + + assert!(res.status().is_success()); + + let body = String::from_utf8(res.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + + let items: Vec<(Uuid, Option)> = query_as("SELECT id, name FROM items") + .fetch_all(&pool) + .await + .unwrap(); + + for (id, name) in items { + assert!(body.contains(&format!(r#"href="/item/{id}""#))); + + if let Some(name) = name { + assert!(body.contains(&format!(">{name}"))); + } + } +} + +#[sqlx::test(fixtures("default"))] +async fn show(pool: PgPool) { + let srv = test::init_service(li7y::app(&common::config(), &pool)).await; + + let session_cookie = common::session_cookie(&srv).await; + + let req = test::TestRequest::get() + .uri("/item/663f45e6-b11a-4197-8ce4-c784ac9ee617") + .cookie(session_cookie.clone()) + .to_request(); + + let res = test::call_service(&srv, req).await; + + assert!(res.status().is_success()); + + let body = String::from_utf8(res.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + + assert!(body.contains("

Item 2 <")); + assert!(body.contains("UUID663f45e6-b11a-4197-8ce4-c784ac9ee617")); + assert!(body.contains("NameItem 2")); + assert!(body + .contains(r#"href="/item-class/8a979306-b4c6-4ef8-900d-68f64abb2975">Subclass 1.1"#)); + assert!(body.contains(r#"href="/item/4fc0f5f4-4dca-4c24-844d-1f464cb32afa">Item 1"#)); + assert!(body.contains(r#""#)); + assert!(body.contains( + r#"href="/item/049298e2-73db-42fb-957d-a741655648b1">Original Packaging of Item 2"# + )); + assert!(body.contains(">Lorem ipsum 3")); + assert!(body.contains(">acquire