diff --git a/src/db.rs b/src/db.rs index 26ca512..54d77e4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -8,7 +8,7 @@ use sqlx::{ const MAX_CONNS: u32 = 200; const MIN_CONNS: u32 = 5; -const TIMEOUT: u64 = 11; +const TIMEOUT: u64 = 3; pub fn get_db_pool() -> SqlitePool { let db_filename = { diff --git a/src/signup/handlers.rs b/src/signup/handlers.rs index 8487b80..ec5c866 100644 --- a/src/signup/handlers.rs +++ b/src/signup/handlers.rs @@ -196,8 +196,7 @@ pub(crate) async fn create_user( CreateUserErrorKind::UnknownDBError })?; - let invitation = - Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?; + let invitation = Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?; let invited_by = validate_invitation(invitation, &mut tx).await?; @@ -370,23 +369,110 @@ mod test { // honestly this is basically the whole suite here //-************************************************************************ mod failure { - use super::*; - use crate::signup::handlers::{CreateUserError, CreateUserErrorKind}; + use std::time::Duration; - // various ways to fuck up signup - const PASSWORD_MISMATCH_FORM: &str = - "username=bad_user&displayname=Bad+User&password=aaaa&pw_verify=bbbb&invitation=0000000000000000000000001A"; - const PASSWORD_SHORT_FORM: &str = - "username=bad_user&displayname=Bad+User&password=a&pw_verify=a&invitation=0000000000000000000000001A"; - const PASSWORD_LONG_FORM: &str = "username=bad_user&displayname=Bad+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&invitation=0000000000000000000000001A"; - const USERNAME_SHORT_FORM: &str = - "username=&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; - const USERNAME_LONG_FORM: &str = - "username=bad_user12345678901234567890&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; - const DISPLAYNAME_LONG_FORM: &str = "username=bad_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&invitation=0000000000000000000000001A"; + use axum::response::IntoResponse; + + use super::*; + use crate::{ + signup::handlers::{CreateUserError, CreateUserErrorKind}, + Invitation, + }; + + #[test] + fn used_up_invite() { + let lucky1 = "username=lucky1&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + let lucky2 = "username=lucky2&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + let unlucky = "username=lucky3&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + let pool = get_db_pool(); + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let server = server_with_pool(&pool).await; + let body = massage(lucky1); + + let _ = server + .post("/signup") + // 303 is "failure", but that's a successful signup + .expect_failure() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + let user = User::try_get("lucky1", &pool).await; + assert!(user.is_ok() && user.unwrap().is_some()); + + let body = massage(lucky2); + let _ = server + .post("/signup") + .expect_failure() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + let user = User::try_get("lucky2", &pool).await; + assert!(user.is_ok() && user.unwrap().is_some()); + + let body = massage(unlucky); + let resp = server + .post("/signup") + // failure to sign up is not a failed request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + let user = User::try_get("unlucky", &pool).await; + assert!(user.is_ok() && user.unwrap().is_none()); + + let body = resp.as_bytes(); + let expected: CreateUserError = CreateUserErrorKind::BadInvitation.into(); + let expected = expected.into_response().into_body(); + let expected = axum::body::to_bytes(expected, usize::MAX).await.unwrap(); + + assert_eq!(&expected, body); + }); + } + + #[test] + fn expired_invite() { + let pool = get_db_pool(); + let rt = Runtime::new().unwrap(); + rt.block_on(async { + // this function adds a user with the omega id, so the invite can be added + let server = server_with_pool(&pool).await; + + let invite = + Invitation::new(Julid::omega()).with_expires_in(Duration::from_millis(1)); + let invite = invite.commit(&pool).await.unwrap(); + + std::thread::sleep(Duration::from_millis(100)); + + let tooslow = + format!("username=tooslow&password=aaaa&pw_verify=aaaa&invitation={invite}"); + let body = massage(&tooslow); + + let resp = server + .post("/signup") + // failure to sign up is not a failed request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + let user = User::try_get("unlucky", &pool).await; + assert!(user.is_ok() && user.unwrap().is_none()); + + let body = String::from_utf8(resp.as_bytes().to_vec()).unwrap(); + let expected: CreateUserError = CreateUserErrorKind::BadInvitation.into(); + let expected = expected.into_response().into_body(); + let bytes = axum::body::to_bytes(expected, usize::MAX).await.unwrap(); + let expected = String::from_utf8(bytes.to_vec()).unwrap(); + + assert_eq!(&expected, &body); + }); + } #[test] fn password_mismatch() { + const PASSWORD_MISMATCH_FORM: &str = + "username=bad_user&displayname=Bad+User&password=aaaa&pw_verify=bbbb&invitation=0000000000000000000000001A"; + let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { @@ -396,7 +482,7 @@ mod test { let resp = server .post("/signup") // failure to sign up is not failure to submit the request - //.expect_success() + .expect_success() .bytes(body) .content_type(FORM_CONTENT_TYPE) .await; @@ -413,6 +499,9 @@ mod test { #[test] fn password_short() { + const PASSWORD_SHORT_FORM: &str = + "username=bad_user&displayname=Bad+User&password=a&pw_verify=a&invitation=0000000000000000000000001A"; + let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { @@ -439,6 +528,7 @@ mod test { #[test] fn password_long() { + const PASSWORD_LONG_FORM: &str = "username=bad_user&displayname=Bad+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&invitation=0000000000000000000000001A"; let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { @@ -473,10 +563,12 @@ mod test { let pool = get_db_pool(); let rt = Runtime::new().unwrap(); + let invitation: Julid = INVITE_ID_INT.into(); + rt.block_on(async { let server = server_with_pool(&pool).await; let form = - format!("username=bad_user&displayname=Test+User&password={pw}&pw_verify={pw}&invitation=0"); + format!("username=bad_user&displayname=Test+User&password={pw}&pw_verify={pw}&invitation={invitation}"); let body = massage(&form); let resp = server @@ -499,6 +591,9 @@ mod test { #[test] fn username_short() { + const USERNAME_SHORT_FORM: &str = + "username=&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { @@ -525,6 +620,9 @@ mod test { #[test] fn username_long() { + const USERNAME_LONG_FORM: &str = + "username=bad_user12345678901234567890&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; + let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { @@ -588,6 +686,7 @@ mod test { #[test] fn displayname_long() { + const DISPLAYNAME_LONG_FORM: &str = "username=bad_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&invitation=0000000000000000000000001A"; let pool = get_db_pool(); let rt = Runtime::new().unwrap(); rt.block_on(async { diff --git a/src/signup/mod.rs b/src/signup/mod.rs index 078d9f2..64ef5d8 100644 --- a/src/signup/mod.rs +++ b/src/signup/mod.rs @@ -15,14 +15,15 @@ pub struct CreateInviteError(#[from] CreateInviteErrorKind); #[non_exhaustive] pub enum CreateInviteErrorKind { DBError, - TooManyUses, + NoOwner, + Unknown, } -#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] pub struct Invitation { - pub id: Julid, - pub owner: Julid, - pub expires_at: Option, + id: Julid, + owner: Julid, + expires_at: Option, remaining: i16, } @@ -38,7 +39,18 @@ impl Invitation { .await .map_err(|e| { tracing::debug!("Got error creating invite: {e}"); - CreateInviteErrorKind::DBError + match e { + sqlx::Error::Database(dbe) => { + let exit = dbe.code().unwrap_or_default().parse().unwrap_or(0); + // https://www.sqlite.org/rescode.html#constraint_foreignkey + if exit == 787u32 { + CreateInviteErrorKind::NoOwner + } else { + CreateInviteErrorKind::DBError + } + } + _ => CreateInviteErrorKind::Unknown, + } })? .ok_or(CreateInviteErrorKind::DBError.into()) }