summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/routes.rs353
-rw-r--r--src/routes/healthcheck.rs22
-rw-r--r--src/routes/login.rs140
-rw-r--r--src/routes/register.rs139
-rw-r--r--src/routes/user.rs106
5 files changed, 423 insertions, 337 deletions
diff --git a/src/routes.rs b/src/routes.rs
index 1ec4e30..d9a2a0b 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,170 +1,30 @@
-use std::{str::FromStr, sync::Arc};
+use std::sync::Arc;
-use argon2::{
- password_hash::{rand_core::OsRng, SaltString},
- Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
-};
use axum::{
- extract::State,
- http::{header::SET_COOKIE, StatusCode, Uri},
+ http::{StatusCode, Uri},
response::IntoResponse,
- Json,
-};
-use axum_extra::{
- extract::cookie::{Cookie, SameSite},
- routing::{RouterExt, TypedPath},
};
-use jsonwebtoken::{EncodingKey, Header};
-use serde::Deserialize;
+use axum_extra::routing::RouterExt;
-use crate::{
- model::{LoginSchema, RegisterSchema, TokenClaims, User},
- state::AppState,
- Error,
-};
+use crate::state::AppState;
+
+mod healthcheck;
+mod login;
+mod register;
+mod user;
#[tracing::instrument]
pub fn init_router(state: Arc<AppState>) -> axum::Router {
axum::Router::new()
// .route("/api/user", get(get_user))
- .typed_get(HealthCheck::get)
- .typed_get(UserUuid::get)
- .typed_post(Register::post)
- .typed_post(Login::post)
+ .typed_get(healthcheck::HealthCheck::get)
+ .typed_get(user::UserUuid::get)
+ .typed_post(register::Register::post)
+ .typed_post(login::Login::post)
.fallback(fallback)
.with_state(state)
}
-#[derive(Debug, Deserialize, TypedPath)]
-#[typed_path("/api/healthcheck")]
-pub struct HealthCheck;
-
-impl HealthCheck {
- #[tracing::instrument]
- pub async fn get(self) -> impl IntoResponse {
- const MESSAGE: &str = "Unnamed server";
-
- let json_response = serde_json::json!({
- "status": "success",
- "message": MESSAGE
- });
-
- Json(json_response)
- }
-}
-
-#[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<Arc<AppState>>) -> impl IntoResponse {
- sqlx::query_as!(User, "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/register")]
-pub struct Register;
-
-impl Register {
- #[tracing::instrument(skip(password))]
- pub async fn post(
- self,
- State(state): State<Arc<AppState>>,
- Json(RegisterSchema {
- name,
- email,
- password,
- }): Json<RegisterSchema>,
- ) -> impl IntoResponse {
- email_address::EmailAddress::from_str(&email)?;
-
- let exists: Option<bool> =
- sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)")
- .bind(email.to_ascii_lowercase())
- .fetch_one(&state.pool)
- .await?;
-
- if exists.is_some_and(|b| b) {
- return Err(Error::EmailExists);
- }
-
- let salt = SaltString::generate(&mut OsRng);
- let password_hash = Argon2::default().hash_password(password.as_bytes(), &salt)?;
-
- let user = sqlx::query_as!(
- User,
- "INSERT INTO users (name,email,password_hash) VALUES ($1, $2, $3) RETURNING *",
- name,
- email.to_ascii_lowercase(),
- password_hash.to_string()
- )
- .fetch_one(&state.pool)
- .await?;
-
- Ok((StatusCode::CREATED, Json(user)))
- }
-}
-
-#[derive(Debug, Deserialize, TypedPath)]
-#[typed_path("/api/login")]
-pub struct Login;
-
-impl Login {
- #[tracing::instrument(skip(state, password))]
- pub async fn post(
- self,
- State(state): State<Arc<AppState>>,
- Json(LoginSchema { email, password }): Json<LoginSchema>,
- ) -> Result<impl IntoResponse, Error> {
- let User {
- uuid,
- password_hash,
- ..
- } = sqlx::query_as!(
- User,
- "SELECT * FROM users WHERE email = $1",
- email.to_ascii_lowercase()
- )
- .fetch_optional(&state.pool)
- .await?
- .ok_or(Error::LoginInvalid)?;
-
- Argon2::default()
- .verify_password(password.as_bytes(), &PasswordHash::new(&password_hash)?)?;
-
- let token = jsonwebtoken::encode(
- &Header::default(),
- &TokenClaims::new(uuid, state.jwt_max_age),
- &EncodingKey::from_secret(state.jwt_secret.as_ref()),
- )?;
-
- let cookie = Cookie::build(("token", token.to_owned()))
- .path("/")
- .max_age(state.jwt_max_age)
- .same_site(SameSite::Lax)
- .http_only(true)
- .build();
-
- let mut response = Json(token).into_response();
-
- response
- .headers_mut()
- .insert(SET_COOKIE, cookie.to_string().parse().unwrap());
-
- Ok(response)
- }
-}
-
pub async fn fallback(uri: Uri) -> impl IntoResponse {
(StatusCode::NOT_FOUND, format!("Route not found: {uri}"))
}
@@ -175,12 +35,13 @@ mod tests {
use axum::{
body::Body,
- http::{header, Request, StatusCode},
+ http::{Request, StatusCode},
};
- use http_body_util::BodyExt;
use sqlx::PgPool;
use tower::ServiceExt;
+ use crate::Error;
+
const JWT_SECRET: &str = "test-jwt-secret-token";
const JWT_MAX_AGE: time::Duration = time::Duration::HOUR;
@@ -207,186 +68,4 @@ mod tests {
Ok(())
}
-
- #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))]
- async fn test_user_ok(pool: PgPool) -> Result<(), Error> {
- 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 = User {
- uuid: uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"),
- 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.unwrap();
-
- assert_eq!(StatusCode::OK, response.status());
-
- let body_bytes = response.into_body().collect().await?.to_bytes();
- let User {
- 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_not_found(pool: PgPool) -> Result<(), Error> {
- 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 = User {
- uuid: uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"),
- 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.unwrap();
-
- assert_eq!(StatusCode::NOT_FOUND, response.status());
-
- Ok(())
- }
-
- #[sqlx::test]
- async fn test_register_created(pool: PgPool) -> Result<(), Error> {
- 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 = RegisterSchema {
- name: "Arthur Dent".to_string(),
- email: "adent@earth.sol".to_string(),
- password: "solongandthanksforallthefish".to_string(),
- };
-
- let request = Request::builder()
- .uri("/api/register")
- .method("POST")
- .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
-
- let response = router.oneshot(request).await.unwrap();
-
- 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_register_conflict(pool: PgPool) -> Result<(), Error> {
- 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 = RegisterSchema {
- name: "Arthur Dent".to_string(),
- email: "adent@earth.sol".to_string(),
- password: "solongandthanksforallthefish".to_string(),
- };
-
- let request = Request::builder()
- .uri("/api/register")
- .method("POST")
- .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
-
- let response = router.oneshot(request).await.unwrap();
-
- assert_eq!(StatusCode::CONFLICT, response.status());
-
- Ok(())
- }
-
- #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))]
- async fn test_login_unauthorized(pool: PgPool) -> Result<(), Error> {
- 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 = LoginSchema {
- email: "adent@earth.sol".to_string(),
- password: "hunter2".to_string(),
- };
-
- let request = Request::builder()
- .uri("/api/login")
- .method("POST")
- .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
-
- let response = router.oneshot(request).await.unwrap();
-
- assert_eq!(StatusCode::UNAUTHORIZED, response.status());
-
- Ok(())
- }
-
- #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))]
- async fn test_login_ok(pool: PgPool) -> Result<(), Error> {
- 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 = LoginSchema {
- email: "adent@earth.sol".to_string(),
- password: "solongandthanksforallthefish".to_string(),
- };
-
- let response = router
- .oneshot(
- Request::builder()
- .uri("/api/login")
- .method("POST")
- .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
- .body(Body::from(serde_json::to_vec(&user).unwrap()))?,
- )
- .await
- .unwrap();
-
- assert_eq!(StatusCode::OK, response.status());
-
- Ok(())
- }
}
diff --git a/src/routes/healthcheck.rs b/src/routes/healthcheck.rs
new file mode 100644
index 0000000..7627336
--- /dev/null
+++ b/src/routes/healthcheck.rs
@@ -0,0 +1,22 @@
+use axum::{response::IntoResponse, Json};
+use axum_extra::routing::TypedPath;
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize, TypedPath)]
+#[typed_path("/api/healthcheck")]
+pub struct HealthCheck;
+
+impl HealthCheck {
+ #[tracing::instrument]
+ pub async fn get(self) -> impl IntoResponse {
+ const MESSAGE: &str = "Unnamed server";
+
+ let json_response = serde_json::json!({
+ "status": "success",
+ "message": MESSAGE
+ });
+
+ Json(json_response)
+ }
+}
+
diff --git a/src/routes/login.rs b/src/routes/login.rs
new file mode 100644
index 0000000..12442e8
--- /dev/null
+++ b/src/routes/login.rs
@@ -0,0 +1,140 @@
+use std::sync::Arc;
+
+use argon2::{Argon2, PasswordHash, PasswordVerifier};
+use axum::{extract::State, http::header::SET_COOKIE, response::IntoResponse, Json};
+use axum_extra::{
+ extract::cookie::{Cookie, SameSite},
+ routing::TypedPath,
+};
+use jsonwebtoken::{EncodingKey, Header};
+use serde::Deserialize;
+
+use crate::{
+ model::{LoginSchema, TokenClaims, User},
+ state::AppState,
+ Error,
+};
+
+#[derive(Debug, Deserialize, TypedPath)]
+#[typed_path("/api/login")]
+pub struct Login;
+
+impl Login {
+ #[tracing::instrument(skip(state, password))]
+ pub async fn post(
+ self,
+ State(state): State<Arc<AppState>>,
+ Json(LoginSchema { email, password }): Json<LoginSchema>,
+ ) -> Result<impl IntoResponse, Error> {
+ let User {
+ uuid,
+ password_hash,
+ ..
+ } = sqlx::query_as!(
+ User,
+ "SELECT * FROM users WHERE email = $1",
+ email.to_ascii_lowercase()
+ )
+ .fetch_optional(&state.pool)
+ .await?
+ .ok_or(Error::LoginInvalid)?;
+
+ Argon2::default()
+ .verify_password(password.as_bytes(), &PasswordHash::new(&password_hash)?)?;
+
+ let token = jsonwebtoken::encode(
+ &Header::default(),
+ &TokenClaims::new(uuid, state.jwt_max_age),
+ &EncodingKey::from_secret(state.jwt_secret.as_ref()),
+ )?;
+
+ let cookie = Cookie::build(("token", token.to_owned()))
+ .path("/")
+ .max_age(state.jwt_max_age)
+ .same_site(SameSite::Lax)
+ .http_only(true)
+ .build();
+
+ let mut response = Json(token).into_response();
+
+ response
+ .headers_mut()
+ .insert(SET_COOKIE, cookie.to_string().parse().unwrap());
+
+ Ok(response)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use axum::{
+ body::Body,
+ http::{header, Request, StatusCode},
+ };
+ use sqlx::PgPool;
+ use tower::ServiceExt;
+
+ use crate::init_router;
+
+ const JWT_SECRET: &str = "test-jwt-secret-token";
+ const JWT_MAX_AGE: time::Duration = time::Duration::HOUR;
+
+ #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
+ async fn test_login_unauthorized(pool: PgPool) -> Result<(), Error> {
+ 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 = LoginSchema {
+ email: "adent@earth.sol".to_string(),
+ password: "hunter2".to_string(),
+ };
+
+ let request = Request::builder()
+ .uri("/api/login")
+ .method("POST")
+ .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
+
+ let response = router.oneshot(request).await.unwrap();
+
+ assert_eq!(StatusCode::UNAUTHORIZED, response.status());
+
+ Ok(())
+ }
+
+ #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
+ async fn test_login_ok(pool: PgPool) -> Result<(), Error> {
+ 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 = LoginSchema {
+ email: "adent@earth.sol".to_string(),
+ password: "solongandthanksforallthefish".to_string(),
+ };
+
+ let response = router
+ .oneshot(
+ Request::builder()
+ .uri("/api/login")
+ .method("POST")
+ .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user).unwrap()))?,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(StatusCode::OK, response.status());
+
+ Ok(())
+ }
+}
diff --git a/src/routes/register.rs b/src/routes/register.rs
new file mode 100644
index 0000000..1c0f82d
--- /dev/null
+++ b/src/routes/register.rs
@@ -0,0 +1,139 @@
+use std::{str::FromStr, sync::Arc};
+
+use argon2::{
+ password_hash::{rand_core::OsRng, SaltString},
+ Argon2, PasswordHasher,
+};
+use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
+use axum_extra::routing::TypedPath;
+use serde::Deserialize;
+
+use crate::{
+ model::{RegisterSchema, User},
+ state::AppState,
+ Error,
+};
+
+#[derive(Debug, Deserialize, TypedPath)]
+#[typed_path("/api/register")]
+pub struct Register;
+
+impl Register {
+ #[tracing::instrument(skip(password))]
+ pub async fn post(
+ self,
+ State(state): State<Arc<AppState>>,
+ Json(RegisterSchema {
+ name,
+ email,
+ password,
+ }): Json<RegisterSchema>,
+ ) -> impl IntoResponse {
+ email_address::EmailAddress::from_str(&email)?;
+
+ let exists: Option<bool> =
+ sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)")
+ .bind(email.to_ascii_lowercase())
+ .fetch_one(&state.pool)
+ .await?;
+
+ if exists.is_some_and(|b| b) {
+ return Err(Error::EmailExists);
+ }
+
+ let salt = SaltString::generate(&mut OsRng);
+ let password_hash = Argon2::default().hash_password(password.as_bytes(), &salt)?;
+
+ let user = sqlx::query_as!(
+ User,
+ "INSERT INTO users (name,email,password_hash) VALUES ($1, $2, $3) RETURNING *",
+ name,
+ email.to_ascii_lowercase(),
+ password_hash.to_string()
+ )
+ .fetch_one(&state.pool)
+ .await?;
+
+ Ok((StatusCode::CREATED, Json(user)))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::init_router;
+
+ use super::*;
+
+ use axum::{
+ body::Body,
+ http::{header, Request, StatusCode},
+ };
+ use http_body_util::BodyExt;
+ use sqlx::PgPool;
+ use tower::ServiceExt;
+
+ const JWT_SECRET: &str = "test-jwt-secret-token";
+ const JWT_MAX_AGE: time::Duration = time::Duration::HOUR;
+
+ #[sqlx::test]
+ async fn test_register_created(pool: PgPool) -> Result<(), Error> {
+ 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 = RegisterSchema {
+ name: "Arthur Dent".to_string(),
+ email: "adent@earth.sol".to_string(),
+ password: "solongandthanksforallthefish".to_string(),
+ };
+
+ let request = Request::builder()
+ .uri("/api/register")
+ .method("POST")
+ .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
+
+ let response = router.oneshot(request).await.unwrap();
+
+ 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_register_conflict(pool: PgPool) -> Result<(), Error> {
+ 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 = RegisterSchema {
+ name: "Arthur Dent".to_string(),
+ email: "adent@earth.sol".to_string(),
+ password: "solongandthanksforallthefish".to_string(),
+ };
+
+ let request = Request::builder()
+ .uri("/api/register")
+ .method("POST")
+ .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
+ .body(Body::from(serde_json::to_vec(&user).unwrap()))?;
+
+ let response = router.oneshot(request).await.unwrap();
+
+ assert_eq!(StatusCode::CONFLICT, response.status());
+
+ Ok(())
+ }
+}
diff --git a/src/routes/user.rs b/src/routes/user.rs
new file mode 100644
index 0000000..71ed9a0
--- /dev/null
+++ b/src/routes/user.rs
@@ -0,0 +1,106 @@
+use std::sync::Arc;
+
+use axum::{extract::State, response::IntoResponse, Json};
+use axum_extra::routing::TypedPath;
+use serde::Deserialize;
+
+use crate::{model::User, 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<Arc<AppState>>) -> impl IntoResponse {
+ sqlx::query_as!(User, "SELECT * FROM users WHERE uuid = $1", self.uuid)
+ .fetch_optional(&state.pool)
+ .await?
+ .ok_or_else(|| Error::UserNotFound)
+ .map(Json)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use axum::{
+ body::Body,
+ http::{Request, StatusCode},
+ };
+ use http_body_util::BodyExt;
+ use sqlx::PgPool;
+ use tower::ServiceExt;
+
+ use crate::init_router;
+
+ const JWT_SECRET: &str = "test-jwt-secret-token";
+ const JWT_MAX_AGE: time::Duration = time::Duration::HOUR;
+
+ #[sqlx::test]
+ async fn test_user_not_found(pool: PgPool) -> Result<(), Error> {
+ 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 = User {
+ uuid: uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"),
+ 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.unwrap();
+
+ assert_eq!(StatusCode::NOT_FOUND, response.status());
+
+ Ok(())
+ }
+
+ #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
+ async fn test_user_ok(pool: PgPool) -> Result<(), Error> {
+ 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 = User {
+ uuid: uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"),
+ 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.unwrap();
+
+ assert_eq!(StatusCode::OK, response.status());
+
+ let body_bytes = response.into_body().collect().await?.to_bytes();
+ let User {
+ 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(())
+ }
+}