Add tests for bad invite signups.

This commit is contained in:
Joe Ardent 2024-01-14 22:16:50 -08:00
parent c2bad4bb94
commit c63786a3fc
3 changed files with 135 additions and 24 deletions

View file

@ -8,7 +8,7 @@ use sqlx::{
const MAX_CONNS: u32 = 200; const MAX_CONNS: u32 = 200;
const MIN_CONNS: u32 = 5; const MIN_CONNS: u32 = 5;
const TIMEOUT: u64 = 11; const TIMEOUT: u64 = 3;
pub fn get_db_pool() -> SqlitePool { pub fn get_db_pool() -> SqlitePool {
let db_filename = { let db_filename = {

View file

@ -196,8 +196,7 @@ pub(crate) async fn create_user(
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::UnknownDBError
})?; })?;
let invitation = let invitation = Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?;
Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?;
let invited_by = validate_invitation(invitation, &mut tx).await?; let invited_by = validate_invitation(invitation, &mut tx).await?;
@ -370,23 +369,110 @@ mod test {
// honestly this is basically the whole suite here // honestly this is basically the whole suite here
//-************************************************************************ //-************************************************************************
mod failure { mod failure {
use super::*; use std::time::Duration;
use crate::signup::handlers::{CreateUserError, CreateUserErrorKind};
// various ways to fuck up signup use axum::response::IntoResponse;
const PASSWORD_MISMATCH_FORM: &str =
"username=bad_user&displayname=Bad+User&password=aaaa&pw_verify=bbbb&invitation=0000000000000000000000001A"; use super::*;
const PASSWORD_SHORT_FORM: &str = use crate::{
"username=bad_user&displayname=Bad+User&password=a&pw_verify=a&invitation=0000000000000000000000001A"; signup::handlers::{CreateUserError, CreateUserErrorKind},
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"; Invitation,
const USERNAME_SHORT_FORM: &str = };
"username=&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A";
const USERNAME_LONG_FORM: &str = #[test]
"username=bad_user12345678901234567890&displayname=Bad+User&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A"; fn used_up_invite() {
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 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] #[test]
fn password_mismatch() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@ -396,7 +482,7 @@ mod test {
let resp = server let resp = server
.post("/signup") .post("/signup")
// failure to sign up is not failure to submit the request // failure to sign up is not failure to submit the request
//.expect_success() .expect_success()
.bytes(body) .bytes(body)
.content_type(FORM_CONTENT_TYPE) .content_type(FORM_CONTENT_TYPE)
.await; .await;
@ -413,6 +499,9 @@ mod test {
#[test] #[test]
fn password_short() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@ -439,6 +528,7 @@ mod test {
#[test] #[test]
fn password_long() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@ -473,10 +563,12 @@ mod test {
let pool = get_db_pool(); let pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
let invitation: Julid = INVITE_ID_INT.into();
rt.block_on(async { rt.block_on(async {
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let form = 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 body = massage(&form);
let resp = server let resp = server
@ -499,6 +591,9 @@ mod test {
#[test] #[test]
fn username_short() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@ -525,6 +620,9 @@ mod test {
#[test] #[test]
fn username_long() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
@ -588,6 +686,7 @@ mod test {
#[test] #[test]
fn displayname_long() { 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 pool = get_db_pool();
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {

View file

@ -15,14 +15,15 @@ pub struct CreateInviteError(#[from] CreateInviteErrorKind);
#[non_exhaustive] #[non_exhaustive]
pub enum CreateInviteErrorKind { pub enum CreateInviteErrorKind {
DBError, 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 struct Invitation {
pub id: Julid, id: Julid,
pub owner: Julid, owner: Julid,
pub expires_at: Option<i64>, expires_at: Option<i64>,
remaining: i16, remaining: i16,
} }
@ -38,7 +39,18 @@ impl Invitation {
.await .await
.map_err(|e| { .map_err(|e| {
tracing::debug!("Got error creating invite: {e}"); tracing::debug!("Got error creating invite: {e}");
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::DBError
}
}
_ => CreateInviteErrorKind::Unknown,
}
})? })?
.ok_or(CreateInviteErrorKind::DBError.into()) .ok_or(CreateInviteErrorKind::DBError.into())
} }