From f17c98703fad0086ac091c9c6ef7ef136e9bcad4 Mon Sep 17 00:00:00 2001 From: Dorian Pula Date: Thu, 8 Feb 2024 14:55:09 -0500 Subject: [PATCH 1/3] Add logging of JSON requests. --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 950bec6..53b7105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,9 @@ async fn echo_request( warn!("Received a non-JSON body."); None }, - Some(Json(value)) => Some(value), + Some(Json(value)) => { + info!("JSON request: {}", value.to_string()); + Some(value)}, }; let response = EchoResponse { method, From bba5134531efd6b15c677073b0958b59691d69d0 Mon Sep 17 00:00:00 2001 From: Dorian Pula Date: Thu, 8 Feb 2024 14:57:57 -0500 Subject: [PATCH 2/3] Update the readme with changes. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 813c531..ce3157a 100644 --- a/README.md +++ b/README.md @@ -29,5 +29,5 @@ sudo dpkg -i "mirror-server-${VERSION}_amd64.deb" * [x] Migrate actix to axum for easier maintainability. * [x] Add mirroring of JSON request. * [x] Add logging to server. -* [ ] Add convenience path to access logging for a certain date / records. +* [x] ~~Add convenience path to access logging for a certain date / records.~~ * [ ] Dockerize mirror-server for easier distribution. From 2ccf33e9728da15d5690c86182039ddda4e3f75f Mon Sep 17 00:00:00 2001 From: Dorian Pula Date: Tue, 13 Feb 2024 16:25:03 -0500 Subject: [PATCH 3/3] Add tests for the mirror-server handling of requests. --- Cargo.toml | 4 ++ src/main.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 147 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index afabaa1..b41e7eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,7 @@ tower = "0.4" tower-http = { version = "0.5", features = ["trace"] } tracing = "0.1" tracing-subscriber = "0.3" + +[dev-dependencies] +axum-test = "14.0" +rstest = "0.18" diff --git a/src/main.rs b/src/main.rs index 53b7105..fde7472 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ use axum::{ - extract::{Host, OriginalUri, Json}, + extract::{Host, Json, OriginalUri}, http::{header::HeaderMap, Method}, Router, }; use clap::Parser; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; use tower_http::trace; -use tracing::{Level, info, warn}; +use tracing::{info, warn, Level}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -22,7 +22,7 @@ struct CliArgs { ips: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] struct EchoResponse { method: String, path: String, @@ -37,7 +37,7 @@ async fn echo_request( original_uri: OriginalUri, host: Host, header_map: HeaderMap, - body: Option> + body: Option>, ) -> Json { let method = method.to_string(); let host = host.0; @@ -47,25 +47,34 @@ async fn echo_request( .map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string())) .collect(); - let json_body = match body { + let body = match body { None => { warn!("Received a non-JSON body."); None - }, + } Some(Json(value)) => { info!("JSON request: {}", value.to_string()); - Some(value)}, + Some(value) + } }; let response = EchoResponse { method, host, path, headers, - body: json_body, + body, }; Json(response) } +fn app() -> Router { + Router::new().fallback(echo_request).layer( + trace::TraceLayer::new_for_http() + .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) + .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), + ) +} + #[tokio::main] async fn main() { let cli_args = CliArgs::parse(); @@ -73,20 +82,136 @@ async fn main() { let listen_on = format!("{ip}:{port}", ip = listen_on.0, port = listen_on.1); // From https://stackoverflow.com/questions/75009289/how-to-enable-logging-tracing-with-axum - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .init(); - let app = Router::new().fallback(echo_request).layer( - trace::TraceLayer::new_for_http() - .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) - .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), - ); + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); info!("Starting the mirror-server to listen to {}", listen_on); let listener = tokio::net::TcpListener::bind(&listen_on) .await .unwrap_or_else(|_| panic!("Failed to binding to {}", listen_on)); - axum::serve(listener, app) + axum::serve(listener, app()) .await .expect("Server should start"); } + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::{HeaderName, HeaderValue, Method, StatusCode}; + use axum_test::TestServer; + use rstest::rstest; + + #[tokio::test] + async fn handles_simple_get_request() { + let expected_headers: BTreeMap = BTreeMap::new(); + let expected_response = EchoResponse { + method: "GET".to_string(), + path: "/".to_string(), + host: "localhost".to_string(), + body: None, + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server.get("/").await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } + + #[rstest] + #[case::single_level("/test")] + #[case::multiple_level("/test/multiple/levels/")] + #[case::slash_ending("/test/")] + #[tokio::test] + async fn handles_different_urls(#[case] url: String) { + let expected_headers: BTreeMap = BTreeMap::new(); + let expected_response = EchoResponse { + method: "GET".to_string(), + path: url.clone(), + host: "localhost".to_string(), + body: None, + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server.get(&url).await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } + + #[rstest] + #[case::post(Method::POST)] + #[case::put(Method::PUT)] + #[case::patch(Method::PATCH)] + #[case::delete(Method::DELETE)] + #[tokio::test] + async fn handles_different_http_methods(#[case] http_method: Method) { + let expected_headers: BTreeMap = BTreeMap::new(); + let expected_response = EchoResponse { + method: http_method.to_string(), + path: "/testing".to_string(), + host: "localhost".to_string(), + body: None, + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server.method(http_method, "/testing").await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } + + #[tokio::test] + async fn handle_non_json_request() { + let mut expected_headers: BTreeMap = BTreeMap::new(); + expected_headers.insert("content-type".to_string(), "text/plain".to_string()); + let expected_response = EchoResponse { + method: "POST".to_string(), + path: "/".to_string(), + host: "localhost".to_string(), + body: None, + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server.post("/").text("hello world").await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } + + #[tokio::test] + async fn handle_json_request() { + let test_json = serde_json::json!({"hello": "world"}); + let mut expected_headers: BTreeMap = BTreeMap::new(); + expected_headers.insert("content-type".to_string(), "application/json".to_string()); + let expected_response = EchoResponse { + method: "POST".to_string(), + path: "/".to_string(), + host: "localhost".to_string(), + body: Some(test_json.clone()), + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server.post("/").json(&test_json).await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } + + #[tokio::test] + async fn handle_extra_headers() { + let mut expected_headers: BTreeMap = BTreeMap::new(); + expected_headers.insert("x-test-message".to_string(), "Howdy!".to_string()); + let expected_response = EchoResponse { + method: "GET".to_string(), + path: "/".to_string(), + host: "localhost".to_string(), + body: None, + headers: expected_headers, + }; + let server = TestServer::new(app()).unwrap(); + let response = server + .get("/") + .add_header( + HeaderName::from_static("x-test-message"), + HeaderValue::from_static("Howdy!"), + ) + .await; + response.assert_status(StatusCode::OK); + response.assert_json::(&expected_response); + } +}