Skip to content

Commit

Permalink
refactor: [torrust#1316] move health check API integration tests to pkg
Browse files Browse the repository at this point in the history
It moved the integration tests for the Health Check API from the main
lib to the `axum-health-check-api-server`.

Some tests have not been moved becuase they depend on other server
pakages. They basically use the test "environments" from other servers which
are not publicly exposed yet. They are only used in integration tests.

We will move those environments to the corresponding server so they can
be used in other packages to run the servers.
  • Loading branch information
josecelano committed Feb 25, 2025
1 parent c248caf commit a841e03
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 42 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/axum-health-check-api-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ serde_json = { version = "1", features = ["preserve_order"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
torrust-axum-server = { version = "3.0.0-develop", path = "../axum-server" }
torrust-server-lib = { version = "3.0.0-develop", path = "../server-lib" }
torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" }
tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] }
tracing = "0"

[dev-dependencies]
reqwest = { version = "0", features = ["json"] }
torrust-tracker-clock = { version = "3.0.0-develop", path = "../clock" }
torrust-tracker-test-helpers = { version = "3.0.0-develop", path = "../test-helpers" }
tracing-subscriber = { version = "0", features = ["json"] }
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ use std::sync::Arc;

use tokio::sync::oneshot::{self, Sender};
use tokio::task::JoinHandle;
use torrust_axum_health_check_api_server::{server, HEALTH_CHECK_API_LOG_TARGET};
use torrust_server_lib::registar::Registar;
use torrust_server_lib::signals::{self, Halted, Started};
use torrust_server_lib::signals::{self, Halted as SignalHalted, Started as SignalStarted};
use torrust_tracker_configuration::HealthCheckApi;

use crate::{server, HEALTH_CHECK_API_LOG_TARGET};

pub type Started = Environment<Running>;

#[derive(Debug)]
pub enum Error {
#[allow(dead_code)]
Expand All @@ -30,6 +33,7 @@ pub struct Environment<S> {
}

impl Environment<Stopped> {
#[must_use]
pub fn new(config: &Arc<HealthCheckApi>, registar: Registar) -> Self {
let bind_to = config.bind_address;

Expand All @@ -41,9 +45,13 @@ impl Environment<Stopped> {

/// Start the test environment for the Health Check API.
/// It runs the API server.
///
/// # Panics
///
/// Will panic if it cannot start the service in a spawned task.
pub async fn start(self) -> Environment<Running> {
let (tx_start, rx_start) = oneshot::channel::<Started>();
let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
let (tx_start, rx_start) = oneshot::channel::<SignalStarted>();
let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<SignalHalted>();

let register = self.registar.entries();

Expand Down Expand Up @@ -81,10 +89,17 @@ impl Environment<Running> {
Environment::<Stopped>::new(config, registar).start().await
}

/// # Errors
///
/// Will return an error if it cannot send the halt signal.
///
/// # Panics
///
/// Will panic if it cannot shutdown the service.
pub async fn stop(self) -> Result<Environment<Stopped>, Error> {
self.state
.halt_task
.send(Halted::Normal)
.send(SignalHalted::Normal)
.map_err(|e| Error::Error(e.to_string()))?;

let bind_to = self.state.task.await.expect("it should shutdown the service");
Expand Down
1 change: 1 addition & 0 deletions packages/axum-health-check-api-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod environment;
pub mod handlers;
pub mod resources;
pub mod responses;
Expand Down
19 changes: 19 additions & 0 deletions packages/axum-health-check-api-server/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! Integration tests.
//!
//! ```text
//! cargo test --test integration
//! ```
mod server;

use torrust_tracker_clock::clock;

/// This code needs to be copied into each crate.
/// Working version, for production.
#[cfg(not(test))]
#[allow(dead_code)]
pub(crate) type CurrentClock = clock::Working;

/// Stopped version, for testing.
#[cfg(test)]
#[allow(dead_code)]
pub(crate) type CurrentClock = clock::Stopped;
5 changes: 5 additions & 0 deletions packages/axum-health-check-api-server/tests/server/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use reqwest::Response;

pub async fn get(path: &str) -> Response {
reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap()
}
29 changes: 29 additions & 0 deletions packages/axum-health-check-api-server/tests/server/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use torrust_axum_health_check_api_server::environment::Started;
use torrust_axum_health_check_api_server::resources::{Report, Status};
use torrust_server_lib::registar::Registar;
use torrust_tracker_test_helpers::{configuration, logging};

use crate::server::client::get;

#[tokio::test]
async fn health_check_endpoint_should_return_status_ok_when_there_is_no_services_registered() {
logging::setup();

let configuration = configuration::ephemeral_with_no_services();

let env = Started::new(&configuration.health_check_api.into(), Registar::default()).await;

let response = get(&format!("http://{}/health_check", env.state.binding)).await; // DevSkim: ignore DS137138

assert_eq!(response.status(), 200);
assert_eq!(response.headers().get("content-type").unwrap(), "application/json");

let report = response
.json::<Report>()
.await
.expect("it should be able to get the report as json");

assert_eq!(report.status, Status::None);

env.stop().await.expect("it should stop the service");
}
2 changes: 2 additions & 0 deletions packages/axum-health-check-api-server/tests/server/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod client;
pub mod contract;
2 changes: 2 additions & 0 deletions packages/test-helpers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ version.workspace = true
[dependencies]
rand = "0"
torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" }
tracing = "0"
tracing-subscriber = { version = "0", features = ["json"] }
1 change: 1 addition & 0 deletions packages/test-helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
//!
//! A collection of functions and types to help with testing the tracker server.
pub mod configuration;
pub mod logging;
pub mod random;
156 changes: 156 additions & 0 deletions packages/test-helpers/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! Setup for logging in tests.
use std::collections::VecDeque;
use std::io;
use std::sync::{Mutex, MutexGuard, Once, OnceLock};

use torrust_tracker_configuration::logging::TraceStyle;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::fmt::MakeWriter;

static INIT: Once = Once::new();

/// A global buffer containing the latest lines captured from logs.
#[doc(hidden)]
pub fn captured_logs_buffer() -> &'static Mutex<CircularBuffer> {
static CAPTURED_LOGS_GLOBAL_BUFFER: OnceLock<Mutex<CircularBuffer>> = OnceLock::new();
CAPTURED_LOGS_GLOBAL_BUFFER.get_or_init(|| Mutex::new(CircularBuffer::new(10000, 200)))
}

pub fn setup() {
INIT.call_once(|| {
tracing_init(LevelFilter::ERROR, &TraceStyle::Default);
});
}

fn tracing_init(level_filter: LevelFilter, style: &TraceStyle) {
let mock_writer = LogCapturer::new(captured_logs_buffer());

let builder = tracing_subscriber::fmt()
.with_max_level(level_filter)
.with_ansi(true)
.with_test_writer()
.with_writer(mock_writer);

let () = match style {
TraceStyle::Default => builder.init(),
TraceStyle::Pretty(display_filename) => builder.pretty().with_file(*display_filename).init(),
TraceStyle::Compact => builder.compact().init(),
TraceStyle::Json => builder.json().init(),
};

tracing::info!("Logging initialized");
}

/// It returns true is there is a log line containing all the texts passed.
///
/// # Panics
///
/// Will panic if it can't get the lock for the global buffer or convert it into
/// a vec.
#[must_use]
#[allow(dead_code)]
pub fn logs_contains_a_line_with(texts: &[&str]) -> bool {
// code-review: we can search directly in the buffer instead of converting
// the buffer into a string but that would slow down the tests because
// cloning should be faster that locking the buffer for searching.
// Because the buffer is not big.
let logs = String::from_utf8(captured_logs_buffer().lock().unwrap().as_vec()).unwrap();

for line in logs.split('\n') {
if contains(line, texts) {
return true;
}
}

false
}

#[allow(dead_code)]
fn contains(text: &str, texts: &[&str]) -> bool {
texts.iter().all(|&word| text.contains(word))
}

/// A tracing writer which captures the latests logs lines into a buffer.
/// It's used to capture the logs in the tests.
#[derive(Debug)]
pub struct LogCapturer<'a> {
logs: &'a Mutex<CircularBuffer>,
}

impl<'a> LogCapturer<'a> {
pub fn new(buf: &'a Mutex<CircularBuffer>) -> Self {
Self { logs: buf }
}

fn buf(&self) -> io::Result<MutexGuard<'a, CircularBuffer>> {
self.logs.lock().map_err(|_| io::Error::from(io::ErrorKind::Other))
}
}

impl io::Write for LogCapturer<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
print!("{}", String::from_utf8(buf.to_vec()).unwrap());

let mut target = self.buf()?;

target.write(buf)
}

fn flush(&mut self) -> io::Result<()> {
self.buf()?.flush()
}
}

impl MakeWriter<'_> for LogCapturer<'_> {
type Writer = Self;

fn make_writer(&self) -> Self::Writer {
LogCapturer::new(self.logs)
}
}

#[derive(Debug)]
pub struct CircularBuffer {
max_size: usize,
buffer: VecDeque<u8>,
}

impl CircularBuffer {
#[must_use]
pub fn new(max_lines: usize, average_line_size: usize) -> Self {
Self {
max_size: max_lines * average_line_size,
buffer: VecDeque::with_capacity(max_lines * average_line_size),
}
}

/// # Errors
///
/// Won't return any error.
#[allow(clippy::unnecessary_wraps)]
pub fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for &byte in buf {
if self.buffer.len() == self.max_size {
// Remove oldest byte to make space
self.buffer.pop_front();
}
self.buffer.push_back(byte);
}

Ok(buf.len())
}

/// # Errors
///
/// Won't return any error.
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::unused_self)]
pub fn flush(&mut self) -> io::Result<()> {
Ok(())
}

#[must_use]
pub fn as_vec(&self) -> Vec<u8> {
self.buffer.iter().copied().collect()
}
}
Loading

0 comments on commit a841e03

Please sign in to comment.