add more invitation tests

This commit is contained in:
Joe Ardent 2024-01-15 13:10:13 -08:00
parent c63786a3fc
commit ba3e8625f6
2 changed files with 72 additions and 33 deletions

View file

@ -15,8 +15,6 @@ use unicode_segmentation::UnicodeSegmentation;
use super::{templates::*, Invitation};
use crate::{util::empty_string_as_none, User};
const ID_QUERY: &str = "select * from users where id = $1";
//-************************************************************************
// Error types for user creation
//-************************************************************************
@ -147,6 +145,7 @@ pub async fn get_signup_success(
Path(id): Path<String>,
State(pool): State<SqlitePool>,
) -> Response {
const ID_QUERY: &str = "select * from users where id = ?";
let id = id.trim();
let id = Julid::from_str(id).unwrap_or_default();
let user: User = {
@ -250,6 +249,14 @@ async fn validate_invitation(
if remaining < 1 {
return Err(CreateUserErrorKind::BadInvitation);
}
if let Some(ts) = invitation.expires_at {
let now = chrono::Utc::now().timestamp();
if ts < now {
return Err(CreateUserErrorKind::BadInvitation);
}
}
let _ = sqlx::query("update invites set remaining = ? where id = ?")
.bind(remaining - 1)
.bind(invitation.id)
@ -260,13 +267,6 @@ async fn validate_invitation(
CreateUserErrorKind::UnknownDBError
})?;
if let Some(ts) = invitation.expires_at {
let now = chrono::Utc::now().timestamp();
if ts < now {
return Err(CreateUserErrorKind::BadInvitation);
}
}
Ok(invitation.owner)
}
@ -383,7 +383,7 @@ mod 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 unlucky = "username=unlucky&password=aaaa&pw_verify=aaaa&invitation=0000000000000000000000001A";
let pool = get_db_pool();
let rt = Runtime::new().unwrap();
rt.block_on(async {
@ -444,8 +444,10 @@ mod test {
std::thread::sleep(Duration::from_millis(100));
let username = "too slow";
let tooslow =
format!("username=tooslow&password=aaaa&pw_verify=aaaa&invitation={invite}");
format!("username={username}&password=aaaa&pw_verify=aaaa&invitation={invite}");
let body = massage(&tooslow);
let resp = server
@ -455,7 +457,7 @@ mod test {
.bytes(body)
.content_type(FORM_CONTENT_TYPE)
.await;
let user = User::try_get("unlucky", &pool).await;
let user = User::try_get(username, &pool).await;
assert!(user.is_ok() && user.unwrap().is_none());
let body = String::from_utf8(resp.as_bytes().to_vec()).unwrap();

View file

@ -9,17 +9,19 @@ pub mod templates;
#[Error(desc = "Could not create user.")]
#[non_exhaustive]
#[derive(PartialEq, Eq)]
pub struct CreateInviteError(#[from] CreateInviteErrorKind);
#[Error]
#[non_exhaustive]
#[derive(PartialEq, Eq)]
pub enum CreateInviteErrorKind {
DBError,
NoOwner,
Unknown,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Invitation {
id: Julid,
owner: Julid,
@ -27,6 +29,17 @@ pub struct Invitation {
remaining: i16,
}
impl Default for Invitation {
fn default() -> Self {
Self {
id: 0.into(),
owner: 0.into(),
expires_at: None,
remaining: 1,
}
}
}
impl Invitation {
pub async fn commit(&self, db: &SqlitePool) -> Result<Julid, CreateInviteError> {
sqlx::query_scalar(
@ -39,46 +52,70 @@ impl Invitation {
.await
.map_err(|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);
if let sqlx::Error::Database(e) = e {
let exit = e.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,
} else {
CreateInviteErrorKind::Unknown
}
})?
.ok_or(CreateInviteErrorKind::DBError.into())
.ok_or(CreateInviteErrorKind::Unknown.into())
}
pub fn new(owner: Julid) -> Self {
Self {
id: Julid::alpha(), // stand-in value, will let the db fill it in
owner,
expires_at: None,
remaining: 1,
..Default::default()
}
}
pub fn with_uses(&self, uses: u8) -> Self {
Self {
id: self.id,
owner: self.owner,
expires_at: self.expires_at,
remaining: uses as i16,
..*self
}
}
pub fn with_expires_in(&self, expires_in: Duration) -> Self {
Self {
id: self.id,
owner: self.owner,
expires_at: Some((chrono::Utc::now() + expires_in).timestamp()),
remaining: self.remaining,
..*self
}
}
}
#[cfg(test)]
mod test {
use tokio::runtime::Runtime;
use super::*;
use crate::{get_db_pool, User};
#[test]
fn can_create() {
let pool = get_db_pool();
let rt = Runtime::new().unwrap();
rt.block_on(async {
User::omega().try_insert(&pool).await.unwrap();
let invite = Invitation::new(Julid::omega());
invite.commit(&pool).await.unwrap();
});
}
#[test]
fn bad_owner() {
let pool = get_db_pool();
let rt = Runtime::new().unwrap();
rt.block_on(async {
User::omega().try_insert(&pool).await.unwrap();
let invite = Invitation::new(Julid::alpha());
let res = invite.commit(&pool).await;
assert_eq!(res, Err(CreateInviteErrorKind::NoOwner.into()));
});
}
}