732 lines
26 KiB
Rust
732 lines
26 KiB
Rust
use argon2::{
|
|
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
|
Argon2,
|
|
};
|
|
use axum::{
|
|
extract::{Form, Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use julid::Julid;
|
|
use serde::Deserialize;
|
|
use sqlx::{query_as, Sqlite, SqlitePool};
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
use super::{templates::*, Invitation};
|
|
use crate::{util::empty_string_as_none, User};
|
|
|
|
//-************************************************************************
|
|
// Error types for user creation
|
|
//-************************************************************************
|
|
|
|
#[Error(desc = "Could not create user.")]
|
|
#[non_exhaustive]
|
|
pub struct CreateUserError(#[from] CreateUserErrorKind);
|
|
|
|
impl IntoResponse for CreateUserError {
|
|
fn into_response(self) -> Response {
|
|
match self.0 {
|
|
CreateUserErrorKind::UnknownDBError => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
|
|
}
|
|
CreateUserErrorKind::BadInvitation => (
|
|
StatusCode::OK,
|
|
SignupErrorPage("Sorry, that invitation isn't valid.".to_string()),
|
|
)
|
|
.into_response(),
|
|
_ => (StatusCode::OK, format!("{self}")).into_response(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[Error]
|
|
#[non_exhaustive]
|
|
pub enum CreateUserErrorKind {
|
|
BadInvitation,
|
|
AlreadyExists,
|
|
#[error(desc = "Usernames must be between 1 and 20 characters long")]
|
|
BadUsername,
|
|
PasswordMismatch,
|
|
#[error(desc = "Password must have at least 4 and at most 50 characters")]
|
|
BadPassword,
|
|
#[error(desc = "Display name must be less than 100 characters long")]
|
|
BadDisplayname,
|
|
BadEmail,
|
|
UnknownDBError,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct SignupForm {
|
|
pub username: String,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub displayname: Option<String>,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub email: Option<String>,
|
|
pub password: String,
|
|
pub pw_verify: String,
|
|
pub invitation: String,
|
|
}
|
|
|
|
//-************************************************************************
|
|
// User creation route handlers
|
|
//-************************************************************************
|
|
|
|
/// Get Handler: displays the form to create a user
|
|
#[axum::debug_handler]
|
|
pub async fn get_create_user(
|
|
State(_pool): State<SqlitePool>,
|
|
invitation: Option<Path<String>>,
|
|
) -> Result<impl IntoResponse, CreateUserError> {
|
|
let invitation = invitation.ok_or(CreateUserErrorKind::BadInvitation)?;
|
|
let invitation =
|
|
Julid::from_str(&invitation.0).map_err(|_| CreateUserErrorKind::BadInvitation)?;
|
|
|
|
Ok(SignupPage {
|
|
invitation,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
/// Post Handler: validates form values and calls the actual, private user
|
|
/// creation function
|
|
#[axum::debug_handler]
|
|
pub async fn post_create_user(
|
|
State(pool): State<SqlitePool>,
|
|
Form(signup): Form<SignupForm>,
|
|
) -> Result<impl IntoResponse, CreateUserError> {
|
|
use crate::util::validate_optional_length;
|
|
let username = signup.username.trim();
|
|
let password = signup.password.trim();
|
|
let verify = signup.pw_verify.trim();
|
|
|
|
let name_len = username.graphemes(true).size_hint().1.unwrap();
|
|
// we are not ascii exclusivists around here
|
|
if !(1..=20).contains(&name_len) {
|
|
return Err(CreateUserErrorKind::BadUsername.into());
|
|
}
|
|
|
|
if password != verify {
|
|
return Err(CreateUserErrorKind::PasswordMismatch.into());
|
|
}
|
|
let pwlen = password.graphemes(true).size_hint().1.unwrap_or(0);
|
|
let password = password.as_bytes();
|
|
if !(4..=50).contains(&pwlen) {
|
|
return Err(CreateUserErrorKind::BadPassword.into());
|
|
}
|
|
|
|
// clean up the optionals
|
|
let displayname = validate_optional_length(
|
|
&signup.displayname,
|
|
0..100,
|
|
CreateUserErrorKind::BadDisplayname,
|
|
)?;
|
|
|
|
let email = validate_optional_length(&signup.email, 5..30, CreateUserErrorKind::BadEmail)?;
|
|
|
|
let user = create_user(
|
|
username,
|
|
&displayname,
|
|
&email,
|
|
password,
|
|
&pool,
|
|
&signup.invitation,
|
|
)
|
|
.await?;
|
|
let when = user.id.created_at();
|
|
tracing::debug!("created {user:?} at {when}");
|
|
|
|
let resp = axum::response::Redirect::to(&format!("/signup_success/{}", user.id));
|
|
|
|
Ok(resp)
|
|
}
|
|
|
|
/// Generic handler for successful signup
|
|
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 = {
|
|
query_as(ID_QUERY)
|
|
.bind(id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
let mut resp = SignupSuccessPage(user.clone()).into_response();
|
|
|
|
if user.username.is_empty() || id.is_alpha() {
|
|
// redirect to front page if we got here without a valid user ID
|
|
*resp.status_mut() = StatusCode::SEE_OTHER;
|
|
resp.headers_mut().insert("Location", "/".parse().unwrap());
|
|
}
|
|
|
|
resp
|
|
}
|
|
|
|
//-************************************************************************
|
|
// private fns
|
|
//-************************************************************************
|
|
|
|
pub(crate) async fn create_user(
|
|
username: &str,
|
|
displayname: &Option<String>,
|
|
email: &Option<String>,
|
|
password: &[u8],
|
|
pool: &SqlitePool,
|
|
invitation: &str,
|
|
) -> Result<User, CreateUserError> {
|
|
const CREATE_QUERY: &str =
|
|
"insert into users (username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5) returning *";
|
|
|
|
// Argon2 with default params (Argon2id v19)
|
|
let argon2 = Argon2::default();
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
let pwhash = argon2
|
|
.hash_password(password, &salt)
|
|
.unwrap() // safe to unwrap, we know the salt is valid
|
|
.to_string();
|
|
|
|
let mut tx = pool.begin().await.map_err(|e| {
|
|
tracing::debug!("db error: {e}");
|
|
CreateUserErrorKind::UnknownDBError
|
|
})?;
|
|
|
|
let invitation = Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?;
|
|
let invited_by = validate_invitation(invitation, &mut tx).await?;
|
|
|
|
let user = sqlx::query_as(CREATE_QUERY)
|
|
.bind(username)
|
|
.bind(displayname)
|
|
.bind(email)
|
|
.bind(&pwhash)
|
|
.bind(invited_by)
|
|
.fetch_one(&mut *tx)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::info!("Got error inserting new user: {e}");
|
|
match e {
|
|
sqlx::Error::Database(db) => {
|
|
let exit = db.code().unwrap_or_default().parse().unwrap_or(0);
|
|
// https://www.sqlite.org/rescode.html codes for unique constraint violations:
|
|
if exit == 2067u32 || exit == 1555 {
|
|
CreateUserErrorKind::AlreadyExists
|
|
} else {
|
|
CreateUserErrorKind::UnknownDBError
|
|
}
|
|
}
|
|
_ => CreateUserErrorKind::UnknownDBError,
|
|
}
|
|
})?;
|
|
|
|
tx.commit().await.map_err(|e| {
|
|
tracing::debug!("db error: {e}");
|
|
CreateUserErrorKind::UnknownDBError
|
|
})?;
|
|
|
|
Ok(user)
|
|
}
|
|
|
|
async fn validate_invitation(
|
|
invitation: Julid,
|
|
tx: &mut sqlx::Transaction<'_, Sqlite>,
|
|
) -> Result<Julid, CreateUserErrorKind> {
|
|
let invitation: Invitation = sqlx::query_as("select * from invites where id = ?")
|
|
.bind(invitation)
|
|
.fetch_optional(&mut **tx)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::debug!("db error: {e}");
|
|
CreateUserErrorKind::UnknownDBError
|
|
})?
|
|
.ok_or(CreateUserErrorKind::BadInvitation)?;
|
|
|
|
let remaining = invitation.remaining;
|
|
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)
|
|
.execute(&mut **tx)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::debug!("db error: {e}");
|
|
CreateUserErrorKind::UnknownDBError
|
|
})?;
|
|
|
|
Ok(invitation.owner)
|
|
}
|
|
|
|
//-************************************************************************
|
|
// TESTS
|
|
//-************************************************************************
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use axum::http::StatusCode;
|
|
use julid::Julid;
|
|
use tokio::runtime::Runtime;
|
|
|
|
use crate::{
|
|
db::get_db_pool,
|
|
signup::templates::{SignupPage, SignupSuccessPage},
|
|
test_utils::{massage, server_with_pool, FORM_CONTENT_TYPE, INVITE_ID_INT},
|
|
User,
|
|
};
|
|
|
|
#[test]
|
|
fn post_create_user() {
|
|
let pool = get_db_pool();
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
let server = server_with_pool(&pool).await;
|
|
let id: Julid = INVITE_ID_INT.into();
|
|
let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id);
|
|
let body = massage(&form);
|
|
|
|
let resp = server
|
|
.post("/signup")
|
|
.expect_failure() // 303 is "failure"
|
|
.bytes(body)
|
|
.content_type(FORM_CONTENT_TYPE)
|
|
.await;
|
|
|
|
assert_eq!(StatusCode::SEE_OTHER, resp.status_code());
|
|
|
|
// get the new user from the db
|
|
let user = User::try_get("good_user", &pool).await;
|
|
assert!(user.is_ok());
|
|
assert!(user.unwrap().is_some());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn get_create_user() {
|
|
let pool = get_db_pool();
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
let server = server_with_pool(&pool).await;
|
|
let invitation: Julid = INVITE_ID_INT.into();
|
|
let path = format!("/signup/{invitation}");
|
|
let resp = server.get(&path).await;
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = SignupPage {
|
|
invitation,
|
|
..Default::default()
|
|
}
|
|
.to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn handle_signup_success() {
|
|
let pool = get_db_pool();
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
let server = server_with_pool(&pool).await;
|
|
let id: Julid = INVITE_ID_INT.into();
|
|
let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id);
|
|
let body = massage(&form);
|
|
|
|
let resp = server
|
|
.post("/signup")
|
|
.expect_failure() // 303 is "failure"
|
|
.bytes(body)
|
|
.content_type(FORM_CONTENT_TYPE)
|
|
.await;
|
|
|
|
assert_eq!(StatusCode::SEE_OTHER, resp.status_code());
|
|
|
|
// get the new user from the db
|
|
let user = User::try_get("good_user", &pool).await.unwrap().unwrap();
|
|
let id = user.id;
|
|
|
|
|
|
let path = format!("/signup_success/{id}");
|
|
|
|
let resp = server.get(&path).expect_success().await;
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = SignupSuccessPage(user).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
//-************************************************************************
|
|
// honestly this is basically the whole suite here
|
|
//-************************************************************************
|
|
mod failure {
|
|
use std::time::Duration;
|
|
|
|
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=unlucky&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))
|
|
.commit(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
let username = "too slow";
|
|
|
|
let tooslow =
|
|
format!("username={username}&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(username, &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 {
|
|
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("bad_user", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::PasswordMismatch).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[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 {
|
|
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("bad_user", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadPassword).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[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 {
|
|
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("bad_user", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadPassword).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn multibyte_password_too_short() {
|
|
let pw = "🤡";
|
|
// min length is 4 distinct graphemes; this is one grapheme that is four bytes,
|
|
// so it's not valid
|
|
assert_eq!(pw.len(), 4);
|
|
|
|
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={invitation}");
|
|
let body = massage(&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("bad_user", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadPassword).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[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 {
|
|
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("", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadUsername).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[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 {
|
|
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("bad_user12345678901234567890", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadUsername).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn username_duplicate() {
|
|
let pool = get_db_pool();
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
let server = server_with_pool(&pool).await;
|
|
let id: Julid = INVITE_ID_INT.into();
|
|
let form = format!("username=good_user&displayname=Good+User&password=aaaa&pw_verify=aaaa&invitation={}", id);
|
|
let body = massage(&form);
|
|
//let body = massage(GOOD_FORM);
|
|
|
|
let _resp = server
|
|
.post("/signup")
|
|
.expect_failure() // 303 is "failure"
|
|
.bytes(body.clone())
|
|
.content_type(FORM_CONTENT_TYPE)
|
|
.await;
|
|
|
|
// get the new user from the db
|
|
let user = User::try_get("good_user", &pool).await;
|
|
assert!(user.unwrap().is_some());
|
|
|
|
// now try again
|
|
let resp = server
|
|
.post("/signup")
|
|
.expect_success()
|
|
.bytes(body)
|
|
.content_type(FORM_CONTENT_TYPE)
|
|
.await;
|
|
|
|
assert_eq!(resp.status_code(), StatusCode::OK);
|
|
let expected = CreateUserError(CreateUserErrorKind::AlreadyExists).to_string();
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[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 {
|
|
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("bad_user", &pool).await;
|
|
assert!(user.is_ok() && user.unwrap().is_none());
|
|
|
|
let body = std::str::from_utf8(resp.as_bytes()).unwrap();
|
|
let expected = CreateUserError(CreateUserErrorKind::BadDisplayname).to_string();
|
|
assert_eq!(&expected, body);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn handle_signup_success() {
|
|
let pool = get_db_pool();
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let server = server_with_pool(&pool).await;
|
|
|
|
let path = "/signup_success/nope";
|
|
|
|
let resp = server.get(path).expect_failure().await;
|
|
assert_eq!(resp.status_code(), StatusCode::SEE_OTHER);
|
|
});
|
|
}
|
|
}
|
|
}
|