From 57704def31e6c91325ce6fbfeaaca41029a6eb69 Mon Sep 17 00:00:00 2001 From: Sandro Eiler Date: Sat, 25 Nov 2023 08:48:09 +0100 Subject: [PATCH] feat: add route and tests --- Cargo.lock | 16 +++++++++++ Cargo.toml | 8 +++--- src/error.rs | 67 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 16 +++++++++-- tests/health_check.rs | 62 ++++++++++++++++++++++++++++++++++----- 5 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 4c2ebd8..a64b205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,6 +440,8 @@ dependencies = [ "axum", "hyper", "reqwest", + "serde", + "serde_json", "tokio", ] @@ -802,6 +804,20 @@ name = "serde" version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/Cargo.toml b/Cargo.toml index 240d499..5303cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,11 @@ name = "learn_axum" [dependencies] tokio = { version = "1.32.0", features = ["full"] } hyper = { version = "0.14.27", features = ["full"] } -# # Serde / json -# serde = { version = "1.0", features = ["derive"] } -# serde_json = "1" +# Serde / json +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" # serde_with = "3" -# # Axum +# Axum axum = { version = "0.6.20" } # tower-http = { version = "0.4.4", features = ["fs"] } # tower-cookies = "0.9" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e414b15 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,67 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde::Serialize; + +pub type Result = core::result::Result; + +#[derive(Clone, Debug, Serialize, strum_macros::AsRefStr)] +#[serde(tag = "type", content = "data")] +pub enum Error { + LoginFail, + + // -- Auth errors. + AuthFailNoAuthTokenCookie, + AuthFailTokenWrongFormat, + AuthFailCtxNotInRequestExt, + + // -- Model errors. + PropertyDeleteFailIdNotFound { id: u64 }, +} + +impl Error { + pub fn client_status_and_error(&self) -> (StatusCode, ClientError) { + match self { + // -- Login. + Self::LoginFail => (StatusCode::UNAUTHORIZED, ClientError::LOGIN_FAIL), + + // -- Auth. + Self::AuthFailNoAuthTokenCookie + | Self::AuthFailTokenWrongFormat + | Self::AuthFailCtxNotInRequestExt => (StatusCode::FORBIDDEN, ClientError::NO_AUTH), + + // -- Model. + Self::PropertyDeleteFailIdNotFound { .. } => { + (StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS) + } + + // -- Fallback. + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + ClientError::SERVICE_ERROR, + ), + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + println!("->> {:<12} - {self:?}", "INTO_RESPONSE"); + + // Create a placeholder Axum response. + let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response(); + + // Insert the Error into the response. + response.extensions_mut().insert(self); + + response + } +} + +#[derive(Debug, strum_macros::AsRefStr)] +#[allow(non_camel_case_types)] +pub enum ClientError { + LOGIN_FAIL, + NO_AUTH, + INVALID_PARAMS, + SERVICE_ERROR, +} diff --git a/src/lib.rs b/src/lib.rs index 37bcb68..4baa722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,18 +2,28 @@ use axum::extract::{Path, Query}; use axum::http::{Method, Uri}; use axum::response::{Html, IntoResponse, Response}; -use axum::routing::{get, get_service, IntoMakeService}; +use axum::routing::{get, get_service, post, IntoMakeService}; use axum::Server; use axum::{middleware, Json, Router}; use hyper::server::conn::AddrIncoming; +use serde::Deserialize; +use serde_json::{json, Value}; use std::net::SocketAddr; use std::net::TcpListener; pub type App = Server>; -/// API routes +#[derive(Deserialize)] +struct FormData { + email: String, + name: String, +} + +/// API routing fn app() -> Router { - Router::new().route("/health_check", get(|| async {})) + Router::new() + .route("/health_check", get(|| async {})) + .route("/subscriptions", post(|| async {})) } /// Start the server diff --git a/tests/health_check.rs b/tests/health_check.rs index d9de26b..3834d5c 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -6,29 +6,77 @@ struct TestApp { #[tokio::test] async fn health_check_works() { + // Arrange let TestApp { addr, .. } = spawn_app().await; + // Act let client = reqwest::Client::new(); let response = client .get(format!("http://{addr}/health_check")) .send() .await .expect("Failed to execute request."); + + // Assert assert!(response.status().is_success()); assert_eq!(Some(0), response.content_length()); } -// fn spawn_app() { -// let server = learn_axum::run().expect("Failed to bind address."); -// tokio::spawn(server); -// } +#[tokio::test] +async fn subscribe_returns_a_200_for_valid_form_data() { + // Arrange + let TestApp { addr, .. } = spawn_app().await; + let client = reqwest::Client::new(); + + // Act + let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; + let response = client + .post(&format!("http://{addr}/subscriptions")) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body) + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert_eq!(200, response.status().as_u16()); +} + +#[tokio::test] +async fn subscribe_returns_a_400_when_data_is_missing() { + // Arrange + let TestApp { addr, .. } = spawn_app().await; + let client = reqwest::Client::new(); + let test_cases = vec![ + ("name=le%20guin", "missing the email"), + ("email=ursula_le_guin%40gmail.com", "missing the name"), + ("", "missing both name and email"), + ]; + + for (invalid_body, error_message) in test_cases { + // Act + let response = client + .post(&format!("http://{addr}/subscriptions")) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(invalid_body) + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert_eq!( + 400, + response.status().as_u16(), + "The API did not fail with 400 Bad Request when the payload was {}.", + error_message + ); + } +} + async fn spawn_app() -> TestApp { let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port"); let addr = listener.local_addr().unwrap(); - let server = learn_axum::run(listener).expect("Failed to bind to address"); - tokio::spawn(server); - TestApp { addr } }