From 8d040ad36899d0dedc0a645b9531e4d80ecea88d Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sat, 3 Jun 2023 15:45:19 -0700 Subject: [PATCH] Finish signup tests. --- src/lib.rs | 5 +- src/login.rs | 6 +- src/signup.rs | 262 +++++++++++++++++++++++++++++++++++++++------- src/templates.rs | 4 + src/test_utils.rs | 4 +- 5 files changed, 233 insertions(+), 48 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 01afc36..6638c22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,10 +30,7 @@ pub async fn app(db_pool: SqlitePool, secret: &[u8]) -> Router { Router::new() .route("/", get(handle_slash).post(handle_slash)) .route("/signup", get(get_create_user).post(post_create_user)) - .route( - "/signup_success/:id", - get(handle_signup_success).post(handle_signup_success), - ) + .route("/signup_success/:id", get(handle_signup_success)) .route("/login", get(get_login).post(post_login)) .route("/logout", get(get_logout).post(post_logout)) .fallback(handle_slash_redir) diff --git a/src/login.rs b/src/login.rs index 9d597ee..f9532f2 100644 --- a/src/login.rs +++ b/src/login.rs @@ -103,11 +103,9 @@ pub async fn post_logout(mut auth: AuthContext) -> impl IntoResponse { #[cfg(test)] mod test { - use axum::body::Bytes; - use crate::{ templates::{Index, LoginGet, LogoutGet, LogoutPost}, - test_utils::{get_user, massage, server, FORM_CONTENT_TYPE}, + test_utils::{get_test_user, massage, server, FORM_CONTENT_TYPE}, }; const LOGIN_FORM: &str = "username=test_user&password=a"; @@ -201,7 +199,7 @@ mod test { assert_eq!(resp.status_code(), 303); let logged_in = Index { - user: Some(get_user()), + user: Some(get_test_user()), } .to_string(); diff --git a/src/signup.rs b/src/signup.rs index 986f172..02bb93f 100644 --- a/src/signup.rs +++ b/src/signup.rs @@ -2,7 +2,6 @@ use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, }; -use askama::Template; use axum::{ extract::{Form, Path, State}, http::StatusCode, @@ -12,7 +11,10 @@ use sqlx::{query_as, SqlitePool}; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; -use crate::{templates::CreateUser, User}; +use crate::{ + templates::{CreateUser, CreateUserSuccess}, + User, +}; pub(crate) const CREATE_QUERY: &str = "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; @@ -22,10 +24,6 @@ const ID_QUERY: &str = "select * from witches where id = $1"; // Result types for user creation //-************************************************************************ -#[derive(Debug, Clone, Template)] -#[template(path = "signup_success.html")] -pub struct CreateUserSuccess(User); - #[Error(desc = "Could not create user.")] #[non_exhaustive] pub struct CreateUserError(#[from] CreateUserErrorKind); @@ -36,7 +34,7 @@ impl IntoResponse for CreateUserError { CreateUserErrorKind::UnknownDBError => { (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() } - _ => (StatusCode::BAD_REQUEST, format!("{self}")).into_response(), + _ => (StatusCode::OK, format!("{self}")).into_response(), } } } @@ -133,7 +131,7 @@ pub async fn post_create_user( let id = user.id.as_simple().to_string(); let location = format!("/signup_success/{id}"); - let resp = axum::response::Redirect::temporary(&location); + let resp = axum::response::Redirect::to(&location); Ok(resp) } @@ -157,7 +155,7 @@ pub async fn handle_signup_success( if user.username.is_empty() || id.is_empty() { // redirect to front page if we got here without a valid witch ID - *resp.status_mut() = StatusCode::TEMPORARY_REDIRECT; + *resp.status_mut() = StatusCode::SEE_OTHER; resp.headers_mut().insert("Location", "/".parse().unwrap()); } @@ -229,53 +227,241 @@ pub(crate) async fn create_user( } } +//-************************************************************************ +// TESTS +//-************************************************************************ + #[cfg(test)] mod test { use crate::{ db::get_pool, - test_utils::{get_user, server_with_pool}, + templates::{CreateUser, CreateUserSuccess}, + test_utils::{get_test_user, insert_user, massage, server_with_pool, FORM_CONTENT_TYPE}, + User, }; const GOOD_FORM: &str = "username=test_user&displayname=Test+User&password=aaaa&pw_verify=aaaa"; #[tokio::test] - async fn post_create_user_success() { - todo!() - } + async fn post_create_user() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(GOOD_FORM); - mod failure { - // various ways to fuck up signup - const MISMATCH_PW_FORM: &str = - "username=test_user&displayname=Test+User&password=aaaa&pw_verify=bbbb"; - const SHORT_PW_FORM: &str = - "username=test_user&displayname=Test+User&password=a&pw_verify=a"; - const LONG_USERNAME_FORM: &str = - "username=test_user12345678901234567890&displayname=Test+User& - password=aaaa&pw_verify=aaaa"; + let _resp = server + .post("/signup") + .expect_failure() // 303 is "failure" + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; - #[tokio::test] - async fn mismatch_pw() { - todo!() - } - - #[tokio::test] - async fn too_short_pw() { - todo!() - } - - #[tokio::test] - async fn too_long_username() { - todo!() - } + // get the new user from the db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_ok()); } #[tokio::test] async fn get_create_user() { - todo!() + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + + let resp = server.get("/signup").await; + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = CreateUser::default().to_string(); + assert_eq!(&expected, body); } #[tokio::test] async fn handle_signup_success() { - todo!() + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + + let user = get_test_user(); + insert_user(&user, &pool).await; + let id = user.id.as_simple().to_string(); + + let path = format!("/signup_success/{id}"); + + let resp = server.get(&path).expect_success().await; + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = CreateUserSuccess(user).to_string(); + assert_eq!(&expected, body); + } + + //-************************************************************************ + // honestly this is basically the whole suite here + //-************************************************************************ + mod failure { + use axum::http::StatusCode; + + use super::*; + use crate::signup::CreateUserError; + + // various ways to fuck up signup + const PASSWORD_MISMATCH_FORM: &str = + "username=test_user&displayname=Test+User&password=aaaa&pw_verify=bbbb"; + const PASSWORD_SHORT_FORM: &str = + "username=test_user&displayname=Test+User&password=a&pw_verify=a"; + const PASSWORD_LONG_FORM: &str = "username=test_user&displayname=Test+User&password=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda&pw_verify=sphinx+of+black+qwartz+judge+my+vow+etc+etc+yadd+yadda"; + const USERNAME_SHORT_FORM: &str = + "username=&displayname=Test+User&password=aaaa&pw_verify=aaaa"; + const USERNAME_LONG_FORM: &str = + "username=test_user12345678901234567890&displayname=Test+User&password=aaaa&pw_verify=aaaa"; + const DISPLAYNAME_LONG_FORM: &str = "username=test_user&displayname=Since+time+immemorial%2C+display+names+have+been+subject+to+a+number+of+conventions%2C+restrictions%2C+usages%2C+and+even+incentives.+Have+we+finally+gone+too+far%3F+In+this+essay%2C+&password=aaaa&pw_verify=aaaa"; + + #[tokio::test] + async fn password_mismatch() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(PASSWORD_MISMATCH_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::PasswordMismatch).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn password_short() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(PASSWORD_SHORT_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::BadPassword).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn password_long() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(PASSWORD_LONG_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::BadPassword).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn username_short() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(USERNAME_SHORT_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::BadUsername).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn username_long() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(USERNAME_LONG_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::BadUsername).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn displayname_long() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + let body = massage(DISPLAYNAME_LONG_FORM); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = + CreateUserError(crate::signup::CreateUserErrorKind::BadDisplayname).to_string(); + assert_eq!(&expected, body); + } + + #[tokio::test] + async fn handle_signup_success() { + let pool = get_pool().await; + let server = server_with_pool(&pool).await; + + let path = format!("/signup_success/nope"); + + let resp = server.get(&path).expect_failure().await; + assert_eq!(resp.status_code(), StatusCode::SEE_OTHER); + } } } diff --git a/src/templates.rs b/src/templates.rs index 45ed268..9f8748d 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -13,6 +13,10 @@ pub struct CreateUser { pub pw_verify: String, } +#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] +#[template(path = "signup_success.html")] +pub struct CreateUserSuccess(pub User); + #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] #[template(path = "login_post.html")] pub struct LoginPost { diff --git a/src/test_utils.rs b/src/test_utils.rs index 3f6559e..20ae1b8 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -7,7 +7,7 @@ use crate::User; pub const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; -pub fn get_user() -> User { +pub fn get_test_user() -> User { User { username: "test_user".to_string(), // corresponding to a password of "a": @@ -22,7 +22,7 @@ pub async fn server() -> TestServer { let pool = crate::db::get_pool().await; let secret = [0u8; 64]; - let user = get_user(); + let user = get_test_user(); sqlx::query(crate::signup::CREATE_QUERY) .bind(user.id) .bind(&user.username)