use std::sync::Arc; use axum::{extract::State, response::IntoResponse, Extension, Json}; use axum_extra::routing::TypedPath; use serde::Deserialize; use crate::{jwt::Claims, model::UserSchema, state::AppState, Error}; #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/user/:uuid")] pub struct UserUuid { pub uuid: uuid::Uuid, } impl UserUuid { /// Get a user with a specific `uuid` #[tracing::instrument] pub async fn get(self, State(state): State>) -> impl IntoResponse { sqlx::query_as!(UserSchema, "SELECT * FROM users WHERE uuid = $1", self.uuid) .fetch_optional(&state.pool) .await? .ok_or_else(|| Error::UserNotFound) .map(Json) } } #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/user")] pub struct User; impl User { #[tracing::instrument] pub async fn get( self, State(state): State>, Extension(Claims { sub, iat, exp }): Extension, ) -> Result { sqlx::query_as!(UserSchema, "SELECT * FROM users WHERE uuid = $1", sub) .fetch_optional(&state.pool) .await? .ok_or_else(|| Error::UserNotFound) .map(Json) } } #[cfg(test)] mod tests { use super::*; use axum::{ body::Body, http::{header::AUTHORIZATION, Request, StatusCode}, }; use http_body_util::BodyExt; use sqlx::PgPool; use tower::ServiceExt; use crate::{init_router, model::UserSchema}; const JWT_SECRET: &str = "test-jwt-secret-token"; const JWT_MAX_AGE: time::Duration = time::Duration::HOUR; const UUID: uuid::Uuid = uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"); type TestResult> = std::result::Result; #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))] async fn test_user_uuid_ok(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let user = UserSchema { uuid: UUID, name: "Arthur Dent".to_string(), email: "adent@earth.sol".to_string(), ..Default::default() }; let request = Request::builder() .uri(format!("/api/user/{}", user.uuid)) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::OK, response.status()); let body_bytes = response.into_body().collect().await?.to_bytes(); let UserSchema { uuid, name, email, .. } = serde_json::from_slice(&body_bytes)?; assert_eq!(user.uuid, uuid); assert_eq!(user.name, name); assert_eq!(user.email, email); Ok(()) } #[sqlx::test] async fn test_user_uuid_not_found(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let user = UserSchema { uuid: UUID, name: "Arthur Dent".to_string(), email: "adent@earth.sol".to_string(), ..Default::default() }; let request = Request::builder() .uri(format!("/api/user/{}", user.uuid)) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::NOT_FOUND, response.status()); Ok(()) } #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))] async fn test_user_ok(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let token = Claims::new(UUID, JWT_MAX_AGE).encode(JWT_SECRET.as_ref())?; let request = Request::builder() .uri("/api/user") .header(AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::OK, response.status()); let body_bytes = response.into_body().collect().await?.to_bytes(); let UserSchema { uuid, name, email, .. } = serde_json::from_slice(&body_bytes)?; assert_eq!(UUID, uuid); assert_eq!("Arthur Dent", name); assert_eq!("adent@earth.sol", email); Ok(()) } #[sqlx::test] async fn test_user_unauthorized_bad_token(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let token = Claims::new(UUID, JWT_MAX_AGE).encode("BAD_SECRET".as_ref())?; let request = Request::builder() .uri("/api/user") .header(AUTHORIZATION, format!("Bearer {token}")) .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); Ok(()) } #[sqlx::test] async fn test_user_unauthorized_invalid_token(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let request = Request::builder() .uri("/api/user") .header(AUTHORIZATION, "Bearer invalidtoken") .body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); Ok(()) } #[sqlx::test] async fn test_user_unauthorized_missing_token(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let request = Request::builder().uri("/api/user").body(Body::empty())?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::BAD_REQUEST, response.status()); Ok(()) } }