diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e3c9650
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+target/
+Cargo.lock
+**/*.rs.bk
+.idea
diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..c398f79
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,59 @@
+---
+kind: pipeline
+type: docker
+name: mirror-server
+
+steps:
+ - name: create-build-image
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker-username
+ password:
+ from_secret: docker-password
+ registry: code.birch-tree.net
+ repo: code.birch-tree.net/dorian/mirror-server
+ target: BUILD
+ tags:
+ - build
+ cache_from:
+ - code.birch-tree.net/dorian/mirror-server:build
+
+ - name: test
+ image: code.birch-tree.net/dorian/mirror-server:build
+ commands:
+ - cargo test
+ depends_on:
+ - create-build-image
+
+ - name: create-release-image
+ image: plugins/docker
+ settings:
+ username:
+ from_secret: docker-username
+ password:
+ from_secret: docker-password
+ registry: code.birch-tree.net
+ repo: code.birch-tree.net/dorian/mirror-server
+ tags:
+ - 0.3.0
+ - latest
+ cache_from:
+ - code.birch-tree.net/dorian/mirror-server:latest
+ - code.birch-tree.net/dorian/mirror-server:build
+ depends_on:
+ - test
+
+ - name: create-debian-package
+ image: code.birch-tree.net/dorian/mirror-server:build
+ commands:
+ - ./publish-deb.sh
+ environment:
+ USERNAME: dorian
+ PASSWORD:
+ from_secret: gitea-release-password
+ depends_on:
+ - test
+
+image_pull_secrets:
+ - docker-config
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/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..097a0da
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+FROM rust:1.76 AS BUILD
+
+ENV APP_NAME=mirror-server
+ENV APP_HOME=/srv/${APP_NAME}
+RUN apt update \
+ && apt install -y curl \
+ && cargo install cargo-deb
+
+# Setup working env.
+RUN mkdir -p ${APP_HOME}
+WORKDIR ${APP_HOME}
+
+# Create build target cache.
+RUN USER=root cargo init --bin .
+COPY ["Cargo.toml", "./"]
+RUN cargo build \
+ && cargo build --tests \
+ && cargo build --release \
+ && rm src/*.rs
+
+# Bring in the source
+COPY ["src", "./src/"]
+
+# Build the example API.
+RUN cargo build --release
+
+# Create the minimal server image.
+FROM debian:buster-slim AS SERVER
+
+ENV APP_NAME=mirror-server
+ENV APP_HOME=/srv/${APP_NAME}
+RUN apt update
+
+RUN mkdir -p ${APP_HOME}
+WORKDIR ${APP_HOME}
+
+COPY --from=BUILD ${APP_HOME}/target/release/${APP_NAME} /usr/local/bin/
+CMD ${APP_NAME}
\ No newline at end of file
diff --git a/README.md b/README.md
index 813c531..c165ec7 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
A simple server for mirroring HTTP requests for testing.
+[![Build Status](https://ci.birch-tree.net/api/badges/dorian/mirror-server/status.svg)](https://ci.birch-tree.net/dorian/mirror-server)
+
## Getting Started
* Use the latest stable version of Rust using rustup.
@@ -29,5 +31,7 @@ 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.
-* [ ] Dockerize mirror-server for easier distribution.
+* [x] Create Docker image for mirror-server for easier distribution.
+* [x] Add publishing of DEB and Docker image to DroneCI.
+* [ ] Add documentation to the API.
+* [ ] Publish crate on .
diff --git a/publish-deb.sh b/publish-deb.sh
index aee0fff..b5b23aa 100755
--- a/publish-deb.sh
+++ b/publish-deb.sh
@@ -1,22 +1,28 @@
-#!/usr/bin/env bash
+#!/usr/bin/bash
-read -r -p "Username: " USERNAME
-read -r -s -p "Password: " PASSWORD
-
-_pkg_name=mirror-server
-_pkg_version=0.2.0
-_deb_file="${_pkg_name}_${_pkg_version}_amd64.deb"
-_deb_path="target/debian"
-_gitea_server="code.birch-tree.net"
-
-if [[ ! -f "${_deb_path}/${_deb_file}" ]];
-then
- echo "Run cargo deb first!"
- exit 1
+if [[ -z "${USERNAME}" ]]; then
+ echo "Set the USERNAME for Gitea"
+ exit 1
fi
-curl --user "${USERNAME}:${PASSWORD}" \
- --upload-file "${_deb_path}/${_deb_file}" \
- -X PUT \
- "https://${_gitea_server}/api/packages/${USERNAME}/generic/${_pkg_name}/${_pkg_version}/${_deb_file}"
+if [[ -z "${PASSWORD}" ]]; then
+ echo "Set the PASSWORD for Gitea"
+ exit 1
+fi
+echo 'Create the Debian package...'
+
+pkg_full_path=$(cargo deb)
+pkg_filename=$(basename "${pkg_full_path}")
+pkg_name=$(echo "${pkg_filename}" | awk -F _ '{print $1}')
+pkg_version=$(echo "${pkg_filename}" | awk -F _ '{print $2}')
+
+gitea_server="code.birch-tree.net"
+
+curl --user "${USERNAME}:${PASSWORD}" \
+ --upload-file "${pkg_full_path}" \
+ --silent \
+ -X PUT \
+ "https://${gitea_server}/api/packages/${USERNAME}/generic/${pkg_name}/${pkg_version}/${pkg_filename}"
+
+echo "Published ${pkg_name} v${pkg_version} ===> ${pkg_filename}"
diff --git a/src/main.rs b/src/main.rs
index 950bec6..95c8ff1 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,
@@ -35,35 +35,46 @@ struct EchoResponse {
async fn echo_request(
method: Method,
original_uri: OriginalUri,
- host: Host,
+ Host(host): Host,
header_map: HeaderMap,
- body: Option>
+ body: Option>,
) -> Json {
let method = method.to_string();
- let host = host.0;
+ let host = host;
let path = original_uri.path().to_string();
let headers = header_map
.iter()
.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)) => Some(value),
+ }
+ Some(Json(value)) => {
+ info!("JSON request: {}", value.to_string());
+ 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();
@@ -71,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);
+ }
+}