summaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/error.rs19
-rw-r--r--src/api/users.rs184
2 files changed, 73 insertions, 130 deletions
diff --git a/src/api/error.rs b/src/api/error.rs
index 2af7228..10b5468 100644
--- a/src/api/error.rs
+++ b/src/api/error.rs
@@ -1,7 +1,7 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
- Sqlx(#[from] sqlx::Error),
+ Sqlx(#[source] sqlx::Error),
#[error("Route not found: {0}")]
RouteNotFound(axum::http::Uri),
@@ -22,7 +22,7 @@ pub enum Error {
InvalidToken,
#[error("User with that email already exists")]
- EmailExists,
+ UserExists,
#[error("Invalid email: {0}")]
EmailInvalid(#[from] email_address::Error),
@@ -34,6 +34,19 @@ pub enum Error {
Auth(#[from] crate::auth::error::Error),
}
+impl From<sqlx::Error> for Error {
+ fn from(value: sqlx::Error) -> Self {
+ match value {
+ sqlx::Error::Database(db_err)
+ if db_err.is_unique_violation() && db_err.table().is_some_and(|s| s == "user_") =>
+ {
+ Error::UserExists
+ }
+ err => Error::Sqlx(err),
+ }
+ }
+}
+
impl From<axum_extra::typed_header::TypedHeaderRejection> for Error {
fn from(value: axum_extra::typed_header::TypedHeaderRejection) -> Self {
if value.is_missing() {
@@ -53,7 +66,7 @@ impl axum::response::IntoResponse for Error {
Self::RouteNotFound(_) | Self::UserNotFound | Self::TaskNotFound => {
StatusCode::NOT_FOUND
}
- Self::EmailExists => StatusCode::CONFLICT,
+ Self::UserExists => StatusCode::CONFLICT,
Self::InvalidToken => StatusCode::UNAUTHORIZED,
Self::HeaderNotFound(ref h) if h == AUTHORIZATION => StatusCode::UNAUTHORIZED,
Self::HeaderNotFound(_) => StatusCode::BAD_REQUEST,
diff --git a/src/api/users.rs b/src/api/users.rs
index e07bf7e..d4e5d57 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -46,36 +46,29 @@ pub async fn create(
email,
password,
}): Json<Registration>,
-) -> impl IntoResponse {
+) -> Result<impl IntoResponse, Error> {
email_address::EmailAddress::from_str(&email)?;
- let exists: Option<bool> = sqlx::query_scalar!(
- "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)",
- email.to_ascii_lowercase()
- )
- .fetch_one(&pool)
- .await?;
-
- if exists.is_some_and(|b| b) {
- return Err(Error::EmailExists);
- }
-
- // TODO: Move this into a micro service, possibly behind a feature flag.
- let (status, (access, refresh)) =
- crate::auth::credentials::create(State(pool.clone()), Json(Credential { password }))
- .await?;
-
let user = sqlx::query_as!(
User,
- "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *",
- refresh.sub,
+ "INSERT INTO user_ (name,email) VALUES ($1, $2) RETURNING *",
name,
email.to_ascii_lowercase(),
)
.fetch_one(&pool)
.await?;
- Ok((status, access, refresh, Json(user)))
+ // TODO: Move this into a micro service, possibly behind a feature flag.
+ crate::auth::credentials::create(
+ State(pool.clone()),
+ Json(Credential {
+ id: user.id,
+ password,
+ }),
+ )
+ .await
+ .map(|(status, claims)| (status, claims, Json(user)))
+ .map_err(Into::into)
}
pub async fn show(
@@ -116,30 +109,31 @@ mod tests {
const USER_EMAIL: &str = "adent@earth.sol";
const USER_PASSWORD: &str = "solongandthanksforallthefish";
- #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
- async fn test_get_ok_self(pool: PgPool) -> TestResult {
+ #[sqlx::test]
+ async fn test_users_post_created(pool: PgPool) -> TestResult {
setup_test_env();
let router = Router::new().merge(router()).with_state(AppState { pool });
+ let user = serde_json::json!( {
+ "name": USER_NAME,
+ "email": USER_EMAIL,
+ "password": USER_PASSWORD,
+ });
+
let request = Request::builder()
- .uri(format!("/users/{}", USER_ID))
- .header(
- COOKIE,
- AccessClaims::issue(USER_ID).as_cookie()?.to_string(),
- )
- .body(Body::empty())?;
+ .uri("/users")
+ .method("POST")
+ .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user)?))?;
let response = router.oneshot(request).await?;
- assert_eq!(StatusCode::OK, response.status());
+ assert_eq!(StatusCode::CREATED, response.status());
let body_bytes = response.into_body().collect().await?.to_bytes();
- let User {
- id, name, email, ..
- } = serde_json::from_slice(&body_bytes)?;
+ let User { name, email, .. } = serde_json::from_slice(&body_bytes)?;
- assert_eq!(USER_ID, id);
assert_eq!(USER_NAME, name);
assert_eq!(USER_EMAIL, email);
@@ -147,7 +141,32 @@ mod tests {
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
- async fn test_get_ok_other(pool: PgPool) -> TestResult {
+ async fn test_users_post_conflict(pool: PgPool) -> TestResult {
+ setup_test_env();
+
+ let router = Router::new().merge(router()).with_state(AppState { pool });
+
+ let user = serde_json::json!( {
+ "name": USER_NAME,
+ "email": USER_EMAIL,
+ "password": USER_PASSWORD,
+ });
+
+ let request = Request::builder()
+ .uri("/users")
+ .method("POST")
+ .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user)?))?;
+
+ let response = router.oneshot(request).await?;
+
+ assert_eq!(StatusCode::CONFLICT, response.status());
+
+ Ok(())
+ }
+
+ #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
+ async fn test_users_get_ok(pool: PgPool) -> TestResult {
setup_test_env();
let router = Router::new().merge(router()).with_state(AppState { pool });
@@ -179,7 +198,7 @@ mod tests {
}
#[sqlx::test]
- async fn test_get_not_found(pool: PgPool) -> TestResult {
+ async fn test_users_get_not_found(pool: PgPool) -> TestResult {
setup_test_env();
let router = Router::new().merge(router()).with_state(AppState { pool });
@@ -188,7 +207,9 @@ mod tests {
.uri(format!("/users/{}", USER_ID))
.header(
COOKIE,
- AccessClaims::issue(USER_ID).as_cookie()?.to_string(),
+ AccessClaims::issue(uuid::Uuid::new_v4())
+ .as_cookie()?
+ .to_string(),
)
.body(Body::empty())?;
@@ -198,95 +219,4 @@ mod tests {
Ok(())
}
-
- #[sqlx::test]
- async fn test_get_unauthorized_invalid_token_format(pool: PgPool) -> TestResult {
- setup_test_env();
-
- let router = Router::new().merge(router()).with_state(AppState { pool });
-
- let request = Request::builder()
- .uri(format!("/users/{}", USER_ID))
- .header(COOKIE, "token=sadfasdfsdfs")
- .body(Body::empty())?;
-
- let response = router.oneshot(request).await?;
-
- assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
-
- Ok(())
- }
-
- #[sqlx::test]
- async fn test_get_unauthorized_missing_token(pool: PgPool) -> TestResult {
- setup_test_env();
-
- let router = Router::new().merge(router()).with_state(AppState { pool });
-
- let request = Request::builder()
- .uri(format!("/users/{}", USER_ID))
- .body(Body::empty())?;
-
- let response = router.oneshot(request).await?;
-
- assert_eq!(StatusCode::UNAUTHORIZED, response.status());
-
- Ok(())
- }
-
- #[sqlx::test]
- async fn test_post_created(pool: PgPool) -> TestResult {
- setup_test_env();
-
- let router = Router::new().merge(router()).with_state(AppState { pool });
-
- let user = serde_json::json!( {
- "name": USER_NAME,
- "email": USER_EMAIL,
- "password": USER_PASSWORD,
- });
-
- let request = Request::builder()
- .uri("/users")
- .method("POST")
- .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user)?))?;
-
- let response = router.oneshot(request).await?;
-
- assert_eq!(StatusCode::CREATED, response.status());
-
- let body_bytes = response.into_body().collect().await?.to_bytes();
- let User { name, email, .. } = serde_json::from_slice(&body_bytes)?;
-
- assert_eq!(USER_NAME, name);
- assert_eq!(USER_EMAIL, email);
-
- Ok(())
- }
-
- #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
- async fn test_post_conflict(pool: PgPool) -> TestResult {
- setup_test_env();
-
- let router = Router::new().merge(router()).with_state(AppState { pool });
-
- let user = serde_json::json!( {
- "name": USER_NAME,
- "email": USER_EMAIL,
- "password": USER_PASSWORD,
- });
-
- let request = Request::builder()
- .uri("/users")
- .method("POST")
- .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user)?))?;
-
- let response = router.oneshot(request).await?;
-
- assert_eq!(StatusCode::CONFLICT, response.status());
-
- Ok(())
- }
}