From 3b492570a24e87e45e22c0f0240a9ba92cb803b2 Mon Sep 17 00:00:00 2001
From: Cameron Garnham <me@da2ce7.com>
Date: Wed, 3 Jan 2024 15:58:40 +1100
Subject: [PATCH 1/3] dev: extract config from core::tracker

---
 packages/configuration/src/lib.rs        | 18 ++----
 src/bootstrap/app.rs                     | 12 ++--
 src/bootstrap/jobs/torrent_cleanup.rs    |  2 +-
 src/bootstrap/jobs/tracker_apis.rs       | 15 +++--
 src/core/mod.rs                          | 70 +++++++++++++++++-------
 src/core/services/mod.rs                 |  4 +-
 src/core/services/statistics/mod.rs      |  6 +-
 src/core/services/torrent.rs             | 22 ++++----
 src/main.rs                              |  2 +-
 src/servers/apis/routes.rs               | 11 ++--
 src/servers/apis/server.rs               | 22 ++++++--
 src/servers/apis/v1/middlewares/auth.rs  | 19 ++++---
 src/servers/http/v1/handlers/announce.rs | 10 ++--
 src/servers/http/v1/handlers/scrape.rs   | 10 ++--
 src/servers/http/v1/services/announce.rs | 27 +++------
 src/servers/http/v1/services/scrape.rs   | 42 +++-----------
 src/servers/udp/handlers.rs              | 46 ++++++++--------
 tests/servers/api/test_environment.rs    | 26 ++++-----
 tests/servers/http/v1/contract.rs        | 22 +++++---
 19 files changed, 202 insertions(+), 184 deletions(-)

diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs
index a8f605289..4b81aed8b 100644
--- a/packages/configuration/src/lib.rs
+++ b/packages/configuration/src/lib.rs
@@ -229,7 +229,7 @@
 //! [health_check_api]
 //! bind_address = "127.0.0.1:1313"
 //!```
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 use std::net::IpAddr;
 use std::str::FromStr;
 use std::sync::Arc;
@@ -337,6 +337,8 @@ pub struct HttpTracker {
     pub ssl_key_path: Option<String>,
 }
 
+pub type AccessTokens = HashMap<String, String>;
+
 /// Configuration for the HTTP API.
 #[serde_as]
 #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
@@ -360,21 +362,13 @@ pub struct HttpApi {
     /// token and the value is the token itself. The token is used to
     /// authenticate the user. All tokens are valid for all endpoints and have
     /// the all permissions.
-    pub access_tokens: HashMap<String, String>,
+    pub access_tokens: AccessTokens,
 }
 
 impl HttpApi {
     fn override_admin_token(&mut self, api_admin_token: &str) {
         self.access_tokens.insert("admin".to_string(), api_admin_token.to_string());
     }
-
-    /// Checks if the given token is one of the token in the configuration.
-    #[must_use]
-    pub fn contains_token(&self, token: &str) -> bool {
-        let tokens: HashMap<String, String> = self.access_tokens.clone();
-        let tokens: HashSet<String> = tokens.into_values().collect();
-        tokens.contains(token)
-    }
 }
 
 /// Configuration for the Health Check API.
@@ -804,7 +798,7 @@ mod tests {
     fn http_api_configuration_should_check_if_it_contains_a_token() {
         let configuration = Configuration::default();
 
-        assert!(configuration.http_api.contains_token("MyAccessToken"));
-        assert!(!configuration.http_api.contains_token("NonExistingToken"));
+        assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken"));
+        assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken"));
     }
 }
diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs
index 4a6f79a96..09b624566 100644
--- a/src/bootstrap/app.rs
+++ b/src/bootstrap/app.rs
@@ -24,8 +24,8 @@ use crate::shared::crypto::ephemeral_instance_keys;
 
 /// It loads the configuration from the environment and builds the main domain [`Tracker`] struct.
 #[must_use]
-pub fn setup() -> (Arc<Configuration>, Arc<Tracker>) {
-    let configuration = Arc::new(initialize_configuration());
+pub fn setup() -> (Configuration, Arc<Tracker>) {
+    let configuration = initialize_configuration();
     let tracker = initialize_with_configuration(&configuration);
 
     (configuration, tracker)
@@ -35,7 +35,7 @@ pub fn setup() -> (Arc<Configuration>, Arc<Tracker>) {
 ///
 /// The configuration may be obtained from the environment (via config file or env vars).
 #[must_use]
-pub fn initialize_with_configuration(configuration: &Arc<Configuration>) -> Arc<Tracker> {
+pub fn initialize_with_configuration(configuration: &Configuration) -> Arc<Tracker> {
     initialize_static();
     initialize_logging(configuration);
     Arc::new(initialize_tracker(configuration))
@@ -60,13 +60,13 @@ pub fn initialize_static() {
 /// The tracker is the domain layer service. It's the entrypoint to make requests to the domain layer.
 /// It's used by other higher-level components like the UDP and HTTP trackers or the tracker API.
 #[must_use]
-pub fn initialize_tracker(config: &Arc<Configuration>) -> Tracker {
-    tracker_factory(config.clone())
+pub fn initialize_tracker(config: &Configuration) -> Tracker {
+    tracker_factory(config)
 }
 
 /// It initializes the log level, format and channel.
 ///
 /// See [the logging setup](crate::bootstrap::logging::setup) for more info about logging.
-pub fn initialize_logging(config: &Arc<Configuration>) {
+pub fn initialize_logging(config: &Configuration) {
     bootstrap::logging::setup(config);
 }
diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs
index d3b084d31..6647e0249 100644
--- a/src/bootstrap/jobs/torrent_cleanup.rs
+++ b/src/bootstrap/jobs/torrent_cleanup.rs
@@ -25,7 +25,7 @@ use crate::core;
 ///
 /// Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about that option.
 #[must_use]
-pub fn start_job(config: &Arc<Configuration>, tracker: &Arc<core::Tracker>) -> JoinHandle<()> {
+pub fn start_job(config: &Configuration, tracker: &Arc<core::Tracker>) -> JoinHandle<()> {
     let weak_tracker = std::sync::Arc::downgrade(tracker);
     let interval = config.inactive_peer_cleanup_interval;
 
diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs
index e50a83651..43cb5de8e 100644
--- a/src/bootstrap/jobs/tracker_apis.rs
+++ b/src/bootstrap/jobs/tracker_apis.rs
@@ -26,7 +26,7 @@ use std::sync::Arc;
 use axum_server::tls_rustls::RustlsConfig;
 use log::info;
 use tokio::task::JoinHandle;
-use torrust_tracker_configuration::HttpApi;
+use torrust_tracker_configuration::{AccessTokens, HttpApi};
 
 use super::make_rust_tls;
 use crate::core;
@@ -64,8 +64,10 @@ pub async fn start_job(config: &HttpApi, tracker: Arc<core::Tracker>, version: V
             .await
             .map(|tls| tls.expect("it should have a valid tracker api tls configuration"));
 
+        let access_tokens = Arc::new(config.access_tokens.clone());
+
         match version {
-            Version::V1 => Some(start_v1(bind_to, tls, tracker.clone()).await),
+            Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), access_tokens).await),
         }
     } else {
         info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration.");
@@ -73,9 +75,14 @@ pub async fn start_job(config: &HttpApi, tracker: Arc<core::Tracker>, version: V
     }
 }
 
-async fn start_v1(socket: SocketAddr, tls: Option<RustlsConfig>, tracker: Arc<core::Tracker>) -> JoinHandle<()> {
+async fn start_v1(
+    socket: SocketAddr,
+    tls: Option<RustlsConfig>,
+    tracker: Arc<core::Tracker>,
+    access_tokens: Arc<AccessTokens>,
+) -> JoinHandle<()> {
     let server = ApiServer::new(Launcher::new(socket, tls))
-        .start(tracker)
+        .start(tracker, access_tokens)
         .await
         .expect("it should be able to start to the tracker api");
 
diff --git a/src/core/mod.rs b/src/core/mod.rs
index fc44877c8..dac298462 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -447,6 +447,7 @@ use std::time::Duration;
 
 use derive_more::Constructor;
 use futures::future::join_all;
+use log::debug;
 use tokio::sync::mpsc::error::SendError;
 use torrust_tracker_configuration::{AnnouncePolicy, Configuration};
 use torrust_tracker_primitives::TrackerMode;
@@ -472,17 +473,19 @@ pub const TORRENT_PEERS_LIMIT: usize = 74;
 /// Typically, the `Tracker` is used by a higher application service that handles
 /// the network layer.
 pub struct Tracker {
-    /// `Tracker` configuration. See [`torrust-tracker-configuration`](torrust_tracker_configuration)
-    pub config: Arc<Configuration>,
+    announce_policy: AnnouncePolicy,
     /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite)
     /// or [`MySQL`](crate::core::databases::mysql)
     pub database: Arc<Box<dyn Database>>,
     mode: TrackerMode,
+    policy: TrackerPolicy,
     keys: tokio::sync::RwLock<std::collections::HashMap<Key, auth::ExpiringKey>>,
     whitelist: tokio::sync::RwLock<std::collections::HashSet<InfoHash>>,
     pub torrents: Arc<RepositoryAsyncSingle>,
     stats_event_sender: Option<Box<dyn statistics::EventSender>>,
     stats_repository: statistics::Repo,
+    external_ip: Option<IpAddr>,
+    on_reverse_proxy: bool,
 }
 
 /// Structure that holds general `Tracker` torrents metrics.
@@ -500,6 +503,12 @@ pub struct TorrentsMetrics {
     pub torrents: u64,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Default, Constructor)]
+pub struct TrackerPolicy {
+    pub remove_peerless_torrents: bool,
+    pub max_peer_timeout: u32,
+    pub persistent_torrent_completed_stat: bool,
+}
 /// Structure that holds the data returned by the `announce` request.
 #[derive(Clone, Debug, PartialEq, Constructor, Default)]
 pub struct AnnounceData {
@@ -556,7 +565,7 @@ impl Tracker {
     ///
     /// Will return a `databases::error::Error` if unable to connect to database. The `Tracker` is responsible for the persistence.
     pub fn new(
-        config: Arc<Configuration>,
+        config: &Configuration,
         stats_event_sender: Option<Box<dyn statistics::EventSender>>,
         stats_repository: statistics::Repo,
     ) -> Result<Tracker, databases::error::Error> {
@@ -565,7 +574,8 @@ impl Tracker {
         let mode = config.mode;
 
         Ok(Tracker {
-            config,
+            //config,
+            announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval),
             mode,
             keys: tokio::sync::RwLock::new(std::collections::HashMap::new()),
             whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()),
@@ -573,6 +583,13 @@ impl Tracker {
             stats_event_sender,
             stats_repository,
             database,
+            external_ip: config.get_ext_ip(),
+            policy: TrackerPolicy::new(
+                config.remove_peerless_torrents,
+                config.max_peer_timeout,
+                config.persistent_torrent_completed_stat,
+            ),
+            on_reverse_proxy: config.on_reverse_proxy,
         })
     }
 
@@ -596,6 +613,19 @@ impl Tracker {
         self.is_private()
     }
 
+    /// Returns `true` is the tracker is in whitelisted mode.
+    pub fn is_behind_reverse_proxy(&self) -> bool {
+        self.on_reverse_proxy
+    }
+
+    pub fn get_announce_policy(&self) -> AnnouncePolicy {
+        self.announce_policy
+    }
+
+    pub fn get_maybe_external_ip(&self) -> Option<IpAddr> {
+        self.external_ip
+    }
+
     /// It handles an announce request.
     ///
     /// # Context: Tracker
@@ -617,18 +647,19 @@ impl Tracker {
         // we are actually handling authentication at the handlers level. So I would extract that
         // responsibility into another authentication service.
 
-        peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.config.get_ext_ip()));
+        debug!("Before: {peer:?}");
+        peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.external_ip));
+        debug!("After: {peer:?}");
 
-        let swarm_stats = self.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
+        // we should update the torrent and get the stats before we get the peer list.
+        let stats = self.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
 
         let peers = self.get_torrent_peers_for_peer(info_hash, peer).await;
 
-        let policy = AnnouncePolicy::new(self.config.announce_interval, self.config.min_announce_interval);
-
         AnnounceData {
             peers,
-            stats: swarm_stats,
-            policy,
+            stats,
+            policy: self.get_announce_policy(),
         }
     }
 
@@ -727,7 +758,7 @@ impl Tracker {
 
         let (stats, stats_updated) = self.torrents.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
 
-        if self.config.persistent_torrent_completed_stat && stats_updated {
+        if self.policy.persistent_torrent_completed_stat && stats_updated {
             let completed = stats.downloaded;
             let info_hash = *info_hash;
 
@@ -788,17 +819,17 @@ impl Tracker {
         let mut torrents_lock = self.torrents.get_torrents_mut().await;
 
         // If we don't need to remove torrents we will use the faster iter
-        if self.config.remove_peerless_torrents {
+        if self.policy.remove_peerless_torrents {
             let mut cleaned_torrents_map: BTreeMap<InfoHash, torrent::Entry> = BTreeMap::new();
 
             for (info_hash, torrent_entry) in &mut *torrents_lock {
-                torrent_entry.remove_inactive_peers(self.config.max_peer_timeout);
+                torrent_entry.remove_inactive_peers(self.policy.max_peer_timeout);
 
                 if torrent_entry.peers.is_empty() {
                     continue;
                 }
 
-                if self.config.persistent_torrent_completed_stat && torrent_entry.completed == 0 {
+                if self.policy.persistent_torrent_completed_stat && torrent_entry.completed == 0 {
                     continue;
                 }
 
@@ -808,7 +839,7 @@ impl Tracker {
             *torrents_lock = cleaned_torrents_map;
         } else {
             for torrent_entry in (*torrents_lock).values_mut() {
-                torrent_entry.remove_inactive_peers(self.config.max_peer_timeout);
+                torrent_entry.remove_inactive_peers(self.policy.max_peer_timeout);
             }
         }
     }
@@ -1061,7 +1092,6 @@ mod tests {
 
         use std::net::{IpAddr, Ipv4Addr, SocketAddr};
         use std::str::FromStr;
-        use std::sync::Arc;
 
         use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes};
         use torrust_tracker_test_helpers::configuration;
@@ -1073,21 +1103,21 @@ mod tests {
         use crate::shared::clock::DurationSinceUnixEpoch;
 
         fn public_tracker() -> Tracker {
-            tracker_factory(configuration::ephemeral_mode_public().into())
+            tracker_factory(&configuration::ephemeral_mode_public())
         }
 
         fn private_tracker() -> Tracker {
-            tracker_factory(configuration::ephemeral_mode_private().into())
+            tracker_factory(&configuration::ephemeral_mode_private())
         }
 
         fn whitelisted_tracker() -> Tracker {
-            tracker_factory(configuration::ephemeral_mode_whitelisted().into())
+            tracker_factory(&configuration::ephemeral_mode_whitelisted())
         }
 
         pub fn tracker_persisting_torrents_in_database() -> Tracker {
             let mut configuration = configuration::ephemeral();
             configuration.persistent_torrent_completed_stat = true;
-            tracker_factory(Arc::new(configuration))
+            tracker_factory(&configuration)
         }
 
         fn sample_info_hash() -> InfoHash {
diff --git a/src/core/services/mod.rs b/src/core/services/mod.rs
index f5868fc26..76c6a36f6 100644
--- a/src/core/services/mod.rs
+++ b/src/core/services/mod.rs
@@ -19,12 +19,12 @@ use crate::core::Tracker;
 ///
 /// Will panic if tracker cannot be instantiated.
 #[must_use]
-pub fn tracker_factory(config: Arc<Configuration>) -> Tracker {
+pub fn tracker_factory(config: &Configuration) -> Tracker {
     // Initialize statistics
     let (stats_event_sender, stats_repository) = statistics::setup::factory(config.tracker_usage_statistics);
 
     // Initialize Torrust tracker
-    match Tracker::new(config, stats_event_sender, stats_repository) {
+    match Tracker::new(&Arc::new(config), stats_event_sender, stats_repository) {
         Ok(tracker) => tracker,
         Err(error) => {
             panic!("{}", error)
diff --git a/src/core/services/statistics/mod.rs b/src/core/services/statistics/mod.rs
index f74df62e5..3578c53aa 100644
--- a/src/core/services/statistics/mod.rs
+++ b/src/core/services/statistics/mod.rs
@@ -92,13 +92,13 @@ mod tests {
     use crate::core::services::statistics::{get_metrics, TrackerMetrics};
     use crate::core::services::tracker_factory;
 
-    pub fn tracker_configuration() -> Arc<Configuration> {
-        Arc::new(configuration::ephemeral())
+    pub fn tracker_configuration() -> Configuration {
+        configuration::ephemeral()
     }
 
     #[tokio::test]
     async fn the_statistics_service_should_return_the_tracker_metrics() {
-        let tracker = Arc::new(tracker_factory(tracker_configuration()));
+        let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
         let tracker_metrics = get_metrics(tracker.clone()).await;
 
diff --git a/src/core/services/torrent.rs b/src/core/services/torrent.rs
index f88cf5b50..d1ab29a7f 100644
--- a/src/core/services/torrent.rs
+++ b/src/core/services/torrent.rs
@@ -168,13 +168,13 @@ mod tests {
         use crate::core::services::tracker_factory;
         use crate::shared::bit_torrent::info_hash::InfoHash;
 
-        pub fn tracker_configuration() -> Arc<Configuration> {
-            Arc::new(configuration::ephemeral())
+        pub fn tracker_configuration() -> Configuration {
+            configuration::ephemeral()
         }
 
         #[tokio::test]
         async fn should_return_none_if_the_tracker_does_not_have_the_torrent() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let torrent_info = get_torrent_info(
                 tracker.clone(),
@@ -187,7 +187,7 @@ mod tests {
 
         #[tokio::test]
         async fn should_return_the_torrent_info_if_the_tracker_has_the_torrent() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
             let info_hash = InfoHash::from_str(&hash).unwrap();
@@ -223,13 +223,13 @@ mod tests {
         use crate::core::services::tracker_factory;
         use crate::shared::bit_torrent::info_hash::InfoHash;
 
-        pub fn tracker_configuration() -> Arc<Configuration> {
-            Arc::new(configuration::ephemeral())
+        pub fn tracker_configuration() -> Configuration {
+            configuration::ephemeral()
         }
 
         #[tokio::test]
         async fn should_return_an_empty_result_if_the_tracker_does_not_have_any_torrent() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let torrents = get_torrents(tracker.clone(), &Pagination::default()).await;
 
@@ -238,7 +238,7 @@ mod tests {
 
         #[tokio::test]
         async fn should_return_a_summarized_info_for_all_torrents() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
             let info_hash = InfoHash::from_str(&hash).unwrap();
@@ -262,7 +262,7 @@ mod tests {
 
         #[tokio::test]
         async fn should_allow_limiting_the_number_of_torrents_in_the_result() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
             let info_hash1 = InfoHash::from_str(&hash1).unwrap();
@@ -286,7 +286,7 @@ mod tests {
 
         #[tokio::test]
         async fn should_allow_using_pagination_in_the_result() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
             let info_hash1 = InfoHash::from_str(&hash1).unwrap();
@@ -319,7 +319,7 @@ mod tests {
 
         #[tokio::test]
         async fn should_return_torrents_ordered_by_info_hash() {
-            let tracker = Arc::new(tracker_factory(tracker_configuration()));
+            let tracker = Arc::new(tracker_factory(&tracker_configuration()));
 
             let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
             let info_hash1 = InfoHash::from_str(&hash1).unwrap();
diff --git a/src/main.rs b/src/main.rs
index 87c0fc367..5c65f8e07 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ use torrust_tracker::{app, bootstrap};
 async fn main() {
     let (config, tracker) = bootstrap::app::setup();
 
-    let jobs = app::start(config.clone(), tracker.clone()).await;
+    let jobs = app::start(config.into(), tracker.clone()).await;
 
     // handle the signals
     tokio::select! {
diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs
index fef412f91..227916335 100644
--- a/src/servers/apis/routes.rs
+++ b/src/servers/apis/routes.rs
@@ -9,26 +9,27 @@ use std::sync::Arc;
 
 use axum::routing::get;
 use axum::{middleware, Router};
+use torrust_tracker_configuration::AccessTokens;
 use tower_http::compression::CompressionLayer;
 
 use super::v1;
 use super::v1::context::health_check::handlers::health_check_handler;
+use super::v1::middlewares::auth::State;
 use crate::core::Tracker;
 
 /// Add all API routes to the router.
 #[allow(clippy::needless_pass_by_value)]
-pub fn router(tracker: Arc<Tracker>) -> Router {
+pub fn router(tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Router {
     let router = Router::new();
 
     let api_url_prefix = "/api";
 
     let router = v1::routes::add(api_url_prefix, router, tracker.clone());
 
+    let state = State { access_tokens };
+
     router
-        .layer(middleware::from_fn_with_state(
-            tracker.config.clone(),
-            v1::middlewares::auth::auth,
-        ))
+        .layer(middleware::from_fn_with_state(state, v1::middlewares::auth::auth))
         .route(&format!("{api_url_prefix}/health_check"), get(health_check_handler))
         .layer(CompressionLayer::new())
 }
diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs
index f4fdf8994..d26362f66 100644
--- a/src/servers/apis/server.rs
+++ b/src/servers/apis/server.rs
@@ -32,6 +32,7 @@ use derive_more::Constructor;
 use futures::future::BoxFuture;
 use log::{error, info};
 use tokio::sync::oneshot::{Receiver, Sender};
+use torrust_tracker_configuration::AccessTokens;
 
 use super::routes::router;
 use crate::bootstrap::jobs::Started;
@@ -91,14 +92,14 @@ impl ApiServer<Stopped> {
     /// # Panics
     ///
     /// It would panic if the bound socket address cannot be sent back to this starter.
-    pub async fn start(self, tracker: Arc<Tracker>) -> Result<ApiServer<Running>, Error> {
+    pub async fn start(self, tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Result<ApiServer<Running>, Error> {
         let (tx_start, rx_start) = tokio::sync::oneshot::channel::<Started>();
         let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
 
         let launcher = self.state.launcher;
 
         let task = tokio::spawn(async move {
-            launcher.start(tracker, tx_start, rx_halt).await;
+            launcher.start(tracker, access_tokens, tx_start, rx_halt).await;
             launcher
         });
 
@@ -159,8 +160,14 @@ impl Launcher {
     ///
     /// Will panic if unable to bind to the socket, or unable to get the address of the bound socket.
     /// Will also panic if unable to send message regarding the bound socket address.
-    pub fn start(&self, tracker: Arc<Tracker>, tx_start: Sender<Started>, rx_halt: Receiver<Halted>) -> BoxFuture<'static, ()> {
-        let router = router(tracker);
+    pub fn start(
+        &self,
+        tracker: Arc<Tracker>,
+        access_tokens: Arc<AccessTokens>,
+        tx_start: Sender<Started>,
+        rx_halt: Receiver<Halted>,
+    ) -> BoxFuture<'static, ()> {
+        let router = router(tracker, access_tokens);
         let socket = std::net::TcpListener::bind(self.bind_to).expect("Could not bind tcp_listener to address.");
         let address = socket.local_addr().expect("Could not get local_addr from tcp_listener.");
 
@@ -227,8 +234,13 @@ mod tests {
             .await
             .map(|tls| tls.expect("tls config failed"));
 
+        let access_tokens = Arc::new(config.access_tokens.clone());
+
         let stopped = ApiServer::new(Launcher::new(bind_to, tls));
-        let started = stopped.start(tracker).await.expect("it should start the server");
+        let started = stopped
+            .start(tracker, access_tokens)
+            .await
+            .expect("it should start the server");
         let stopped = started.stop().await.expect("it should stop the server");
 
         assert_eq!(stopped.state.launcher.bind_to, bind_to);
diff --git a/src/servers/apis/v1/middlewares/auth.rs b/src/servers/apis/v1/middlewares/auth.rs
index 7749b3b34..58219c7ca 100644
--- a/src/servers/apis/v1/middlewares/auth.rs
+++ b/src/servers/apis/v1/middlewares/auth.rs
@@ -23,12 +23,12 @@
 //! identify the token.
 use std::sync::Arc;
 
-use axum::extract::{Query, State};
+use axum::extract::{self};
 use axum::http::Request;
 use axum::middleware::Next;
 use axum::response::{IntoResponse, Response};
 use serde::Deserialize;
-use torrust_tracker_configuration::{Configuration, HttpApi};
+use torrust_tracker_configuration::AccessTokens;
 
 use crate::servers::apis::v1::responses::unhandled_rejection_response;
 
@@ -38,11 +38,16 @@ pub struct QueryParams {
     pub token: Option<String>,
 }
 
+#[derive(Clone, Debug)]
+pub struct State {
+    pub access_tokens: Arc<AccessTokens>,
+}
+
 /// Middleware for authentication using a "token" GET param.
 /// The token must be one of the tokens in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi).
 pub async fn auth(
-    State(config): State<Arc<Configuration>>,
-    Query(params): Query<QueryParams>,
+    extract::State(state): extract::State<State>,
+    extract::Query(params): extract::Query<QueryParams>,
     request: Request<axum::body::Body>,
     next: Next,
 ) -> Response {
@@ -50,7 +55,7 @@ pub async fn auth(
         return AuthError::Unauthorized.into_response();
     };
 
-    if !authenticate(&token, &config.http_api) {
+    if !authenticate(&token, &state.access_tokens) {
         return AuthError::TokenNotValid.into_response();
     }
 
@@ -73,8 +78,8 @@ impl IntoResponse for AuthError {
     }
 }
 
-fn authenticate(token: &str, http_api_config: &HttpApi) -> bool {
-    http_api_config.contains_token(token)
+fn authenticate(token: &str, tokens: &AccessTokens) -> bool {
+    tokens.values().any(|t| t == token)
 }
 
 /// `500` error response returned when the token is missing.
diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs
index cfe422e7f..be2085613 100644
--- a/src/servers/http/v1/handlers/announce.rs
+++ b/src/servers/http/v1/handlers/announce.rs
@@ -104,7 +104,7 @@ async fn handle_announce(
         Err(error) => return Err(responses::error::Error::from(error)),
     }
 
-    let peer_ip = match peer_ip_resolver::invoke(tracker.config.on_reverse_proxy, client_ip_sources) {
+    let peer_ip = match peer_ip_resolver::invoke(tracker.is_behind_reverse_proxy(), client_ip_sources) {
         Ok(peer_ip) => peer_ip,
         Err(error) => return Err(responses::error::Error::from(error)),
     };
@@ -166,19 +166,19 @@ mod tests {
     use crate::shared::bit_torrent::info_hash::InfoHash;
 
     fn private_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_private().into())
+        tracker_factory(&configuration::ephemeral_mode_private())
     }
 
     fn whitelisted_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_whitelisted().into())
+        tracker_factory(&configuration::ephemeral_mode_whitelisted())
     }
 
     fn tracker_on_reverse_proxy() -> Tracker {
-        tracker_factory(configuration::ephemeral_with_reverse_proxy().into())
+        tracker_factory(&configuration::ephemeral_with_reverse_proxy())
     }
 
     fn tracker_not_on_reverse_proxy() -> Tracker {
-        tracker_factory(configuration::ephemeral_without_reverse_proxy().into())
+        tracker_factory(&configuration::ephemeral_without_reverse_proxy())
     }
 
     fn sample_announce_request() -> Announce {
diff --git a/src/servers/http/v1/handlers/scrape.rs b/src/servers/http/v1/handlers/scrape.rs
index 298d47383..49b1aebc7 100644
--- a/src/servers/http/v1/handlers/scrape.rs
+++ b/src/servers/http/v1/handlers/scrape.rs
@@ -90,7 +90,7 @@ async fn handle_scrape(
     // Authorization for scrape requests is handled at the `Tracker` level
     // for each torrent.
 
-    let peer_ip = match peer_ip_resolver::invoke(tracker.config.on_reverse_proxy, client_ip_sources) {
+    let peer_ip = match peer_ip_resolver::invoke(tracker.is_behind_reverse_proxy(), client_ip_sources) {
         Ok(peer_ip) => peer_ip,
         Err(error) => return Err(responses::error::Error::from(error)),
     };
@@ -121,19 +121,19 @@ mod tests {
     use crate::shared::bit_torrent::info_hash::InfoHash;
 
     fn private_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_private().into())
+        tracker_factory(&configuration::ephemeral_mode_private())
     }
 
     fn whitelisted_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_whitelisted().into())
+        tracker_factory(&configuration::ephemeral_mode_whitelisted())
     }
 
     fn tracker_on_reverse_proxy() -> Tracker {
-        tracker_factory(configuration::ephemeral_with_reverse_proxy().into())
+        tracker_factory(&configuration::ephemeral_with_reverse_proxy())
     }
 
     fn tracker_not_on_reverse_proxy() -> Tracker {
-        tracker_factory(configuration::ephemeral_without_reverse_proxy().into())
+        tracker_factory(&configuration::ephemeral_without_reverse_proxy())
     }
 
     fn sample_scrape_request() -> Scrape {
diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs
index 80dc1ca5b..b791defd7 100644
--- a/src/servers/http/v1/services/announce.rs
+++ b/src/servers/http/v1/services/announce.rs
@@ -56,7 +56,7 @@ mod tests {
     use crate::shared::clock::DurationSinceUnixEpoch;
 
     fn public_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_public().into())
+        tracker_factory(&configuration::ephemeral_mode_public())
     }
 
     fn sample_info_hash() -> InfoHash {
@@ -94,7 +94,6 @@ mod tests {
         use std::sync::Arc;
 
         use mockall::predicate::eq;
-        use torrust_tracker_configuration::AnnouncePolicy;
         use torrust_tracker_test_helpers::configuration;
 
         use super::{sample_peer_using_ipv4, sample_peer_using_ipv6};
@@ -119,7 +118,7 @@ mod tests {
                     complete: 1,
                     incomplete: 0,
                 },
-                policy: AnnouncePolicy::default(),
+                policy: tracker.get_announce_policy(),
             };
 
             assert_eq!(announce_data, expected_announce_data);
@@ -135,14 +134,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let mut peer = sample_peer_using_ipv4();
 
@@ -154,7 +147,7 @@ mod tests {
             configuration.external_ip =
                 Some(IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)).to_string());
 
-            Tracker::new(Arc::new(configuration), Some(stats_event_sender), statistics::Repo::new()).unwrap()
+            Tracker::new(&configuration, Some(stats_event_sender), statistics::Repo::new()).unwrap()
         }
 
         fn peer_with_the_ipv4_loopback_ip() -> Peer {
@@ -199,14 +192,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let mut peer = sample_peer_using_ipv6();
 
diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs
index c2fa104de..82ca15dc8 100644
--- a/src/servers/http/v1/services/scrape.rs
+++ b/src/servers/http/v1/services/scrape.rs
@@ -69,7 +69,7 @@ mod tests {
     use crate::shared::clock::DurationSinceUnixEpoch;
 
     fn public_tracker() -> Tracker {
-        tracker_factory(configuration::ephemeral_mode_public().into())
+        tracker_factory(&configuration::ephemeral_mode_public())
     }
 
     fn sample_info_hashes() -> Vec<InfoHash> {
@@ -145,14 +145,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1));
 
@@ -169,14 +163,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969));
 
@@ -228,14 +216,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1));
 
@@ -252,14 +234,8 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let tracker = Arc::new(
-                Tracker::new(
-                    Arc::new(configuration::ephemeral()),
-                    Some(stats_event_sender),
-                    statistics::Repo::new(),
-                )
-                .unwrap(),
-            );
+            let tracker =
+                Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
 
             let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969));
 
diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs
index 34ebaec89..b77cd3a42 100644
--- a/src/servers/udp/handlers.rs
+++ b/src/servers/udp/handlers.rs
@@ -151,7 +151,7 @@ pub async fn handle_announce(
     if remote_addr.is_ipv4() {
         let announce_response = AnnounceResponse {
             transaction_id: wrapped_announce_request.announce_request.transaction_id,
-            announce_interval: AnnounceInterval(i64::from(tracker.config.announce_interval) as i32),
+            announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32),
             leechers: NumberOfPeers(i64::from(response.stats.incomplete) as i32),
             seeders: NumberOfPeers(i64::from(response.stats.complete) as i32),
             peers: response
@@ -176,7 +176,7 @@ pub async fn handle_announce(
     } else {
         let announce_response = AnnounceResponse {
             transaction_id: wrapped_announce_request.announce_request.transaction_id,
-            announce_interval: AnnounceInterval(i64::from(tracker.config.announce_interval) as i32),
+            announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32),
             leechers: NumberOfPeers(i64::from(response.stats.incomplete) as i32),
             seeders: NumberOfPeers(i64::from(response.stats.complete) as i32),
             peers: response
@@ -282,8 +282,8 @@ mod tests {
     use crate::core::{peer, Tracker};
     use crate::shared::clock::{Current, Time};
 
-    fn tracker_configuration() -> Arc<Configuration> {
-        Arc::new(default_testing_tracker_configuration())
+    fn tracker_configuration() -> Configuration {
+        default_testing_tracker_configuration()
     }
 
     fn default_testing_tracker_configuration() -> Configuration {
@@ -291,18 +291,18 @@ mod tests {
     }
 
     fn public_tracker() -> Arc<Tracker> {
-        initialized_tracker(configuration::ephemeral_mode_public().into())
+        initialized_tracker(&configuration::ephemeral_mode_public())
     }
 
     fn private_tracker() -> Arc<Tracker> {
-        initialized_tracker(configuration::ephemeral_mode_private().into())
+        initialized_tracker(&configuration::ephemeral_mode_private())
     }
 
     fn whitelisted_tracker() -> Arc<Tracker> {
-        initialized_tracker(configuration::ephemeral_mode_whitelisted().into())
+        initialized_tracker(&configuration::ephemeral_mode_whitelisted())
     }
 
-    fn initialized_tracker(configuration: Arc<Configuration>) -> Arc<Tracker> {
+    fn initialized_tracker(configuration: &Configuration) -> Arc<Tracker> {
         tracker_factory(configuration).into()
     }
 
@@ -452,8 +452,9 @@ mod tests {
 
             let client_socket_address = sample_ipv4_socket_address();
 
-            let torrent_tracker =
-                Arc::new(core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
+            let torrent_tracker = Arc::new(
+                core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+            );
             handle_connect(client_socket_address, &sample_connect_request(), &torrent_tracker)
                 .await
                 .unwrap();
@@ -469,8 +470,9 @@ mod tests {
                 .returning(|_| Box::pin(future::ready(Some(Ok(())))));
             let stats_event_sender = Box::new(stats_event_sender_mock);
 
-            let torrent_tracker =
-                Arc::new(core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap());
+            let torrent_tracker = Arc::new(
+                core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+            );
             handle_connect(sample_ipv6_remote_addr(), &sample_connect_request(), &torrent_tracker)
                 .await
                 .unwrap();
@@ -710,7 +712,7 @@ mod tests {
                 let stats_event_sender = Box::new(stats_event_sender_mock);
 
                 let tracker = Arc::new(
-                    core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+                    core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
                 );
 
                 handle_announce(
@@ -756,12 +758,11 @@ mod tests {
 
                     let peers = tracker.get_torrent_peers(&info_hash.0.into()).await;
 
-                    let external_ip_in_tracker_configuration =
-                        tracker.config.external_ip.clone().unwrap().parse::<Ipv4Addr>().unwrap();
+                    let external_ip_in_tracker_configuration = tracker.get_maybe_external_ip().unwrap();
 
                     let expected_peer = TorrentPeerBuilder::default()
                         .with_peer_id(peer::Id(peer_id.0))
-                        .with_peer_addr(SocketAddr::new(IpAddr::V4(external_ip_in_tracker_configuration), client_port))
+                        .with_peer_addr(SocketAddr::new(external_ip_in_tracker_configuration, client_port))
                         .into();
 
                     assert_eq!(peers[0], expected_peer);
@@ -938,7 +939,7 @@ mod tests {
                 let stats_event_sender = Box::new(stats_event_sender_mock);
 
                 let tracker = Arc::new(
-                    core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+                    core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
                 );
 
                 let remote_addr = sample_ipv6_remote_addr();
@@ -968,7 +969,7 @@ mod tests {
                     let configuration = Arc::new(TrackerConfigurationBuilder::default().with_external_ip("::126.0.0.1").into());
                     let (stats_event_sender, stats_repository) = Keeper::new_active_instance();
                     let tracker =
-                        Arc::new(core::Tracker::new(configuration, Some(stats_event_sender), stats_repository).unwrap());
+                        Arc::new(core::Tracker::new(&configuration, Some(stats_event_sender), stats_repository).unwrap());
 
                     let loopback_ipv4 = Ipv4Addr::new(127, 0, 0, 1);
                     let loopback_ipv6 = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1);
@@ -994,8 +995,9 @@ mod tests {
 
                     let peers = tracker.get_torrent_peers(&info_hash.0.into()).await;
 
-                    let _external_ip_in_tracker_configuration =
-                        tracker.config.external_ip.clone().unwrap().parse::<Ipv6Addr>().unwrap();
+                    let external_ip_in_tracker_configuration = tracker.get_maybe_external_ip().unwrap();
+
+                    assert!(external_ip_in_tracker_configuration.is_ipv6());
 
                     // There's a special type of IPv6 addresses that provide compatibility with IPv4.
                     // The last 32 bits of these addresses represent an IPv4, and are represented like this:
@@ -1246,7 +1248,7 @@ mod tests {
 
                 let remote_addr = sample_ipv4_remote_addr();
                 let tracker = Arc::new(
-                    core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+                    core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
                 );
 
                 handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker)
@@ -1278,7 +1280,7 @@ mod tests {
 
                 let remote_addr = sample_ipv6_remote_addr();
                 let tracker = Arc::new(
-                    core::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
+                    core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(),
                 );
 
                 handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker)
diff --git a/tests/servers/api/test_environment.rs b/tests/servers/api/test_environment.rs
index 166bfd7d1..c6878c674 100644
--- a/tests/servers/api/test_environment.rs
+++ b/tests/servers/api/test_environment.rs
@@ -1,13 +1,12 @@
-use std::net::SocketAddr;
 use std::sync::Arc;
 
-use axum_server::tls_rustls::RustlsConfig;
 use futures::executor::block_on;
 use torrust_tracker::bootstrap::jobs::make_rust_tls;
 use torrust_tracker::core::peer::Peer;
 use torrust_tracker::core::Tracker;
 use torrust_tracker::servers::apis::server::{ApiServer, Launcher, RunningApiServer, StoppedApiServer};
 use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
+use torrust_tracker_configuration::HttpApi;
 
 use super::connection_info::ConnectionInfo;
 use crate::common::app::setup_with_configuration;
@@ -18,7 +17,7 @@ pub type StoppedTestEnvironment = TestEnvironment<Stopped>;
 pub type RunningTestEnvironment = TestEnvironment<Running>;
 
 pub struct TestEnvironment<S> {
-    pub cfg: Arc<torrust_tracker_configuration::Configuration>,
+    pub config: Arc<HttpApi>,
     pub tracker: Arc<Tracker>,
     pub state: S,
 }
@@ -41,9 +40,10 @@ impl<S> TestEnvironment<S> {
 
 impl TestEnvironment<Stopped> {
     pub fn new(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let tracker = setup_with_configuration(&Arc::new(cfg));
+        let cfg = Arc::new(cfg);
+        let tracker = setup_with_configuration(&cfg);
 
-        let config = tracker.config.http_api.clone();
+        let config = Arc::new(cfg.http_api.clone());
 
         let bind_to = config
             .bind_address
@@ -53,25 +53,23 @@ impl TestEnvironment<Stopped> {
         let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path))
             .map(|tls| tls.expect("tls config failed"));
 
-        Self::new_stopped(tracker, bind_to, tls)
-    }
-
-    pub fn new_stopped(tracker: Arc<Tracker>, bind_to: SocketAddr, tls: Option<RustlsConfig>) -> Self {
         let api_server = api_server(Launcher::new(bind_to, tls));
 
         Self {
-            cfg: tracker.config.clone(),
+            config,
             tracker,
             state: Stopped { api_server },
         }
     }
 
     pub async fn start(self) -> TestEnvironment<Running> {
+        let access_tokens = Arc::new(self.config.access_tokens.clone());
+
         TestEnvironment {
-            cfg: self.cfg,
+            config: self.config,
             tracker: self.tracker.clone(),
             state: Running {
-                api_server: self.state.api_server.start(self.tracker).await.unwrap(),
+                api_server: self.state.api_server.start(self.tracker, access_tokens).await.unwrap(),
             },
         }
     }
@@ -90,7 +88,7 @@ impl TestEnvironment<Running> {
 
     pub async fn stop(self) -> TestEnvironment<Stopped> {
         TestEnvironment {
-            cfg: self.cfg,
+            config: self.config,
             tracker: self.tracker,
             state: Stopped {
                 api_server: self.state.api_server.stop().await.unwrap(),
@@ -101,7 +99,7 @@ impl TestEnvironment<Running> {
     pub fn get_connection_info(&self) -> ConnectionInfo {
         ConnectionInfo {
             bind_address: self.state.api_server.state.binding.to_string(),
-            api_token: self.cfg.http_api.access_tokens.get("admin").cloned(),
+            api_token: self.config.access_tokens.get("admin").cloned(),
         }
     }
 }
diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs
index f3d1fcef0..e394779ad 100644
--- a/tests/servers/http/v1/contract.rs
+++ b/tests/servers/http/v1/contract.rs
@@ -387,13 +387,15 @@ mod for_all_config_modes {
                 )
                 .await;
 
+            let announce_policy = test_env.tracker.get_announce_policy();
+
             assert_announce_response(
                 response,
                 &Announce {
                     complete: 1, // the peer for this test
                     incomplete: 0,
-                    interval: test_env.tracker.config.announce_interval,
-                    min_interval: test_env.tracker.config.min_announce_interval,
+                    interval: announce_policy.interval,
+                    min_interval: announce_policy.interval_min,
                     peers: vec![],
                 },
             )
@@ -426,14 +428,16 @@ mod for_all_config_modes {
                 )
                 .await;
 
+            let announce_policy = test_env.tracker.get_announce_policy();
+
             // It should only contain the previously announced peer
             assert_announce_response(
                 response,
                 &Announce {
                     complete: 2,
                     incomplete: 0,
-                    interval: test_env.tracker.config.announce_interval,
-                    min_interval: test_env.tracker.config.min_announce_interval,
+                    interval: announce_policy.interval,
+                    min_interval: announce_policy.interval_min,
                     peers: vec![DictionaryPeer::from(previously_announced_peer)],
                 },
             )
@@ -475,6 +479,8 @@ mod for_all_config_modes {
                 )
                 .await;
 
+            let announce_policy = test_env.tracker.get_announce_policy();
+
             // The newly announced peer is not included on the response peer list,
             // but all the previously announced peers should be included regardless the IP version they are using.
             assert_announce_response(
@@ -482,8 +488,8 @@ mod for_all_config_modes {
                 &Announce {
                     complete: 3,
                     incomplete: 0,
-                    interval: test_env.tracker.config.announce_interval,
-                    min_interval: test_env.tracker.config.min_announce_interval,
+                    interval: announce_policy.interval,
+                    min_interval: announce_policy.interval_min,
                     peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)],
                 },
             )
@@ -787,7 +793,7 @@ mod for_all_config_modes {
             let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
-            assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap());
+            assert_eq!(peer_addr.ip(), test_env.tracker.get_maybe_external_ip().unwrap());
             assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
 
             test_env.stop().await;
@@ -826,7 +832,7 @@ mod for_all_config_modes {
             let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
-            assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap());
+            assert_eq!(peer_addr.ip(), test_env.tracker.get_maybe_external_ip().unwrap());
             assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
 
             test_env.stop().await;

From b310c7558f5717145f1b1aa14cf6dd7839722c45 Mon Sep 17 00:00:00 2001
From: Cameron Garnham <me@da2ce7.com>
Date: Fri, 5 Jan 2024 17:21:06 +1100
Subject: [PATCH 2/3] dev: extract config from health check

---
 cSpell.json                                   |   2 +
 src/app.rs                                    |  29 +++-
 src/bootstrap/jobs/health_check_api.rs        |   9 +-
 src/bootstrap/jobs/http_tracker.rs            |  22 ++-
 src/bootstrap/jobs/tracker_apis.rs            |  16 +-
 src/bootstrap/jobs/udp_tracker.rs             |   5 +-
 src/main.rs                                   |   2 +-
 src/servers/apis/server.rs                    |  65 ++++++--
 src/servers/health_check_api/handlers.rs      | 150 ++++--------------
 src/servers/health_check_api/resources.rs     |  31 +++-
 src/servers/health_check_api/responses.rs     |  10 +-
 src/servers/health_check_api/server.rs        |   7 +-
 src/servers/http/server.rs                    |  40 ++++-
 src/servers/mod.rs                            |   1 +
 src/servers/registar.rs                       |  95 +++++++++++
 src/servers/udp/server.rs                     |  26 ++-
 src/shared/bit_torrent/tracker/udp/client.rs  |  26 ++-
 tests/servers/api/test_environment.rs         |   8 +-
 tests/servers/health_check_api/contract.rs    |   9 +-
 .../health_check_api/test_environment.rs      |   9 +-
 tests/servers/http/test_environment.rs        |   8 +-
 tests/servers/udp/test_environment.rs         |   5 +-
 22 files changed, 392 insertions(+), 183 deletions(-)
 create mode 100644 src/servers/registar.rs

diff --git a/cSpell.json b/cSpell.json
index 7b3ce4de9..e02c6ed87 100644
--- a/cSpell.json
+++ b/cSpell.json
@@ -34,6 +34,7 @@
         "Cyberneering",
         "datagram",
         "datetime",
+        "Deque",
         "Dijke",
         "distroless",
         "dockerhub",
@@ -91,6 +92,7 @@
         "Rasterbar",
         "realpath",
         "reannounce",
+        "Registar",
         "repr",
         "reqwest",
         "rerequests",
diff --git a/src/app.rs b/src/app.rs
index 3608aa22e..3ec9806d3 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -28,6 +28,7 @@ use tokio::task::JoinHandle;
 use torrust_tracker_configuration::Configuration;
 
 use crate::bootstrap::jobs::{health_check_api, http_tracker, torrent_cleanup, tracker_apis, udp_tracker};
+use crate::servers::registar::Registar;
 use crate::{core, servers};
 
 /// # Panics
@@ -36,9 +37,11 @@ use crate::{core, servers};
 ///
 /// - Can't retrieve tracker keys from database.
 /// - Can't load whitelist from database.
-pub async fn start(config: Arc<Configuration>, tracker: Arc<core::Tracker>) -> Vec<JoinHandle<()>> {
+pub async fn start(config: &Configuration, tracker: Arc<core::Tracker>) -> Vec<JoinHandle<()>> {
     let mut jobs: Vec<JoinHandle<()>> = Vec::new();
 
+    let registar = Registar::default();
+
     // Load peer keys
     if tracker.is_private() {
         tracker
@@ -67,31 +70,45 @@ pub async fn start(config: Arc<Configuration>, tracker: Arc<core::Tracker>) -> V
                 udp_tracker_config.bind_address, config.mode
             );
         } else {
-            jobs.push(udp_tracker::start_job(udp_tracker_config, tracker.clone()).await);
+            jobs.push(udp_tracker::start_job(udp_tracker_config, tracker.clone(), registar.give_form()).await);
         }
     }
 
     // Start the HTTP blocks
     for http_tracker_config in &config.http_trackers {
-        if let Some(job) = http_tracker::start_job(http_tracker_config, tracker.clone(), servers::http::Version::V1).await {
+        if let Some(job) = http_tracker::start_job(
+            http_tracker_config,
+            tracker.clone(),
+            registar.give_form(),
+            servers::http::Version::V1,
+        )
+        .await
+        {
             jobs.push(job);
         };
     }
 
     // Start HTTP API
     if config.http_api.enabled {
-        if let Some(job) = tracker_apis::start_job(&config.http_api, tracker.clone(), servers::apis::Version::V1).await {
+        if let Some(job) = tracker_apis::start_job(
+            &config.http_api,
+            tracker.clone(),
+            registar.give_form(),
+            servers::apis::Version::V1,
+        )
+        .await
+        {
             jobs.push(job);
         };
     }
 
     // Start runners to remove torrents without peers, every interval
     if config.inactive_peer_cleanup_interval > 0 {
-        jobs.push(torrent_cleanup::start_job(&config, &tracker));
+        jobs.push(torrent_cleanup::start_job(config, &tracker));
     }
 
     // Start Health Check API
-    jobs.push(health_check_api::start_job(config).await);
+    jobs.push(health_check_api::start_job(&config.health_check_api, registar.entries()).await);
 
     jobs
 }
diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs
index 9fed56435..1a9815280 100644
--- a/src/bootstrap/jobs/health_check_api.rs
+++ b/src/bootstrap/jobs/health_check_api.rs
@@ -13,15 +13,15 @@
 //!
 //! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration)
 //! for the API configuration options.
-use std::sync::Arc;
 
 use log::info;
 use tokio::sync::oneshot;
 use tokio::task::JoinHandle;
-use torrust_tracker_configuration::Configuration;
+use torrust_tracker_configuration::HealthCheckApi;
 
 use super::Started;
 use crate::servers::health_check_api::server;
+use crate::servers::registar::ServiceRegistry;
 
 /// This function starts a new Health Check API server with the provided
 /// configuration.
@@ -33,9 +33,8 @@ use crate::servers::health_check_api::server;
 /// # Panics
 ///
 /// It would panic if unable to send the  `ApiServerJobStarted` notice.
-pub async fn start_job(config: Arc<Configuration>) -> JoinHandle<()> {
+pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> JoinHandle<()> {
     let bind_addr = config
-        .health_check_api
         .bind_address
         .parse::<std::net::SocketAddr>()
         .expect("it should have a valid health check bind address");
@@ -46,7 +45,7 @@ pub async fn start_job(config: Arc<Configuration>) -> JoinHandle<()> {
     let join_handle = tokio::spawn(async move {
         info!(target: "Health Check API", "Starting on: http://{}", bind_addr);
 
-        let handle = server::start(bind_addr, tx_start, config.clone());
+        let handle = server::start(bind_addr, tx_start, register);
 
         if let Ok(()) = handle.await {
             info!(target: "Health Check API", "Stopped server running on: http://{}", bind_addr);
diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs
index 69ff345db..0a0638b78 100644
--- a/src/bootstrap/jobs/http_tracker.rs
+++ b/src/bootstrap/jobs/http_tracker.rs
@@ -22,6 +22,7 @@ use super::make_rust_tls;
 use crate::core;
 use crate::servers::http::server::{HttpServer, Launcher};
 use crate::servers::http::Version;
+use crate::servers::registar::ServiceRegistrationForm;
 
 /// It starts a new HTTP server with the provided configuration and version.
 ///
@@ -32,7 +33,12 @@ use crate::servers::http::Version;
 ///
 /// It would panic if the `config::HttpTracker` struct would contain inappropriate values.
 ///
-pub async fn start_job(config: &HttpTracker, tracker: Arc<core::Tracker>, version: Version) -> Option<JoinHandle<()>> {
+pub async fn start_job(
+    config: &HttpTracker,
+    tracker: Arc<core::Tracker>,
+    form: ServiceRegistrationForm,
+    version: Version,
+) -> Option<JoinHandle<()>> {
     if config.enabled {
         let socket = config
             .bind_address
@@ -44,7 +50,7 @@ pub async fn start_job(config: &HttpTracker, tracker: Arc<core::Tracker>, versio
             .map(|tls| tls.expect("it should have a valid http tracker tls configuration"));
 
         match version {
-            Version::V1 => Some(start_v1(socket, tls, tracker.clone()).await),
+            Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await),
         }
     } else {
         info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration.");
@@ -52,9 +58,14 @@ pub async fn start_job(config: &HttpTracker, tracker: Arc<core::Tracker>, versio
     }
 }
 
-async fn start_v1(socket: SocketAddr, tls: Option<RustlsConfig>, tracker: Arc<core::Tracker>) -> JoinHandle<()> {
+async fn start_v1(
+    socket: SocketAddr,
+    tls: Option<RustlsConfig>,
+    tracker: Arc<core::Tracker>,
+    form: ServiceRegistrationForm,
+) -> JoinHandle<()> {
     let server = HttpServer::new(Launcher::new(socket, tls))
-        .start(tracker)
+        .start(tracker, form)
         .await
         .expect("it should be able to start to the http tracker");
 
@@ -80,6 +91,7 @@ mod tests {
     use crate::bootstrap::app::initialize_with_configuration;
     use crate::bootstrap::jobs::http_tracker::start_job;
     use crate::servers::http::Version;
+    use crate::servers::registar::Registar;
 
     #[tokio::test]
     async fn it_should_start_http_tracker() {
@@ -88,7 +100,7 @@ mod tests {
         let tracker = initialize_with_configuration(&cfg);
         let version = Version::V1;
 
-        start_job(config, tracker, version)
+        start_job(config, tracker, Registar::default().give_form(), version)
             .await
             .expect("it should be able to join to the http tracker start-job");
     }
diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs
index 43cb5de8e..ffd7c7407 100644
--- a/src/bootstrap/jobs/tracker_apis.rs
+++ b/src/bootstrap/jobs/tracker_apis.rs
@@ -32,6 +32,7 @@ use super::make_rust_tls;
 use crate::core;
 use crate::servers::apis::server::{ApiServer, Launcher};
 use crate::servers::apis::Version;
+use crate::servers::registar::ServiceRegistrationForm;
 
 /// This is the message that the "launcher" spawned task sends to the main
 /// application process to notify the API server was successfully started.
@@ -53,7 +54,12 @@ pub struct ApiServerJobStarted();
 /// It would panic if unable to send the  `ApiServerJobStarted` notice.
 ///
 ///
-pub async fn start_job(config: &HttpApi, tracker: Arc<core::Tracker>, version: Version) -> Option<JoinHandle<()>> {
+pub async fn start_job(
+    config: &HttpApi,
+    tracker: Arc<core::Tracker>,
+    form: ServiceRegistrationForm,
+    version: Version,
+) -> Option<JoinHandle<()>> {
     if config.enabled {
         let bind_to = config
             .bind_address
@@ -67,7 +73,7 @@ pub async fn start_job(config: &HttpApi, tracker: Arc<core::Tracker>, version: V
         let access_tokens = Arc::new(config.access_tokens.clone());
 
         match version {
-            Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), access_tokens).await),
+            Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), form, access_tokens).await),
         }
     } else {
         info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration.");
@@ -79,10 +85,11 @@ async fn start_v1(
     socket: SocketAddr,
     tls: Option<RustlsConfig>,
     tracker: Arc<core::Tracker>,
+    form: ServiceRegistrationForm,
     access_tokens: Arc<AccessTokens>,
 ) -> JoinHandle<()> {
     let server = ApiServer::new(Launcher::new(socket, tls))
-        .start(tracker, access_tokens)
+        .start(tracker, form, access_tokens)
         .await
         .expect("it should be able to start to the tracker api");
 
@@ -101,6 +108,7 @@ mod tests {
     use crate::bootstrap::app::initialize_with_configuration;
     use crate::bootstrap::jobs::tracker_apis::start_job;
     use crate::servers::apis::Version;
+    use crate::servers::registar::Registar;
 
     #[tokio::test]
     async fn it_should_start_http_tracker() {
@@ -109,7 +117,7 @@ mod tests {
         let tracker = initialize_with_configuration(&cfg);
         let version = Version::V1;
 
-        start_job(config, tracker, version)
+        start_job(config, tracker, Registar::default().give_form(), version)
             .await
             .expect("it should be able to join to the tracker api start-job");
     }
diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs
index 20ef0c793..275ce1381 100644
--- a/src/bootstrap/jobs/udp_tracker.rs
+++ b/src/bootstrap/jobs/udp_tracker.rs
@@ -13,6 +13,7 @@ use tokio::task::JoinHandle;
 use torrust_tracker_configuration::UdpTracker;
 
 use crate::core;
+use crate::servers::registar::ServiceRegistrationForm;
 use crate::servers::udp::server::{Launcher, UdpServer};
 
 /// It starts a new UDP server with the provided configuration.
@@ -25,14 +26,14 @@ use crate::servers::udp::server::{Launcher, UdpServer};
 /// It will panic if it is unable to start the UDP service.
 /// It will panic if the task did not finish successfully.
 #[must_use]
-pub async fn start_job(config: &UdpTracker, tracker: Arc<core::Tracker>) -> JoinHandle<()> {
+pub async fn start_job(config: &UdpTracker, tracker: Arc<core::Tracker>, form: ServiceRegistrationForm) -> JoinHandle<()> {
     let bind_to = config
         .bind_address
         .parse::<std::net::SocketAddr>()
         .expect("it should have a valid udp tracker bind address");
 
     let server = UdpServer::new(Launcher::new(bind_to))
-        .start(tracker)
+        .start(tracker, form)
         .await
         .expect("it should be able to start the udp tracker");
 
diff --git a/src/main.rs b/src/main.rs
index 5c65f8e07..bd07f4a58 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ use torrust_tracker::{app, bootstrap};
 async fn main() {
     let (config, tracker) = bootstrap::app::setup();
 
-    let jobs = app::start(config.into(), tracker.clone()).await;
+    let jobs = app::start(&config, tracker).await;
 
     // handle the signals
     tokio::select! {
diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs
index d26362f66..8aef9744c 100644
--- a/src/servers/apis/server.rs
+++ b/src/servers/apis/server.rs
@@ -37,6 +37,7 @@ use torrust_tracker_configuration::AccessTokens;
 use super::routes::router;
 use crate::bootstrap::jobs::Started;
 use crate::core::Tracker;
+use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm};
 use crate::servers::signals::{graceful_shutdown, Halted};
 
 /// Errors that can occur when starting or stopping the API server.
@@ -75,6 +76,21 @@ pub struct Running {
     pub task: tokio::task::JoinHandle<Launcher>,
 }
 
+impl Running {
+    #[must_use]
+    pub fn new(
+        binding: SocketAddr,
+        halt_task: tokio::sync::oneshot::Sender<Halted>,
+        task: tokio::task::JoinHandle<Launcher>,
+    ) -> Self {
+        Self {
+            binding,
+            halt_task,
+            task,
+        }
+    }
+}
+
 impl ApiServer<Stopped> {
     #[must_use]
     pub fn new(launcher: Launcher) -> Self {
@@ -92,7 +108,12 @@ impl ApiServer<Stopped> {
     /// # Panics
     ///
     /// It would panic if the bound socket address cannot be sent back to this starter.
-    pub async fn start(self, tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Result<ApiServer<Running>, Error> {
+    pub async fn start(
+        self,
+        tracker: Arc<Tracker>,
+        form: ServiceRegistrationForm,
+        access_tokens: Arc<AccessTokens>,
+    ) -> Result<ApiServer<Running>, Error> {
         let (tx_start, rx_start) = tokio::sync::oneshot::channel::<Started>();
         let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
 
@@ -104,13 +125,14 @@ impl ApiServer<Stopped> {
         });
 
         let api_server = match rx_start.await {
-            Ok(started) => ApiServer {
-                state: Running {
-                    binding: started.address,
-                    halt_task: tx_halt,
-                    task,
-                },
-            },
+            Ok(started) => {
+                form.send(ServiceRegistration::new(started.address, check_fn))
+                    .expect("it should be able to send service registration");
+
+                ApiServer {
+                    state: Running::new(started.address, tx_halt, task),
+                }
+            }
             Err(err) => {
                 let msg = format!("Unable to start API server: {err}");
                 error!("{}", msg);
@@ -142,6 +164,27 @@ impl ApiServer<Running> {
     }
 }
 
+/// Checks the Health by connecting to the API service endpoint.
+///
+/// # Errors
+///
+/// This function will return an error if unable to connect.
+/// Or if there request returns an error code.
+#[must_use]
+pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob {
+    let url = format!("http://{binding}/api/health_check");
+
+    let info = format!("checking api health check at: {url}");
+
+    let job = tokio::spawn(async move {
+        match reqwest::get(url).await {
+            Ok(response) => Ok(response.status().to_string()),
+            Err(err) => Err(err.to_string()),
+        }
+    });
+    ServiceHealthCheckJob::new(*binding, info, job)
+}
+
 /// A struct responsible for starting the API server.
 #[derive(Constructor, Debug)]
 pub struct Launcher {
@@ -218,6 +261,7 @@ mod tests {
     use crate::bootstrap::app::initialize_with_configuration;
     use crate::bootstrap::jobs::make_rust_tls;
     use crate::servers::apis::server::{ApiServer, Launcher};
+    use crate::servers::registar::Registar;
 
     #[tokio::test]
     async fn it_should_be_able_to_start_and_stop() {
@@ -237,8 +281,11 @@ mod tests {
         let access_tokens = Arc::new(config.access_tokens.clone());
 
         let stopped = ApiServer::new(Launcher::new(bind_to, tls));
+
+        let register = &Registar::default();
+
         let started = stopped
-            .start(tracker, access_tokens)
+            .start(tracker, register.give_form(), access_tokens)
             .await
             .expect("it should start the server");
         let stopped = started.stop().await.expect("it should stop the server");
diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs
index 4403676af..35382583e 100644
--- a/src/servers/health_check_api/handlers.rs
+++ b/src/servers/health_check_api/handlers.rs
@@ -1,135 +1,45 @@
-use std::net::SocketAddr;
-use std::sync::Arc;
+use std::collections::VecDeque;
 
-use aquatic_udp_protocol::{ConnectRequest, Response, TransactionId};
 use axum::extract::State;
 use axum::Json;
-use torrust_tracker_configuration::{Configuration, HttpApi, HttpTracker, UdpTracker};
 
-use super::resources::Report;
+use super::resources::{CheckReport, Report};
 use super::responses;
-use crate::shared::bit_torrent::tracker::udp::client::new_udp_tracker_client_connected;
-
-/// If port 0 is specified in the configuration the OS will automatically
-/// assign a free port. But we do now know in from the configuration.
-/// We can only know it after starting the socket.
-const UNKNOWN_PORT: u16 = 0;
+use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistry};
 
 /// Endpoint for container health check.
 ///
-/// This endpoint only checks services when we know the port from the
-/// configuration. If port 0 is specified in the configuration the health check
-/// for that service is skipped.
-pub(crate) async fn health_check_handler(State(config): State<Arc<Configuration>>) -> Json<Report> {
-    if let Some(err_response) = api_health_check(&config.http_api).await {
-        return err_response;
-    }
-
-    if let Some(err_response) = http_trackers_health_check(&config.http_trackers).await {
-        return err_response;
-    }
-
-    if let Some(err_response) = udp_trackers_health_check(&config.udp_trackers).await {
-        return err_response;
-    }
-
-    responses::ok()
-}
-
-async fn api_health_check(config: &HttpApi) -> Option<Json<Report>> {
-    // todo: when port 0 is specified in the configuration get the port from the
-    // running service, after starting it as we do for testing with ephemeral
-    // configurations.
-
-    if config.enabled {
-        let addr: SocketAddr = config.bind_address.parse().expect("invalid socket address for API");
+/// Creates a vector [`CheckReport`] from the input set of [`CheckJob`], and then builds a report from the results.
+///
+pub(crate) async fn health_check_handler(State(register): State<ServiceRegistry>) -> Json<Report> {
+    #[allow(unused_assignments)]
+    let mut checks: VecDeque<ServiceHealthCheckJob> = VecDeque::new();
 
-        if addr.port() != UNKNOWN_PORT {
-            let health_check_url = format!("http://{addr}/api/health_check");
+    {
+        let mutex = register.lock();
 
-            if !get_req_is_ok(&health_check_url).await {
-                return Some(responses::error(format!(
-                    "API is not healthy. Health check endpoint: {health_check_url}"
-                )));
-            }
-        }
+        checks = mutex.await.values().map(ServiceRegistration::spawn_check).collect();
     }
 
-    None
-}
-
-async fn http_trackers_health_check(http_trackers: &Vec<HttpTracker>) -> Option<Json<Report>> {
-    // todo: when port 0 is specified in the configuration get the port from the
-    // running service, after starting it as we do for testing with ephemeral
-    // configurations.
-
-    for http_tracker_config in http_trackers {
-        if !http_tracker_config.enabled {
-            continue;
-        }
-
-        let addr: SocketAddr = http_tracker_config
-            .bind_address
-            .parse()
-            .expect("invalid socket address for HTTP tracker");
-
-        if addr.port() != UNKNOWN_PORT {
-            let health_check_url = format!("http://{addr}/health_check");
-
-            if !get_req_is_ok(&health_check_url).await {
-                return Some(responses::error(format!(
-                    "HTTP Tracker is not healthy. Health check endpoint: {health_check_url}"
-                )));
+    let jobs = checks.drain(..).map(|c| {
+        tokio::spawn(async move {
+            CheckReport {
+                binding: c.binding,
+                info: c.info.clone(),
+                result: c.job.await.expect("it should be able to join into the checking function"),
             }
-        }
+        })
+    });
+
+    let results: Vec<CheckReport> = futures::future::join_all(jobs)
+        .await
+        .drain(..)
+        .map(|r| r.expect("it should be able to connect to the job"))
+        .collect();
+
+    if results.iter().any(CheckReport::fail) {
+        responses::error("health check failed".to_string(), results)
+    } else {
+        responses::ok(results)
     }
-
-    None
-}
-
-async fn udp_trackers_health_check(udp_trackers: &Vec<UdpTracker>) -> Option<Json<Report>> {
-    // todo: when port 0 is specified in the configuration get the port from the
-    // running service, after starting it as we do for testing with ephemeral
-    // configurations.
-
-    for udp_tracker_config in udp_trackers {
-        if !udp_tracker_config.enabled {
-            continue;
-        }
-
-        let addr: SocketAddr = udp_tracker_config
-            .bind_address
-            .parse()
-            .expect("invalid socket address for UDP tracker");
-
-        if addr.port() != UNKNOWN_PORT && !can_connect_to_udp_tracker(&addr.to_string()).await {
-            return Some(responses::error(format!(
-                "UDP Tracker is not healthy. Can't connect to: {addr}"
-            )));
-        }
-    }
-
-    None
-}
-
-async fn get_req_is_ok(url: &str) -> bool {
-    match reqwest::get(url).await {
-        Ok(response) => response.status().is_success(),
-        Err(_err) => false,
-    }
-}
-
-/// Tries to connect to an UDP tracker. It returns true if it succeeded.
-async fn can_connect_to_udp_tracker(url: &str) -> bool {
-    let client = new_udp_tracker_client_connected(url).await;
-
-    let connect_request = ConnectRequest {
-        transaction_id: TransactionId(123),
-    };
-
-    client.send(connect_request.into()).await;
-
-    let response = client.receive().await;
-
-    matches!(response, Response::Connect(_connect_response))
 }
diff --git a/src/servers/health_check_api/resources.rs b/src/servers/health_check_api/resources.rs
index 3fadcf456..bb57cf20b 100644
--- a/src/servers/health_check_api/resources.rs
+++ b/src/servers/health_check_api/resources.rs
@@ -1,31 +1,54 @@
+use std::net::SocketAddr;
+
 use serde::{Deserialize, Serialize};
 
-#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
 pub enum Status {
     Ok,
     Error,
 }
 
-#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub struct CheckReport {
+    pub binding: SocketAddr,
+    pub info: String,
+    pub result: Result<String, String>,
+}
+
+impl CheckReport {
+    #[must_use]
+    pub fn pass(&self) -> bool {
+        self.result.is_ok()
+    }
+    #[must_use]
+    pub fn fail(&self) -> bool {
+        self.result.is_err()
+    }
+}
+
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
 pub struct Report {
     pub status: Status,
     pub message: String,
+    pub details: Vec<CheckReport>,
 }
 
 impl Report {
     #[must_use]
-    pub fn ok() -> Report {
+    pub fn ok(details: Vec<CheckReport>) -> Report {
         Self {
             status: Status::Ok,
             message: String::new(),
+            details,
         }
     }
 
     #[must_use]
-    pub fn error(message: String) -> Report {
+    pub fn error(message: String, details: Vec<CheckReport>) -> Report {
         Self {
             status: Status::Error,
             message,
+            details,
         }
     }
 }
diff --git a/src/servers/health_check_api/responses.rs b/src/servers/health_check_api/responses.rs
index 043e271db..8658caeb4 100644
--- a/src/servers/health_check_api/responses.rs
+++ b/src/servers/health_check_api/responses.rs
@@ -1,11 +1,11 @@
 use axum::Json;
 
-use super::resources::Report;
+use super::resources::{CheckReport, Report};
 
-pub fn ok() -> Json<Report> {
-    Json(Report::ok())
+pub fn ok(details: Vec<CheckReport>) -> Json<Report> {
+    Json(Report::ok(details))
 }
 
-pub fn error(message: String) -> Json<Report> {
-    Json(Report::error(message))
+pub fn error(message: String, details: Vec<CheckReport>) -> Json<Report> {
+    Json(Report::error(message, details))
 }
diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs
index fb807d09c..a7cbf4a8a 100644
--- a/src/servers/health_check_api/server.rs
+++ b/src/servers/health_check_api/server.rs
@@ -3,7 +3,6 @@
 //! This API is intended to be used by the container infrastructure to check if
 //! the whole application is healthy.
 use std::net::SocketAddr;
-use std::sync::Arc;
 
 use axum::routing::get;
 use axum::{Json, Router};
@@ -12,10 +11,10 @@ use futures::Future;
 use log::info;
 use serde_json::json;
 use tokio::sync::oneshot::Sender;
-use torrust_tracker_configuration::Configuration;
 
 use crate::bootstrap::jobs::Started;
 use crate::servers::health_check_api::handlers::health_check_handler;
+use crate::servers::registar::ServiceRegistry;
 
 /// Starts Health Check API server.
 ///
@@ -25,12 +24,12 @@ use crate::servers::health_check_api::handlers::health_check_handler;
 pub fn start(
     address: SocketAddr,
     tx: Sender<Started>,
-    config: Arc<Configuration>,
+    register: ServiceRegistry,
 ) -> impl Future<Output = Result<(), std::io::Error>> {
     let app = Router::new()
         .route("/", get(|| async { Json(json!({})) }))
         .route("/health_check", get(health_check_handler))
-        .with_state(config);
+        .with_state(register);
 
     let handle = Handle::new();
     let cloned_handle = handle.clone();
diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs
index 0a4b687b5..20e57db57 100644
--- a/src/servers/http/server.rs
+++ b/src/servers/http/server.rs
@@ -12,6 +12,7 @@ use tokio::sync::oneshot::{Receiver, Sender};
 use super::v1::routes::router;
 use crate::bootstrap::jobs::Started;
 use crate::core::Tracker;
+use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm};
 use crate::servers::signals::{graceful_shutdown, Halted};
 
 /// Error that can occur when starting or stopping the HTTP server.
@@ -143,7 +144,7 @@ impl HttpServer<Stopped> {
     ///
     /// It would panic spawned HTTP server launcher cannot send the bound `SocketAddr`
     /// back to the main thread.
-    pub async fn start(self, tracker: Arc<Tracker>) -> Result<HttpServer<Running>, Error> {
+    pub async fn start(self, tracker: Arc<Tracker>, form: ServiceRegistrationForm) -> Result<HttpServer<Running>, Error> {
         let (tx_start, rx_start) = tokio::sync::oneshot::channel::<Started>();
         let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
 
@@ -157,9 +158,14 @@ impl HttpServer<Stopped> {
             launcher
         });
 
+        let binding = rx_start.await.expect("it should be able to start the service").address;
+
+        form.send(ServiceRegistration::new(binding, check_fn))
+            .expect("it should be able to send service registration");
+
         Ok(HttpServer {
             state: Running {
-                binding: rx_start.await.expect("unable to start service").address,
+                binding,
                 halt_task: tx_halt,
                 task,
             },
@@ -188,6 +194,28 @@ impl HttpServer<Running> {
     }
 }
 
+/// Checks the Health by connecting to the HTTP tracker endpoint.
+///
+/// # Errors
+///
+/// This function will return an error if unable to connect.
+/// Or if the request returns an error.
+#[must_use]
+pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob {
+    let url = format!("http://{binding}/health_check");
+
+    let info = format!("checking http tracker health check at: {url}");
+
+    let job = tokio::spawn(async move {
+        match reqwest::get(url).await {
+            Ok(response) => Ok(response.status().to_string()),
+            Err(err) => Err(err.to_string()),
+        }
+    });
+
+    ServiceHealthCheckJob::new(*binding, info, job)
+}
+
 #[cfg(test)]
 mod tests {
     use std::sync::Arc;
@@ -197,6 +225,7 @@ mod tests {
     use crate::bootstrap::app::initialize_with_configuration;
     use crate::bootstrap::jobs::make_rust_tls;
     use crate::servers::http::server::{HttpServer, Launcher};
+    use crate::servers::registar::Registar;
 
     #[tokio::test]
     async fn it_should_be_able_to_start_and_stop() {
@@ -213,8 +242,13 @@ mod tests {
             .await
             .map(|tls| tls.expect("tls config failed"));
 
+        let register = &Registar::default();
+
         let stopped = HttpServer::new(Launcher::new(bind_to, tls));
-        let started = stopped.start(tracker).await.expect("it should start the server");
+        let started = stopped
+            .start(tracker, register.give_form())
+            .await
+            .expect("it should start the server");
         let stopped = started.stop().await.expect("it should stop the server");
 
         assert_eq!(stopped.state.launcher.bind_to, bind_to);
diff --git a/src/servers/mod.rs b/src/servers/mod.rs
index 077109f35..b0e222d2a 100644
--- a/src/servers/mod.rs
+++ b/src/servers/mod.rs
@@ -2,5 +2,6 @@
 pub mod apis;
 pub mod health_check_api;
 pub mod http;
+pub mod registar;
 pub mod signals;
 pub mod udp;
diff --git a/src/servers/registar.rs b/src/servers/registar.rs
new file mode 100644
index 000000000..0fb8d6acc
--- /dev/null
+++ b/src/servers/registar.rs
@@ -0,0 +1,95 @@
+//! Registar. Registers Services for Health Check.
+
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use derive_more::Constructor;
+use tokio::sync::Mutex;
+use tokio::task::JoinHandle;
+
+/// A [`ServiceHeathCheckResult`] is returned by a completed health check.
+pub type ServiceHeathCheckResult = Result<String, String>;
+
+/// The [`ServiceHealthCheckJob`] has a health check job with it's metadata
+///
+/// The `job` awaits a [`ServiceHeathCheckResult`].
+#[derive(Debug, Constructor)]
+pub struct ServiceHealthCheckJob {
+    pub binding: SocketAddr,
+    pub info: String,
+    pub job: JoinHandle<ServiceHeathCheckResult>,
+}
+
+/// The function specification [`FnSpawnServiceHeathCheck`].
+///
+/// A function fulfilling this specification will spawn a new [`ServiceHealthCheckJob`].
+pub type FnSpawnServiceHeathCheck = fn(&SocketAddr) -> ServiceHealthCheckJob;
+
+/// A [`ServiceRegistration`] is provided to the [`Registar`] for registration.
+///
+/// Each registration includes a function that fulfils the [`FnSpawnServiceHeathCheck`] specification.
+#[derive(Clone, Debug, Constructor)]
+pub struct ServiceRegistration {
+    binding: SocketAddr,
+    check_fn: FnSpawnServiceHeathCheck,
+}
+
+impl ServiceRegistration {
+    #[must_use]
+    pub fn spawn_check(&self) -> ServiceHealthCheckJob {
+        (self.check_fn)(&self.binding)
+    }
+}
+
+/// A [`ServiceRegistrationForm`] will return a completed [`ServiceRegistration`] to the [`Registar`].
+pub type ServiceRegistrationForm = tokio::sync::oneshot::Sender<ServiceRegistration>;
+
+/// The [`ServiceRegistry`] contains each unique [`ServiceRegistration`] by it's [`SocketAddr`].
+pub type ServiceRegistry = Arc<Mutex<HashMap<SocketAddr, ServiceRegistration>>>;
+
+/// The [`Registar`] manages the [`ServiceRegistry`].
+#[derive(Clone, Debug)]
+pub struct Registar {
+    registry: ServiceRegistry,
+}
+
+#[allow(clippy::derivable_impls)]
+impl Default for Registar {
+    fn default() -> Self {
+        Self {
+            registry: ServiceRegistry::default(),
+        }
+    }
+}
+
+impl Registar {
+    pub fn new(register: ServiceRegistry) -> Self {
+        Self { registry: register }
+    }
+
+    /// Registers a Service
+    #[must_use]
+    pub fn give_form(&self) -> ServiceRegistrationForm {
+        let (tx, rx) = tokio::sync::oneshot::channel::<ServiceRegistration>();
+        let register = self.clone();
+        tokio::spawn(async move {
+            register.insert(rx).await;
+        });
+        tx
+    }
+
+    /// Inserts a listing into the registry.
+    async fn insert(&self, rx: tokio::sync::oneshot::Receiver<ServiceRegistration>) {
+        let listing = rx.await.expect("it should receive the listing");
+
+        let mut mutex = self.registry.lock().await;
+        mutex.insert(listing.binding, listing);
+    }
+
+    /// Returns the [`ServiceRegistry`] of services
+    #[must_use]
+    pub fn entries(&self) -> ServiceRegistry {
+        self.registry.clone()
+    }
+}
diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs
index 001603b08..5a1977d01 100644
--- a/src/servers/udp/server.rs
+++ b/src/servers/udp/server.rs
@@ -32,8 +32,10 @@ use tokio::task::JoinHandle;
 
 use crate::bootstrap::jobs::Started;
 use crate::core::Tracker;
+use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm};
 use crate::servers::signals::{shutdown_signal_with_message, Halted};
 use crate::servers::udp::handlers::handle_packet;
+use crate::shared::bit_torrent::tracker::udp::client::check;
 use crate::shared::bit_torrent::tracker::udp::MAX_PACKET_SIZE;
 
 /// Error that can occur when starting or stopping the UDP server.
@@ -117,7 +119,7 @@ impl UdpServer<Stopped> {
     ///
     /// It panics if unable to receive the bound socket address from service.
     ///
-    pub async fn start(self, tracker: Arc<Tracker>) -> Result<UdpServer<Running>, Error> {
+    pub async fn start(self, tracker: Arc<Tracker>, form: ServiceRegistrationForm) -> Result<UdpServer<Running>, Error> {
         let (tx_start, rx_start) = tokio::sync::oneshot::channel::<Started>();
         let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
 
@@ -135,7 +137,10 @@ impl UdpServer<Stopped> {
             launcher
         });
 
-        let binding = rx_start.await.expect("unable to start service").address;
+        let binding = rx_start.await.expect("it should be able to start the service").address;
+
+        form.send(ServiceRegistration::new(binding, Udp::check))
+            .expect("it should be able to send service registration");
 
         let running_udp_server: UdpServer<Running> = UdpServer {
             state: Running {
@@ -305,6 +310,15 @@ impl Udp {
         // doesn't matter if it reaches or not
         drop(socket.send_to(payload, remote_addr).await);
     }
+
+    fn check(binding: &SocketAddr) -> ServiceHealthCheckJob {
+        let binding = *binding;
+        let info = format!("checking the udp tracker health check at: {binding}");
+
+        let job = tokio::spawn(async move { check(&binding).await });
+
+        ServiceHealthCheckJob::new(binding, info, job)
+    }
 }
 
 #[cfg(test)]
@@ -314,6 +328,7 @@ mod tests {
     use torrust_tracker_test_helpers::configuration::ephemeral_mode_public;
 
     use crate::bootstrap::app::initialize_with_configuration;
+    use crate::servers::registar::Registar;
     use crate::servers::udp::server::{Launcher, UdpServer};
 
     #[tokio::test]
@@ -327,8 +342,13 @@ mod tests {
             .parse::<std::net::SocketAddr>()
             .expect("Tracker API bind_address invalid.");
 
+        let register = &Registar::default();
+
         let stopped = UdpServer::new(Launcher::new(bind_to));
-        let started = stopped.start(tracker).await.expect("it should start the server");
+        let started = stopped
+            .start(tracker, register.give_form())
+            .await
+            .expect("it should start the server");
         let stopped = started.stop().await.expect("it should stop the server");
 
         assert_eq!(stopped.state.launcher.bind_to, bind_to);
diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs
index 5ea982663..f0a981c8a 100644
--- a/src/shared/bit_torrent/tracker/udp/client.rs
+++ b/src/shared/bit_torrent/tracker/udp/client.rs
@@ -1,7 +1,8 @@
 use std::io::Cursor;
+use std::net::SocketAddr;
 use std::sync::Arc;
 
-use aquatic_udp_protocol::{Request, Response};
+use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId};
 use tokio::net::UdpSocket;
 
 use crate::shared::bit_torrent::tracker::udp::{source_address, MAX_PACKET_SIZE};
@@ -105,3 +106,26 @@ pub async fn new_udp_tracker_client_connected(remote_address: &str) -> UdpTracke
     let udp_client = new_udp_client_connected(remote_address).await;
     UdpTrackerClient { udp_client }
 }
+
+/// Helper Function to Check if a UDP Service is Connectable
+///
+/// # Errors
+///
+/// It will return an error if unable to connect to the UDP service.
+pub async fn check(binding: &SocketAddr) -> Result<String, String> {
+    let client = new_udp_tracker_client_connected(binding.to_string().as_str()).await;
+
+    let connect_request = ConnectRequest {
+        transaction_id: TransactionId(123),
+    };
+
+    client.send(connect_request.into()).await;
+
+    let response = client.receive().await;
+
+    if matches!(response, Response::Connect(_connect_response)) {
+        Ok("Connected".to_string())
+    } else {
+        Err("Did not Connect".to_string())
+    }
+}
diff --git a/tests/servers/api/test_environment.rs b/tests/servers/api/test_environment.rs
index c6878c674..080fab551 100644
--- a/tests/servers/api/test_environment.rs
+++ b/tests/servers/api/test_environment.rs
@@ -5,6 +5,7 @@ use torrust_tracker::bootstrap::jobs::make_rust_tls;
 use torrust_tracker::core::peer::Peer;
 use torrust_tracker::core::Tracker;
 use torrust_tracker::servers::apis::server::{ApiServer, Launcher, RunningApiServer, StoppedApiServer};
+use torrust_tracker::servers::registar::Registar;
 use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
 use torrust_tracker_configuration::HttpApi;
 
@@ -69,7 +70,12 @@ impl TestEnvironment<Stopped> {
             config: self.config,
             tracker: self.tracker.clone(),
             state: Running {
-                api_server: self.state.api_server.start(self.tracker, access_tokens).await.unwrap(),
+                api_server: self
+                    .state
+                    .api_server
+                    .start(self.tracker, Registar::default().give_form(), access_tokens)
+                    .await
+                    .unwrap(),
             },
         }
     }
diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs
index 6b816b85f..c02335d05 100644
--- a/tests/servers/health_check_api/contract.rs
+++ b/tests/servers/health_check_api/contract.rs
@@ -1,4 +1,5 @@
-use torrust_tracker::servers::health_check_api::resources::Report;
+use torrust_tracker::servers::health_check_api::resources::{Report, Status};
+use torrust_tracker::servers::registar::Registar;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::health_check_api::client::get;
@@ -8,7 +9,9 @@ use crate::servers::health_check_api::test_environment;
 async fn health_check_endpoint_should_return_status_ok_when_no_service_is_running() {
     let configuration = configuration::ephemeral_with_no_services();
 
-    let (bound_addr, test_env) = test_environment::start(configuration.into()).await;
+    let registar = &Registar::default();
+
+    let (bound_addr, test_env) = test_environment::start(&configuration.health_check_api, registar.entries()).await;
 
     let url = format!("http://{bound_addr}/health_check");
 
@@ -16,7 +19,7 @@ async fn health_check_endpoint_should_return_status_ok_when_no_service_is_runnin
 
     assert_eq!(response.status(), 200);
     assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
-    assert_eq!(response.json::<Report>().await.unwrap(), Report::ok());
+    assert_eq!(response.json::<Report>().await.unwrap().status, Status::Ok);
 
     test_env.abort();
 }
diff --git a/tests/servers/health_check_api/test_environment.rs b/tests/servers/health_check_api/test_environment.rs
index 554e37dbf..18924e101 100644
--- a/tests/servers/health_check_api/test_environment.rs
+++ b/tests/servers/health_check_api/test_environment.rs
@@ -1,17 +1,16 @@
 use std::net::SocketAddr;
-use std::sync::Arc;
 
 use tokio::sync::oneshot;
 use tokio::task::JoinHandle;
 use torrust_tracker::bootstrap::jobs::Started;
 use torrust_tracker::servers::health_check_api::server;
-use torrust_tracker_configuration::Configuration;
+use torrust_tracker::servers::registar::ServiceRegistry;
+use torrust_tracker_configuration::HealthCheckApi;
 
 /// Start the test environment for the Health Check API.
 /// It runs the API server.
-pub async fn start(config: Arc<Configuration>) -> (SocketAddr, JoinHandle<()>) {
+pub async fn start(config: &HealthCheckApi, register: ServiceRegistry) -> (SocketAddr, JoinHandle<()>) {
     let bind_addr = config
-        .health_check_api
         .bind_address
         .parse::<std::net::SocketAddr>()
         .expect("Health Check API bind_address invalid.");
@@ -19,7 +18,7 @@ pub async fn start(config: Arc<Configuration>) -> (SocketAddr, JoinHandle<()>) {
     let (tx, rx) = oneshot::channel::<Started>();
 
     let join_handle = tokio::spawn(async move {
-        let handle = server::start(bind_addr, tx, config.clone());
+        let handle = server::start(bind_addr, tx, register);
         if let Ok(()) = handle.await {
             panic!("Health Check API server on http://{bind_addr} stopped");
         }
diff --git a/tests/servers/http/test_environment.rs b/tests/servers/http/test_environment.rs
index 73961b790..9cab40db2 100644
--- a/tests/servers/http/test_environment.rs
+++ b/tests/servers/http/test_environment.rs
@@ -5,6 +5,7 @@ use torrust_tracker::bootstrap::jobs::make_rust_tls;
 use torrust_tracker::core::peer::Peer;
 use torrust_tracker::core::Tracker;
 use torrust_tracker::servers::http::server::{HttpServer, Launcher, RunningHttpServer, StoppedHttpServer};
+use torrust_tracker::servers::registar::Registar;
 use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
 
 use crate::common::app::setup_with_configuration;
@@ -68,7 +69,12 @@ impl TestEnvironment<Stopped> {
             cfg: self.cfg,
             tracker: self.tracker.clone(),
             state: Running {
-                http_server: self.state.http_server.start(self.tracker).await.unwrap(),
+                http_server: self
+                    .state
+                    .http_server
+                    .start(self.tracker, Registar::default().give_form())
+                    .await
+                    .unwrap(),
             },
         }
     }
diff --git a/tests/servers/udp/test_environment.rs b/tests/servers/udp/test_environment.rs
index bbad6d927..f272b6dd3 100644
--- a/tests/servers/udp/test_environment.rs
+++ b/tests/servers/udp/test_environment.rs
@@ -3,6 +3,7 @@ use std::sync::Arc;
 
 use torrust_tracker::core::peer::Peer;
 use torrust_tracker::core::Tracker;
+use torrust_tracker::servers::registar::Registar;
 use torrust_tracker::servers::udp::server::{Launcher, RunningUdpServer, StoppedUdpServer, UdpServer};
 use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
 
@@ -61,11 +62,13 @@ impl TestEnvironment<Stopped> {
 
     #[allow(dead_code)]
     pub async fn start(self) -> TestEnvironment<Running> {
+        let register = &Registar::default();
+
         TestEnvironment {
             cfg: self.cfg,
             tracker: self.tracker.clone(),
             state: Running {
-                udp_server: self.state.udp_server.start(self.tracker).await.unwrap(),
+                udp_server: self.state.udp_server.start(self.tracker, register.give_form()).await.unwrap(),
             },
         }
     }

From 3f0dcea464bb4fd7cf7424a02dcc9f9329295caf Mon Sep 17 00:00:00 2001
From: Cameron Garnham <me@da2ce7.com>
Date: Sun, 7 Jan 2024 02:55:24 +1100
Subject: [PATCH 3/3] dev: add tests to health check

---
 src/bootstrap/jobs/health_check_api.rs        |   5 +-
 src/servers/health_check_api/handlers.rs      |   5 +
 src/servers/health_check_api/resources.rs     |  10 +
 src/servers/health_check_api/responses.rs     |   4 +
 src/servers/health_check_api/server.rs        |  28 +-
 src/shared/bit_torrent/tracker/udp/client.rs  |  26 +-
 tests/common/app.rs                           |   8 -
 tests/common/mod.rs                           |   1 -
 tests/servers/api/environment.rs              |  94 ++++
 tests/servers/api/mod.rs                      |   5 +-
 tests/servers/api/test_environment.rs         | 126 -----
 .../servers/api/v1/contract/authentication.rs |  38 +-
 .../servers/api/v1/contract/configuration.rs  |   6 +-
 .../api/v1/contract/context/auth_key.rs       | 124 ++--
 .../api/v1/contract/context/health_check.rs   |   8 +-
 .../servers/api/v1/contract/context/stats.rs  |  33 +-
 .../api/v1/contract/context/torrent.rs        |  98 ++--
 .../api/v1/contract/context/whitelist.rs      | 124 ++--
 tests/servers/health_check_api/contract.rs    | 308 +++++++++-
 tests/servers/health_check_api/environment.rs |  91 +++
 tests/servers/health_check_api/mod.rs         |   4 +-
 .../health_check_api/test_environment.rs      |  33 --
 tests/servers/http/environment.rs             |  81 +++
 tests/servers/http/mod.rs                     |   5 +-
 tests/servers/http/test_environment.rs        | 133 -----
 tests/servers/http/v1/contract.rs             | 529 +++++++++---------
 tests/servers/udp/contract.rs                 |  24 +-
 tests/servers/udp/environment.rs              |  78 +++
 tests/servers/udp/mod.rs                      |   6 +-
 tests/servers/udp/test_environment.rs         | 110 ----
 30 files changed, 1182 insertions(+), 963 deletions(-)
 delete mode 100644 tests/common/app.rs
 create mode 100644 tests/servers/api/environment.rs
 delete mode 100644 tests/servers/api/test_environment.rs
 create mode 100644 tests/servers/health_check_api/environment.rs
 delete mode 100644 tests/servers/health_check_api/test_environment.rs
 create mode 100644 tests/servers/http/environment.rs
 delete mode 100644 tests/servers/http/test_environment.rs
 create mode 100644 tests/servers/udp/environment.rs
 delete mode 100644 tests/servers/udp/test_environment.rs

diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs
index 1a9815280..7eeafe97b 100644
--- a/src/bootstrap/jobs/health_check_api.rs
+++ b/src/bootstrap/jobs/health_check_api.rs
@@ -22,6 +22,7 @@ use torrust_tracker_configuration::HealthCheckApi;
 use super::Started;
 use crate::servers::health_check_api::server;
 use crate::servers::registar::ServiceRegistry;
+use crate::servers::signals::Halted;
 
 /// This function starts a new Health Check API server with the provided
 /// configuration.
@@ -40,12 +41,14 @@ pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> Jo
         .expect("it should have a valid health check bind address");
 
     let (tx_start, rx_start) = oneshot::channel::<Started>();
+    let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::<Halted>();
+    drop(tx_halt);
 
     // Run the API server
     let join_handle = tokio::spawn(async move {
         info!(target: "Health Check API", "Starting on: http://{}", bind_addr);
 
-        let handle = server::start(bind_addr, tx_start, register);
+        let handle = server::start(bind_addr, tx_start, rx_halt, register);
 
         if let Ok(()) = handle.await {
             info!(target: "Health Check API", "Stopped server running on: http://{}", bind_addr);
diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs
index 35382583e..944e84a1d 100644
--- a/src/servers/health_check_api/handlers.rs
+++ b/src/servers/health_check_api/handlers.rs
@@ -21,6 +21,11 @@ pub(crate) async fn health_check_handler(State(register): State<ServiceRegistry>
         checks = mutex.await.values().map(ServiceRegistration::spawn_check).collect();
     }
 
+    // if we do not have any checks, lets return a `none` result.
+    if checks.is_empty() {
+        return responses::none();
+    }
+
     let jobs = checks.drain(..).map(|c| {
         tokio::spawn(async move {
             CheckReport {
diff --git a/src/servers/health_check_api/resources.rs b/src/servers/health_check_api/resources.rs
index bb57cf20b..3302fb966 100644
--- a/src/servers/health_check_api/resources.rs
+++ b/src/servers/health_check_api/resources.rs
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
 pub enum Status {
     Ok,
     Error,
+    None,
 }
 
 #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
@@ -34,6 +35,15 @@ pub struct Report {
 }
 
 impl Report {
+    #[must_use]
+    pub fn none() -> Report {
+        Self {
+            status: Status::None,
+            message: String::new(),
+            details: Vec::default(),
+        }
+    }
+
     #[must_use]
     pub fn ok(details: Vec<CheckReport>) -> Report {
         Self {
diff --git a/src/servers/health_check_api/responses.rs b/src/servers/health_check_api/responses.rs
index 8658caeb4..3796d8be4 100644
--- a/src/servers/health_check_api/responses.rs
+++ b/src/servers/health_check_api/responses.rs
@@ -9,3 +9,7 @@ pub fn ok(details: Vec<CheckReport>) -> Json<Report> {
 pub fn error(message: String, details: Vec<CheckReport>) -> Json<Report> {
     Json(Report::error(message, details))
 }
+
+pub fn none() -> Json<Report> {
+    Json(Report::none())
+}
diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs
index a7cbf4a8a..ecc6fe427 100644
--- a/src/servers/health_check_api/server.rs
+++ b/src/servers/health_check_api/server.rs
@@ -8,13 +8,13 @@ use axum::routing::get;
 use axum::{Json, Router};
 use axum_server::Handle;
 use futures::Future;
-use log::info;
 use serde_json::json;
-use tokio::sync::oneshot::Sender;
+use tokio::sync::oneshot::{Receiver, Sender};
 
 use crate::bootstrap::jobs::Started;
 use crate::servers::health_check_api::handlers::health_check_handler;
 use crate::servers::registar::ServiceRegistry;
+use crate::servers::signals::{graceful_shutdown, Halted};
 
 /// Starts Health Check API server.
 ///
@@ -22,30 +22,30 @@ use crate::servers::registar::ServiceRegistry;
 ///
 /// Will panic if binding to the socket address fails.
 pub fn start(
-    address: SocketAddr,
+    bind_to: SocketAddr,
     tx: Sender<Started>,
+    rx_halt: Receiver<Halted>,
     register: ServiceRegistry,
 ) -> impl Future<Output = Result<(), std::io::Error>> {
-    let app = Router::new()
+    let router = Router::new()
         .route("/", get(|| async { Json(json!({})) }))
         .route("/health_check", get(health_check_handler))
         .with_state(register);
 
-    let handle = Handle::new();
-    let cloned_handle = handle.clone();
-
-    let socket = std::net::TcpListener::bind(address).expect("Could not bind tcp_listener to address.");
+    let socket = std::net::TcpListener::bind(bind_to).expect("Could not bind tcp_listener to address.");
     let address = socket.local_addr().expect("Could not get local_addr from tcp_listener.");
 
-    tokio::task::spawn(async move {
-        tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal.");
-        info!("Stopping Torrust Health Check API server o http://{} ...", address);
-        cloned_handle.shutdown();
-    });
+    let handle = Handle::new();
+
+    tokio::task::spawn(graceful_shutdown(
+        handle.clone(),
+        rx_halt,
+        format!("shutting down http server on socket address: {address}"),
+    ));
 
     let running = axum_server::from_tcp(socket)
         .handle(handle)
-        .serve(app.into_make_service_with_connect_info::<SocketAddr>());
+        .serve(router.into_make_service_with_connect_info::<SocketAddr>());
 
     tx.send(Started { address })
         .expect("the Health Check API server should not be dropped");
diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs
index f0a981c8a..00f0b8acf 100644
--- a/src/shared/bit_torrent/tracker/udp/client.rs
+++ b/src/shared/bit_torrent/tracker/udp/client.rs
@@ -1,9 +1,11 @@
 use std::io::Cursor;
 use std::net::SocketAddr;
 use std::sync::Arc;
+use std::time::Duration;
 
 use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId};
 use tokio::net::UdpSocket;
+use tokio::time;
 
 use crate::shared::bit_torrent::tracker::udp::{source_address, MAX_PACKET_SIZE};
 
@@ -112,6 +114,8 @@ pub async fn new_udp_tracker_client_connected(remote_address: &str) -> UdpTracke
 /// # Errors
 ///
 /// It will return an error if unable to connect to the UDP service.
+///
+/// # Panics
 pub async fn check(binding: &SocketAddr) -> Result<String, String> {
     let client = new_udp_tracker_client_connected(binding.to_string().as_str()).await;
 
@@ -121,11 +125,23 @@ pub async fn check(binding: &SocketAddr) -> Result<String, String> {
 
     client.send(connect_request.into()).await;
 
-    let response = client.receive().await;
+    let process = move |response| {
+        if matches!(response, Response::Connect(_connect_response)) {
+            Ok("Connected".to_string())
+        } else {
+            Err("Did not Connect".to_string())
+        }
+    };
+
+    let sleep = time::sleep(Duration::from_millis(2000));
+    tokio::pin!(sleep);
 
-    if matches!(response, Response::Connect(_connect_response)) {
-        Ok("Connected".to_string())
-    } else {
-        Err("Did not Connect".to_string())
+    tokio::select! {
+        () = &mut sleep => {
+              Err("Timed Out".to_string())
+        }
+        response = client.receive() => {
+              process(response)
+        }
     }
 }
diff --git a/tests/common/app.rs b/tests/common/app.rs
deleted file mode 100644
index 1b735bc86..000000000
--- a/tests/common/app.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-use std::sync::Arc;
-
-use torrust_tracker::bootstrap;
-use torrust_tracker::core::Tracker;
-
-pub fn setup_with_configuration(configuration: &Arc<torrust_tracker_configuration::Configuration>) -> Arc<Tracker> {
-    bootstrap::app::initialize_with_configuration(configuration)
-}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 51a8a5b03..b57996292 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -1,4 +1,3 @@
-pub mod app;
 pub mod fixtures;
 pub mod http;
 pub mod udp;
diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs
new file mode 100644
index 000000000..186b7ea3b
--- /dev/null
+++ b/tests/servers/api/environment.rs
@@ -0,0 +1,94 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use futures::executor::block_on;
+use torrust_tracker::bootstrap::app::initialize_with_configuration;
+use torrust_tracker::bootstrap::jobs::make_rust_tls;
+use torrust_tracker::core::peer::Peer;
+use torrust_tracker::core::Tracker;
+use torrust_tracker::servers::apis::server::{ApiServer, Launcher, Running, Stopped};
+use torrust_tracker::servers::registar::Registar;
+use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
+use torrust_tracker_configuration::{Configuration, HttpApi};
+
+use super::connection_info::ConnectionInfo;
+
+pub struct Environment<S> {
+    pub config: Arc<HttpApi>,
+    pub tracker: Arc<Tracker>,
+    pub registar: Registar,
+    pub server: ApiServer<S>,
+}
+
+impl<S> Environment<S> {
+    /// Add a torrent to the tracker
+    pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) {
+        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
+    }
+}
+
+impl Environment<Stopped> {
+    pub fn new(configuration: &Arc<Configuration>) -> Self {
+        let tracker = initialize_with_configuration(configuration);
+
+        let config = Arc::new(configuration.http_api.clone());
+
+        let bind_to = config
+            .bind_address
+            .parse::<std::net::SocketAddr>()
+            .expect("Tracker API bind_address invalid.");
+
+        let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path))
+            .map(|tls| tls.expect("tls config failed"));
+
+        let server = ApiServer::new(Launcher::new(bind_to, tls));
+
+        Self {
+            config,
+            tracker,
+            registar: Registar::default(),
+            server,
+        }
+    }
+
+    pub async fn start(self) -> Environment<Running> {
+        let access_tokens = Arc::new(self.config.access_tokens.clone());
+
+        Environment {
+            config: self.config,
+            tracker: self.tracker.clone(),
+            registar: self.registar.clone(),
+            server: self
+                .server
+                .start(self.tracker, self.registar.give_form(), access_tokens)
+                .await
+                .unwrap(),
+        }
+    }
+}
+
+impl Environment<Running> {
+    pub async fn new(configuration: &Arc<Configuration>) -> Self {
+        Environment::<Stopped>::new(configuration).start().await
+    }
+
+    pub async fn stop(self) -> Environment<Stopped> {
+        Environment {
+            config: self.config,
+            tracker: self.tracker,
+            registar: Registar::default(),
+            server: self.server.stop().await.unwrap(),
+        }
+    }
+
+    pub fn get_connection_info(&self) -> ConnectionInfo {
+        ConnectionInfo {
+            bind_address: self.server.state.binding.to_string(),
+            api_token: self.config.access_tokens.get("admin").cloned(),
+        }
+    }
+
+    pub fn bind_address(&self) -> SocketAddr {
+        self.server.state.binding
+    }
+}
diff --git a/tests/servers/api/mod.rs b/tests/servers/api/mod.rs
index 155ac0de1..9c30e316a 100644
--- a/tests/servers/api/mod.rs
+++ b/tests/servers/api/mod.rs
@@ -1,11 +1,14 @@
 use std::sync::Arc;
 
 use torrust_tracker::core::Tracker;
+use torrust_tracker::servers::apis::server;
 
 pub mod connection_info;
-pub mod test_environment;
+pub mod environment;
 pub mod v1;
 
+pub type Started = environment::Environment<server::Running>;
+
 /// It forces a database error by dropping all tables.
 /// That makes any query fail.
 /// code-review: alternatively we could inject a database mock in the future.
diff --git a/tests/servers/api/test_environment.rs b/tests/servers/api/test_environment.rs
deleted file mode 100644
index 080fab551..000000000
--- a/tests/servers/api/test_environment.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use std::sync::Arc;
-
-use futures::executor::block_on;
-use torrust_tracker::bootstrap::jobs::make_rust_tls;
-use torrust_tracker::core::peer::Peer;
-use torrust_tracker::core::Tracker;
-use torrust_tracker::servers::apis::server::{ApiServer, Launcher, RunningApiServer, StoppedApiServer};
-use torrust_tracker::servers::registar::Registar;
-use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
-use torrust_tracker_configuration::HttpApi;
-
-use super::connection_info::ConnectionInfo;
-use crate::common::app::setup_with_configuration;
-
-#[allow(clippy::module_name_repetitions, dead_code)]
-pub type StoppedTestEnvironment = TestEnvironment<Stopped>;
-#[allow(clippy::module_name_repetitions)]
-pub type RunningTestEnvironment = TestEnvironment<Running>;
-
-pub struct TestEnvironment<S> {
-    pub config: Arc<HttpApi>,
-    pub tracker: Arc<Tracker>,
-    pub state: S,
-}
-
-#[allow(dead_code)]
-pub struct Stopped {
-    api_server: StoppedApiServer,
-}
-
-pub struct Running {
-    api_server: RunningApiServer,
-}
-
-impl<S> TestEnvironment<S> {
-    /// Add a torrent to the tracker
-    pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) {
-        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
-    }
-}
-
-impl TestEnvironment<Stopped> {
-    pub fn new(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let cfg = Arc::new(cfg);
-        let tracker = setup_with_configuration(&cfg);
-
-        let config = Arc::new(cfg.http_api.clone());
-
-        let bind_to = config
-            .bind_address
-            .parse::<std::net::SocketAddr>()
-            .expect("Tracker API bind_address invalid.");
-
-        let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path))
-            .map(|tls| tls.expect("tls config failed"));
-
-        let api_server = api_server(Launcher::new(bind_to, tls));
-
-        Self {
-            config,
-            tracker,
-            state: Stopped { api_server },
-        }
-    }
-
-    pub async fn start(self) -> TestEnvironment<Running> {
-        let access_tokens = Arc::new(self.config.access_tokens.clone());
-
-        TestEnvironment {
-            config: self.config,
-            tracker: self.tracker.clone(),
-            state: Running {
-                api_server: self
-                    .state
-                    .api_server
-                    .start(self.tracker, Registar::default().give_form(), access_tokens)
-                    .await
-                    .unwrap(),
-            },
-        }
-    }
-
-    // pub fn config_mut(&mut self) -> &mut torrust_tracker_configuration::HttpApi {
-    //     &mut self.cfg.http_api
-    // }
-}
-
-impl TestEnvironment<Running> {
-    pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let test_env = StoppedTestEnvironment::new(cfg);
-
-        test_env.start().await
-    }
-
-    pub async fn stop(self) -> TestEnvironment<Stopped> {
-        TestEnvironment {
-            config: self.config,
-            tracker: self.tracker,
-            state: Stopped {
-                api_server: self.state.api_server.stop().await.unwrap(),
-            },
-        }
-    }
-
-    pub fn get_connection_info(&self) -> ConnectionInfo {
-        ConnectionInfo {
-            bind_address: self.state.api_server.state.binding.to_string(),
-            api_token: self.config.access_tokens.get("admin").cloned(),
-        }
-    }
-}
-
-#[allow(clippy::module_name_repetitions)]
-#[allow(dead_code)]
-pub fn stopped_test_environment(cfg: torrust_tracker_configuration::Configuration) -> StoppedTestEnvironment {
-    TestEnvironment::new(cfg)
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub async fn running_test_environment(cfg: torrust_tracker_configuration::Configuration) -> RunningTestEnvironment {
-    TestEnvironment::new_running(cfg).await
-}
-
-pub fn api_server(launcher: Launcher) -> StoppedApiServer {
-    ApiServer::new(launcher)
-}
diff --git a/tests/servers/api/v1/contract/authentication.rs b/tests/servers/api/v1/contract/authentication.rs
index fb8de1810..49981dd02 100644
--- a/tests/servers/api/v1/contract/authentication.rs
+++ b/tests/servers/api/v1/contract/authentication.rs
@@ -1,83 +1,83 @@
 use torrust_tracker_test_helpers::configuration;
 
 use crate::common::http::{Query, QueryParam};
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::asserts::{assert_token_not_valid, assert_unauthorized};
 use crate::servers::api::v1::client::Client;
+use crate::servers::api::Started;
 
 #[tokio::test]
 async fn should_authenticate_requests_by_using_a_token_query_param() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let token = test_env.get_connection_info().api_token.unwrap();
+    let token = env.get_connection_info().api_token.unwrap();
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()))
         .await;
 
     assert_eq!(response.status(), 200);
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_authenticate_requests_when_the_token_is_missing() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request_with_query("stats", Query::default())
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_authenticate_requests_when_the_token_is_empty() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request_with_query("stats", Query::params([QueryParam::new("token", "")].to_vec()))
         .await;
 
     assert_token_not_valid(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_authenticate_requests_when_the_token_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request_with_query("stats", Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()))
         .await;
 
     assert_token_not_valid(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_the_token_query_param_to_be_at_any_position_in_the_url_query() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let token = test_env.get_connection_info().api_token.unwrap();
+    let token = env.get_connection_info().api_token.unwrap();
 
     // At the beginning of the query component
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request(&format!("torrents?token={token}&limit=1"))
         .await;
 
     assert_eq!(response.status(), 200);
 
     // At the end of the query component
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_request(&format!("torrents?limit=1&token={token}"))
         .await;
 
     assert_eq!(response.status(), 200);
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/api/v1/contract/configuration.rs b/tests/servers/api/v1/contract/configuration.rs
index a551a8b36..4220f62d2 100644
--- a/tests/servers/api/v1/contract/configuration.rs
+++ b/tests/servers/api/v1/contract/configuration.rs
@@ -5,7 +5,7 @@
 // use torrust_tracker_test_helpers::configuration;
 
 // use crate::common::app::setup_with_configuration;
-// use crate::servers::api::test_environment::stopped_test_environment;
+// use crate::servers::api::environment::stopped_environment;
 
 #[tokio::test]
 #[ignore]
@@ -27,7 +27,7 @@ async fn should_fail_with_ssl_enabled_and_bad_ssl_config() {
     //         None
     //     };
 
-    // let test_env = new_stopped(tracker, bind_to, tls);
+    // let env = new_stopped(tracker, bind_to, tls);
 
-    // test_env.start().await;
+    // env.start().await;
 }
diff --git a/tests/servers/api/v1/contract/context/auth_key.rs b/tests/servers/api/v1/contract/context/auth_key.rs
index 4c59b4e95..f9630bafe 100644
--- a/tests/servers/api/v1/contract/context/auth_key.rs
+++ b/tests/servers/api/v1/contract/context/auth_key.rs
@@ -4,62 +4,57 @@ use torrust_tracker::core::auth::Key;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
-use crate::servers::api::force_database_error;
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::asserts::{
     assert_auth_key_utf8, assert_failed_to_delete_key, assert_failed_to_generate_key, assert_failed_to_reload_keys,
     assert_invalid_auth_key_param, assert_invalid_key_duration_param, assert_ok, assert_token_not_valid, assert_unauthorized,
 };
 use crate::servers::api::v1::client::Client;
+use crate::servers::api::{force_database_error, Started};
 
 #[tokio::test]
 async fn should_allow_generating_a_new_auth_key() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
 
-    let response = Client::new(test_env.get_connection_info())
-        .generate_auth_key(seconds_valid)
-        .await;
+    let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await;
 
     let auth_key_resource = assert_auth_key_utf8(response).await;
 
     // Verify the key with the tracker
-    assert!(test_env
+    assert!(env
         .tracker
         .verify_auth_key(&auth_key_resource.key.parse::<Key>().unwrap())
         .await
         .is_ok());
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .generate_auth_key(seconds_valid)
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .generate_auth_key(seconds_valid)
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .generate_auth_key(seconds_valid)
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let invalid_key_durations = [
         // "", it returns 404
@@ -68,55 +63,53 @@ async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid(
     ];
 
     for invalid_key_duration in invalid_key_durations {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .post(&format!("key/{invalid_key_duration}"))
             .await;
 
         assert_invalid_key_duration_param(response, invalid_key_duration).await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_the_auth_key_cannot_be_generated() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
     let seconds_valid = 60;
-    let response = Client::new(test_env.get_connection_info())
-        .generate_auth_key(seconds_valid)
-        .await;
+    let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await;
 
     assert_failed_to_generate_key(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_deleting_an_auth_key() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
-    let auth_key = test_env
+    let auth_key = env
         .tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .delete_auth_key(&auth_key.key.to_string())
         .await;
 
     assert_ok(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let invalid_auth_keys = [
         // "", it returns a 404
@@ -129,137 +122,128 @@ async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() {
     ];
 
     for invalid_auth_key in &invalid_auth_keys {
-        let response = Client::new(test_env.get_connection_info())
-            .delete_auth_key(invalid_auth_key)
-            .await;
+        let response = Client::new(env.get_connection_info()).delete_auth_key(invalid_auth_key).await;
 
         assert_invalid_auth_key_param(response, invalid_auth_key).await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_the_auth_key_cannot_be_deleted() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
-    let auth_key = test_env
+    let auth_key = env
         .tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .delete_auth_key(&auth_key.key.to_string())
         .await;
 
     assert_failed_to_delete_key(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_deleting_an_auth_key_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
 
     // Generate new auth key
-    let auth_key = test_env
+    let auth_key = env
         .tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .delete_auth_key(&auth_key.key.to_string())
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .delete_auth_key(&auth_key.key.to_string())
+        .await;
 
     assert_token_not_valid(response).await;
 
     // Generate new auth key
-    let auth_key = test_env
+    let auth_key = env
         .tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .delete_auth_key(&auth_key.key.to_string())
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_reloading_keys() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
-    test_env
-        .tracker
+    env.tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    let response = Client::new(test_env.get_connection_info()).reload_keys().await;
+    let response = Client::new(env.get_connection_info()).reload_keys().await;
 
     assert_ok(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_keys_cannot_be_reloaded() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
-    test_env
-        .tracker
+    env.tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
-    let response = Client::new(test_env.get_connection_info()).reload_keys().await;
+    let response = Client::new(env.get_connection_info()).reload_keys().await;
 
     assert_failed_to_reload_keys(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_reloading_keys_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let seconds_valid = 60;
-    test_env
-        .tracker
+    env.tracker
         .generate_auth_key(Duration::from_secs(seconds_valid))
         .await
         .unwrap();
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .reload_keys()
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .reload_keys()
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .reload_keys()
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/api/v1/contract/context/health_check.rs b/tests/servers/api/v1/contract/context/health_check.rs
index 108ae237a..d8dc3c030 100644
--- a/tests/servers/api/v1/contract/context/health_check.rs
+++ b/tests/servers/api/v1/contract/context/health_check.rs
@@ -1,14 +1,14 @@
 use torrust_tracker::servers::apis::v1::context::health_check::resources::{Report, Status};
 use torrust_tracker_test_helpers::configuration;
 
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::client::get;
+use crate::servers::api::Started;
 
 #[tokio::test]
 async fn health_check_endpoint_should_return_status_ok_if_api_is_running() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let url = format!("http://{}/api/health_check", test_env.get_connection_info().bind_address);
+    let url = format!("http://{}/api/health_check", env.get_connection_info().bind_address);
 
     let response = get(&url, None).await;
 
@@ -16,5 +16,5 @@ async fn health_check_endpoint_should_return_status_ok_if_api_is_running() {
     assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
     assert_eq!(response.json::<Report>().await.unwrap(), Report { status: Status::Ok });
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/api/v1/contract/context/stats.rs b/tests/servers/api/v1/contract/context/stats.rs
index 71738f8e5..54263f8b8 100644
--- a/tests/servers/api/v1/contract/context/stats.rs
+++ b/tests/servers/api/v1/contract/context/stats.rs
@@ -6,22 +6,21 @@ use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::asserts::{assert_stats, assert_token_not_valid, assert_unauthorized};
 use crate::servers::api::v1::client::Client;
+use crate::servers::api::Started;
 
 #[tokio::test]
 async fn should_allow_getting_tracker_statistics() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    test_env
-        .add_torrent_peer(
-            &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(),
-            &PeerBuilder::default().into(),
-        )
-        .await;
+    env.add_torrent_peer(
+        &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(),
+        &PeerBuilder::default().into(),
+    )
+    .await;
 
-    let response = Client::new(test_env.get_connection_info()).get_tracker_statistics().await;
+    let response = Client::new(env.get_connection_info()).get_tracker_statistics().await;
 
     assert_stats(
         response,
@@ -46,26 +45,24 @@ async fn should_allow_getting_tracker_statistics() {
     )
     .await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .get_tracker_statistics()
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .get_tracker_statistics()
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .get_tracker_statistics()
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/api/v1/contract/context/torrent.rs b/tests/servers/api/v1/contract/context/torrent.rs
index dc91e8fc5..63b97b402 100644
--- a/tests/servers/api/v1/contract/context/torrent.rs
+++ b/tests/servers/api/v1/contract/context/torrent.rs
@@ -8,7 +8,6 @@ use torrust_tracker_test_helpers::configuration;
 
 use crate::common::http::{Query, QueryParam};
 use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::asserts::{
     assert_bad_request, assert_invalid_infohash_param, assert_not_found, assert_token_not_valid, assert_torrent_info,
     assert_torrent_list, assert_torrent_not_known, assert_unauthorized,
@@ -17,16 +16,17 @@ use crate::servers::api::v1::client::Client;
 use crate::servers::api::v1::contract::fixtures::{
     invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found,
 };
+use crate::servers::api::Started;
 
 #[tokio::test]
 async fn should_allow_getting_torrents() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
 
-    test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await;
 
-    let response = Client::new(test_env.get_connection_info()).get_torrents(Query::empty()).await;
+    let response = Client::new(env.get_connection_info()).get_torrents(Query::empty()).await;
 
     assert_torrent_list(
         response,
@@ -39,21 +39,21 @@ async fn should_allow_getting_torrents() {
     )
     .await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_limiting_the_torrents_in_the_result() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     // torrents are ordered alphabetically by infohashes
     let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
     let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap();
 
-    test_env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await;
-    test_env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec()))
         .await;
 
@@ -68,21 +68,21 @@ async fn should_allow_limiting_the_torrents_in_the_result() {
     )
     .await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_the_torrents_result_pagination() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     // torrents are ordered alphabetically by infohashes
     let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
     let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap();
 
-    test_env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await;
-    test_env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec()))
         .await;
 
@@ -97,75 +97,73 @@ async fn should_allow_the_torrents_result_pagination() {
     )
     .await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_getting_torrents_when_the_offset_query_parameter_cannot_be_parsed() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let invalid_offsets = [" ", "-1", "1.1", "INVALID OFFSET"];
 
     for invalid_offset in &invalid_offsets {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .get_torrents(Query::params([QueryParam::new("offset", invalid_offset)].to_vec()))
             .await;
 
         assert_bad_request(response, "Failed to deserialize query string: invalid digit found in string").await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_getting_torrents_when_the_limit_query_parameter_cannot_be_parsed() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let invalid_limits = [" ", "-1", "1.1", "INVALID LIMIT"];
 
     for invalid_limit in &invalid_limits {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .get_torrents(Query::params([QueryParam::new("limit", invalid_limit)].to_vec()))
             .await;
 
         assert_bad_request(response, "Failed to deserialize query string: invalid digit found in string").await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_getting_torrents_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .get_torrents(Query::empty())
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .get_torrents(Query::empty())
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .get_torrents(Query::default())
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_getting_a_torrent_info() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
 
     let peer = PeerBuilder::default().into();
 
-    test_env.add_torrent_peer(&info_hash, &peer).await;
+    env.add_torrent_peer(&info_hash, &peer).await;
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_torrent(&info_hash.to_string())
         .await;
 
@@ -181,68 +179,62 @@ async fn should_allow_getting_a_torrent_info() {
     )
     .await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_while_getting_a_torrent_info_when_the_torrent_does_not_exist() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .get_torrent(&info_hash.to_string())
         .await;
 
     assert_torrent_not_known(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_getting_a_torrent_info_when_the_provided_infohash_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     for invalid_infohash in &invalid_infohashes_returning_bad_request() {
-        let response = Client::new(test_env.get_connection_info())
-            .get_torrent(invalid_infohash)
-            .await;
+        let response = Client::new(env.get_connection_info()).get_torrent(invalid_infohash).await;
 
         assert_invalid_infohash_param(response, invalid_infohash).await;
     }
 
     for invalid_infohash in &invalid_infohashes_returning_not_found() {
-        let response = Client::new(test_env.get_connection_info())
-            .get_torrent(invalid_infohash)
-            .await;
+        let response = Client::new(env.get_connection_info()).get_torrent(invalid_infohash).await;
 
         assert_not_found(response).await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap();
 
-    test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await;
+    env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await;
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .get_torrent(&info_hash.to_string())
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .get_torrent(&info_hash.to_string())
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .get_torrent(&info_hash.to_string())
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/api/v1/contract/context/whitelist.rs b/tests/servers/api/v1/contract/context/whitelist.rs
index 60ab4c901..358a4a19e 100644
--- a/tests/servers/api/v1/contract/context/whitelist.rs
+++ b/tests/servers/api/v1/contract/context/whitelist.rs
@@ -4,8 +4,6 @@ use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
-use crate::servers::api::force_database_error;
-use crate::servers::api::test_environment::running_test_environment;
 use crate::servers::api::v1::asserts::{
     assert_failed_to_reload_whitelist, assert_failed_to_remove_torrent_from_whitelist, assert_failed_to_whitelist_torrent,
     assert_invalid_infohash_param, assert_not_found, assert_ok, assert_token_not_valid, assert_unauthorized,
@@ -14,35 +12,33 @@ use crate::servers::api::v1::client::Client;
 use crate::servers::api::v1::contract::fixtures::{
     invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found,
 };
+use crate::servers::api::{force_database_error, Started};
 
 #[tokio::test]
 async fn should_allow_whitelisting_a_torrent() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
 
-    let response = Client::new(test_env.get_connection_info())
-        .whitelist_a_torrent(&info_hash)
-        .await;
+    let response = Client::new(env.get_connection_info()).whitelist_a_torrent(&info_hash).await;
 
     assert_ok(response).await;
     assert!(
-        test_env
-            .tracker
+        env.tracker
             .is_info_hash_whitelisted(&InfoHash::from_str(&info_hash).unwrap())
             .await
     );
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
 
-    let api_client = Client::new(test_env.get_connection_info());
+    let api_client = Client::new(env.get_connection_info());
 
     let response = api_client.whitelist_a_torrent(&info_hash).await;
     assert_ok(response).await;
@@ -50,55 +46,51 @@ async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted()
     let response = api_client.whitelist_a_torrent(&info_hash).await;
     assert_ok(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_whitelisting_a_torrent_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
 
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .whitelist_a_torrent(&info_hash)
-    .await;
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .whitelist_a_torrent(&info_hash)
+        .await;
 
     assert_token_not_valid(response).await;
 
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .whitelist_a_torrent(&info_hash)
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_the_torrent_cannot_be_whitelisted() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
-    let response = Client::new(test_env.get_connection_info())
-        .whitelist_a_torrent(&info_hash)
-        .await;
+    let response = Client::new(env.get_connection_info()).whitelist_a_torrent(&info_hash).await;
 
     assert_failed_to_whitelist_torrent(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_whitelisting_a_torrent_when_the_provided_infohash_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     for invalid_infohash in &invalid_infohashes_returning_bad_request() {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .whitelist_a_torrent(invalid_infohash)
             .await;
 
@@ -106,55 +98,55 @@ async fn should_fail_whitelisting_a_torrent_when_the_provided_infohash_is_invali
     }
 
     for invalid_infohash in &invalid_infohashes_returning_not_found() {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .whitelist_a_torrent(invalid_infohash)
             .await;
 
         assert_not_found(response).await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_removing_a_torrent_from_the_whitelist() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
     let info_hash = InfoHash::from_str(&hash).unwrap();
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .remove_torrent_from_whitelist(&hash)
         .await;
 
     assert_ok(response).await;
-    assert!(!test_env.tracker.is_info_hash_whitelisted(&info_hash).await);
+    assert!(!env.tracker.is_info_hash_whitelisted(&info_hash).await);
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_fail_trying_to_remove_a_non_whitelisted_torrent_from_the_whitelist() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let non_whitelisted_torrent_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .remove_torrent_from_whitelist(&non_whitelisted_torrent_hash)
         .await;
 
     assert_ok(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_removing_a_torrent_from_the_whitelist_when_the_provided_infohash_is_invalid() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     for invalid_infohash in &invalid_infohashes_returning_bad_request() {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .remove_torrent_from_whitelist(invalid_infohash)
             .await;
 
@@ -162,99 +154,97 @@ async fn should_fail_removing_a_torrent_from_the_whitelist_when_the_provided_inf
     }
 
     for invalid_infohash in &invalid_infohashes_returning_not_found() {
-        let response = Client::new(test_env.get_connection_info())
+        let response = Client::new(env.get_connection_info())
             .remove_torrent_from_whitelist(invalid_infohash)
             .await;
 
         assert_not_found(response).await;
     }
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_the_torrent_cannot_be_removed_from_the_whitelist() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
     let info_hash = InfoHash::from_str(&hash).unwrap();
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
-    let response = Client::new(test_env.get_connection_info())
+    let response = Client::new(env.get_connection_info())
         .remove_torrent_from_whitelist(&hash)
         .await;
 
     assert_failed_to_remove_torrent_from_whitelist(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_not_allow_removing_a_torrent_from_the_whitelist_for_unauthenticated_users() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
     let info_hash = InfoHash::from_str(&hash).unwrap();
 
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
-    let response = Client::new(connection_with_invalid_token(
-        test_env.get_connection_info().bind_address.as_str(),
-    ))
-    .remove_torrent_from_whitelist(&hash)
-    .await;
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str()))
+        .remove_torrent_from_whitelist(&hash)
+        .await;
 
     assert_token_not_valid(response).await;
 
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
-    let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str()))
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str()))
         .remove_torrent_from_whitelist(&hash)
         .await;
 
     assert_unauthorized(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_allow_reload_the_whitelist_from_the_database() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
     let info_hash = InfoHash::from_str(&hash).unwrap();
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
 
-    let response = Client::new(test_env.get_connection_info()).reload_whitelist().await;
+    let response = Client::new(env.get_connection_info()).reload_whitelist().await;
 
     assert_ok(response).await;
     /* todo: this assert fails because the whitelist has not been reloaded yet.
        We could add a new endpoint GET /api/whitelist/:info_hash to check if a torrent
        is whitelisted and use that endpoint to check if the torrent is still there after reloading.
     assert!(
-        !(test_env
+        !(env
             .tracker
             .is_info_hash_whitelisted(&InfoHash::from_str(&info_hash).unwrap())
             .await)
     );
     */
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 #[tokio::test]
 async fn should_fail_when_the_whitelist_cannot_be_reloaded_from_the_database() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
     let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
     let info_hash = InfoHash::from_str(&hash).unwrap();
-    test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
+    env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();
 
-    force_database_error(&test_env.tracker);
+    force_database_error(&env.tracker);
 
-    let response = Client::new(test_env.get_connection_info()).reload_whitelist().await;
+    let response = Client::new(env.get_connection_info()).reload_whitelist().await;
 
     assert_failed_to_reload_whitelist(response).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs
index c02335d05..7b00866d3 100644
--- a/tests/servers/health_check_api/contract.rs
+++ b/tests/servers/health_check_api/contract.rs
@@ -3,23 +3,311 @@ use torrust_tracker::servers::registar::Registar;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::health_check_api::client::get;
-use crate::servers::health_check_api::test_environment;
+use crate::servers::health_check_api::Started;
 
 #[tokio::test]
-async fn health_check_endpoint_should_return_status_ok_when_no_service_is_running() {
+async fn health_check_endpoint_should_return_status_ok_when_there_is_no_services_registered() {
     let configuration = configuration::ephemeral_with_no_services();
 
-    let registar = &Registar::default();
+    let env = Started::new(&configuration.health_check_api.into(), Registar::default()).await;
 
-    let (bound_addr, test_env) = test_environment::start(&configuration.health_check_api, registar.entries()).await;
-
-    let url = format!("http://{bound_addr}/health_check");
-
-    let response = get(&url).await;
+    let response = get(&format!("http://{}/health_check", env.state.binding)).await;
 
     assert_eq!(response.status(), 200);
     assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
-    assert_eq!(response.json::<Report>().await.unwrap().status, Status::Ok);
 
-    test_env.abort();
+    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");
+}
+
+mod api {
+    use std::sync::Arc;
+
+    use torrust_tracker::servers::health_check_api::resources::{Report, Status};
+    use torrust_tracker_test_helpers::configuration;
+
+    use crate::servers::api;
+    use crate::servers::health_check_api::client::get;
+    use crate::servers::health_check_api::Started;
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_good_health_for_api_service() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = api::Started::new(&configuration).await;
+
+        let registar = service.registar.clone();
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Ok);
+            assert_eq!(report.message, String::new());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, service.bind_address());
+
+            assert_eq!(details.result, Ok("200 OK".to_string()));
+
+            assert_eq!(
+                details.info,
+                format!(
+                    "checking api health check at: http://{}/api/health_check",
+                    service.bind_address()
+                )
+            );
+
+            env.stop().await.expect("it should stop the service");
+        }
+
+        service.stop().await;
+    }
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_error_when_api_service_was_stopped_after_registration() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = api::Started::new(&configuration).await;
+
+        let binding = service.bind_address();
+
+        let registar = service.registar.clone();
+
+        service.server.stop().await.expect("it should stop udp server");
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Error);
+            assert_eq!(report.message, "health check failed".to_string());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, binding);
+            assert!(details.result.as_ref().is_err_and(|e| e.contains("Connection refused")));
+            assert_eq!(
+                details.info,
+                format!("checking api health check at: http://{binding}/api/health_check")
+            );
+
+            env.stop().await.expect("it should stop the service");
+        }
+    }
+}
+
+mod http {
+    use std::sync::Arc;
+
+    use torrust_tracker::servers::health_check_api::resources::{Report, Status};
+    use torrust_tracker_test_helpers::configuration;
+
+    use crate::servers::health_check_api::client::get;
+    use crate::servers::health_check_api::Started;
+    use crate::servers::http;
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_good_health_for_http_service() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = http::Started::new(&configuration).await;
+
+        let registar = service.registar.clone();
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Ok);
+            assert_eq!(report.message, String::new());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, *service.bind_address());
+            assert_eq!(details.result, Ok("200 OK".to_string()));
+
+            assert_eq!(
+                details.info,
+                format!(
+                    "checking http tracker health check at: http://{}/health_check",
+                    service.bind_address()
+                )
+            );
+
+            env.stop().await.expect("it should stop the service");
+        }
+
+        service.stop().await;
+    }
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_error_when_http_service_was_stopped_after_registration() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = http::Started::new(&configuration).await;
+
+        let binding = *service.bind_address();
+
+        let registar = service.registar.clone();
+
+        service.server.stop().await.expect("it should stop udp server");
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Error);
+            assert_eq!(report.message, "health check failed".to_string());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, binding);
+            assert!(details.result.as_ref().is_err_and(|e| e.contains("Connection refused")));
+            assert_eq!(
+                details.info,
+                format!("checking http tracker health check at: http://{binding}/health_check")
+            );
+
+            env.stop().await.expect("it should stop the service");
+        }
+    }
+}
+
+mod udp {
+    use std::sync::Arc;
+
+    use torrust_tracker::servers::health_check_api::resources::{Report, Status};
+    use torrust_tracker_test_helpers::configuration;
+
+    use crate::servers::health_check_api::client::get;
+    use crate::servers::health_check_api::Started;
+    use crate::servers::udp;
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_good_health_for_udp_service() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = udp::Started::new(&configuration).await;
+
+        let registar = service.registar.clone();
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Ok);
+            assert_eq!(report.message, String::new());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, service.bind_address());
+            assert_eq!(details.result, Ok("Connected".to_string()));
+
+            assert_eq!(
+                details.info,
+                format!("checking the udp tracker health check at: {}", service.bind_address())
+            );
+
+            env.stop().await.expect("it should stop the service");
+        }
+
+        service.stop().await;
+    }
+
+    #[tokio::test]
+    pub(crate) async fn it_should_return_error_when_udp_service_was_stopped_after_registration() {
+        let configuration = Arc::new(configuration::ephemeral());
+
+        let service = udp::Started::new(&configuration).await;
+
+        let binding = service.bind_address();
+
+        let registar = service.registar.clone();
+
+        service.server.stop().await.expect("it should stop udp server");
+
+        {
+            let config = configuration.health_check_api.clone();
+            let env = Started::new(&config.into(), registar).await;
+
+            let response = get(&format!("http://{}/health_check", env.state.binding)).await;
+
+            assert_eq!(response.status(), 200);
+            assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
+
+            let report: Report = response
+                .json()
+                .await
+                .expect("it should be able to get the report from the json");
+
+            assert_eq!(report.status, Status::Error);
+            assert_eq!(report.message, "health check failed".to_string());
+
+            let details = report.details.first().expect("it should have some details");
+
+            assert_eq!(details.binding, binding);
+            assert_eq!(details.result, Err("Timed Out".to_string()));
+            assert_eq!(details.info, format!("checking the udp tracker health check at: {binding}"));
+
+            env.stop().await.expect("it should stop the service");
+        }
+    }
 }
diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs
new file mode 100644
index 000000000..9aa3ab16d
--- /dev/null
+++ b/tests/servers/health_check_api/environment.rs
@@ -0,0 +1,91 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use tokio::sync::oneshot::{self, Sender};
+use tokio::task::JoinHandle;
+use torrust_tracker::bootstrap::jobs::Started;
+use torrust_tracker::servers::health_check_api::server;
+use torrust_tracker::servers::registar::Registar;
+use torrust_tracker::servers::signals::{self, Halted};
+use torrust_tracker_configuration::HealthCheckApi;
+
+#[derive(Debug)]
+pub enum Error {
+    Error(String),
+}
+
+pub struct Running {
+    pub binding: SocketAddr,
+    pub halt_task: Sender<signals::Halted>,
+    pub task: JoinHandle<SocketAddr>,
+}
+
+pub struct Stopped {
+    pub bind_to: SocketAddr,
+}
+
+pub struct Environment<S> {
+    pub registar: Registar,
+    pub state: S,
+}
+
+impl Environment<Stopped> {
+    pub fn new(config: &Arc<HealthCheckApi>, registar: Registar) -> Self {
+        let bind_to = config
+            .bind_address
+            .parse::<std::net::SocketAddr>()
+            .expect("Tracker API bind_address invalid.");
+
+        Self {
+            registar,
+            state: Stopped { bind_to },
+        }
+    }
+
+    /// Start the test environment for the Health Check API.
+    /// It runs the API server.
+    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 register = self.registar.entries();
+
+        let server = tokio::spawn(async move {
+            server::start(self.state.bind_to, tx_start, rx_halt, register)
+                .await
+                .expect("it should start the health check service");
+            self.state.bind_to
+        });
+
+        let binding = rx_start.await.expect("it should send service binding").address;
+
+        Environment {
+            registar: self.registar.clone(),
+            state: Running {
+                task: server,
+                halt_task: tx_halt,
+                binding,
+            },
+        }
+    }
+}
+
+impl Environment<Running> {
+    pub async fn new(config: &Arc<HealthCheckApi>, registar: Registar) -> Self {
+        Environment::<Stopped>::new(config, registar).start().await
+    }
+
+    pub async fn stop(self) -> Result<Environment<Stopped>, Error> {
+        self.state
+            .halt_task
+            .send(Halted::Normal)
+            .map_err(|e| Error::Error(e.to_string()))?;
+
+        let bind_to = self.state.task.await.expect("it should shutdown the service");
+
+        Ok(Environment {
+            registar: self.registar.clone(),
+            state: Stopped { bind_to },
+        })
+    }
+}
diff --git a/tests/servers/health_check_api/mod.rs b/tests/servers/health_check_api/mod.rs
index 89f19a334..9e15c5f62 100644
--- a/tests/servers/health_check_api/mod.rs
+++ b/tests/servers/health_check_api/mod.rs
@@ -1,3 +1,5 @@
 pub mod client;
 pub mod contract;
-pub mod test_environment;
+pub mod environment;
+
+pub type Started = environment::Environment<environment::Running>;
diff --git a/tests/servers/health_check_api/test_environment.rs b/tests/servers/health_check_api/test_environment.rs
deleted file mode 100644
index 18924e101..000000000
--- a/tests/servers/health_check_api/test_environment.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use std::net::SocketAddr;
-
-use tokio::sync::oneshot;
-use tokio::task::JoinHandle;
-use torrust_tracker::bootstrap::jobs::Started;
-use torrust_tracker::servers::health_check_api::server;
-use torrust_tracker::servers::registar::ServiceRegistry;
-use torrust_tracker_configuration::HealthCheckApi;
-
-/// Start the test environment for the Health Check API.
-/// It runs the API server.
-pub async fn start(config: &HealthCheckApi, register: ServiceRegistry) -> (SocketAddr, JoinHandle<()>) {
-    let bind_addr = config
-        .bind_address
-        .parse::<std::net::SocketAddr>()
-        .expect("Health Check API bind_address invalid.");
-
-    let (tx, rx) = oneshot::channel::<Started>();
-
-    let join_handle = tokio::spawn(async move {
-        let handle = server::start(bind_addr, tx, register);
-        if let Ok(()) = handle.await {
-            panic!("Health Check API server on http://{bind_addr} stopped");
-        }
-    });
-
-    let bound_addr = match rx.await {
-        Ok(msg) => msg.address,
-        Err(e) => panic!("the Health Check API server was dropped: {e}"),
-    };
-
-    (bound_addr, join_handle)
-}
diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs
new file mode 100644
index 000000000..326f4e534
--- /dev/null
+++ b/tests/servers/http/environment.rs
@@ -0,0 +1,81 @@
+use std::sync::Arc;
+
+use futures::executor::block_on;
+use torrust_tracker::bootstrap::app::initialize_with_configuration;
+use torrust_tracker::bootstrap::jobs::make_rust_tls;
+use torrust_tracker::core::peer::Peer;
+use torrust_tracker::core::Tracker;
+use torrust_tracker::servers::http::server::{HttpServer, Launcher, Running, Stopped};
+use torrust_tracker::servers::registar::Registar;
+use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
+use torrust_tracker_configuration::{Configuration, HttpTracker};
+
+pub struct Environment<S> {
+    pub config: Arc<HttpTracker>,
+    pub tracker: Arc<Tracker>,
+    pub registar: Registar,
+    pub server: HttpServer<S>,
+}
+
+impl<S> Environment<S> {
+    /// Add a torrent to the tracker
+    pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) {
+        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
+    }
+}
+
+impl Environment<Stopped> {
+    #[allow(dead_code)]
+    pub fn new(configuration: &Arc<Configuration>) -> Self {
+        let tracker = initialize_with_configuration(configuration);
+
+        let config = Arc::new(configuration.http_trackers[0].clone());
+
+        let bind_to = config
+            .bind_address
+            .parse::<std::net::SocketAddr>()
+            .expect("Tracker API bind_address invalid.");
+
+        let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path))
+            .map(|tls| tls.expect("tls config failed"));
+
+        let server = HttpServer::new(Launcher::new(bind_to, tls));
+
+        Self {
+            config,
+            tracker,
+            registar: Registar::default(),
+            server,
+        }
+    }
+
+    #[allow(dead_code)]
+    pub async fn start(self) -> Environment<Running> {
+        Environment {
+            config: self.config,
+            tracker: self.tracker.clone(),
+            registar: self.registar.clone(),
+            server: self.server.start(self.tracker, self.registar.give_form()).await.unwrap(),
+        }
+    }
+}
+
+impl Environment<Running> {
+    pub async fn new(configuration: &Arc<Configuration>) -> Self {
+        Environment::<Stopped>::new(configuration).start().await
+    }
+
+    pub async fn stop(self) -> Environment<Stopped> {
+        Environment {
+            config: self.config,
+            tracker: self.tracker,
+            registar: Registar::default(),
+
+            server: self.server.stop().await.unwrap(),
+        }
+    }
+
+    pub fn bind_address(&self) -> &std::net::SocketAddr {
+        &self.server.state.binding
+    }
+}
diff --git a/tests/servers/http/mod.rs b/tests/servers/http/mod.rs
index cb2885df0..65affc433 100644
--- a/tests/servers/http/mod.rs
+++ b/tests/servers/http/mod.rs
@@ -1,11 +1,14 @@
 pub mod asserts;
 pub mod client;
+pub mod environment;
 pub mod requests;
 pub mod responses;
-pub mod test_environment;
 pub mod v1;
 
+pub type Started = environment::Environment<server::Running>;
+
 use percent_encoding::NON_ALPHANUMERIC;
+use torrust_tracker::servers::http::server;
 
 pub type ByteArray20 = [u8; 20];
 
diff --git a/tests/servers/http/test_environment.rs b/tests/servers/http/test_environment.rs
deleted file mode 100644
index 9cab40db2..000000000
--- a/tests/servers/http/test_environment.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use std::sync::Arc;
-
-use futures::executor::block_on;
-use torrust_tracker::bootstrap::jobs::make_rust_tls;
-use torrust_tracker::core::peer::Peer;
-use torrust_tracker::core::Tracker;
-use torrust_tracker::servers::http::server::{HttpServer, Launcher, RunningHttpServer, StoppedHttpServer};
-use torrust_tracker::servers::registar::Registar;
-use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
-
-use crate::common::app::setup_with_configuration;
-
-#[allow(clippy::module_name_repetitions, dead_code)]
-pub type StoppedTestEnvironment = TestEnvironment<Stopped>;
-#[allow(clippy::module_name_repetitions)]
-pub type RunningTestEnvironment = TestEnvironment<Running>;
-
-pub struct TestEnvironment<S> {
-    pub cfg: Arc<torrust_tracker_configuration::Configuration>,
-    pub tracker: Arc<Tracker>,
-    pub state: S,
-}
-
-#[allow(dead_code)]
-pub struct Stopped {
-    http_server: StoppedHttpServer,
-}
-
-pub struct Running {
-    http_server: RunningHttpServer,
-}
-
-impl<S> TestEnvironment<S> {
-    /// Add a torrent to the tracker
-    pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) {
-        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
-    }
-}
-
-impl TestEnvironment<Stopped> {
-    #[allow(dead_code)]
-    pub fn new_stopped(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let cfg = Arc::new(cfg);
-
-        let tracker = setup_with_configuration(&cfg);
-
-        let config = cfg.http_trackers[0].clone();
-
-        let bind_to = config
-            .bind_address
-            .parse::<std::net::SocketAddr>()
-            .expect("Tracker API bind_address invalid.");
-
-        let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path))
-            .map(|tls| tls.expect("tls config failed"));
-
-        let http_server = HttpServer::new(Launcher::new(bind_to, tls));
-
-        Self {
-            cfg,
-            tracker,
-            state: Stopped { http_server },
-        }
-    }
-
-    #[allow(dead_code)]
-    pub async fn start(self) -> TestEnvironment<Running> {
-        TestEnvironment {
-            cfg: self.cfg,
-            tracker: self.tracker.clone(),
-            state: Running {
-                http_server: self
-                    .state
-                    .http_server
-                    .start(self.tracker, Registar::default().give_form())
-                    .await
-                    .unwrap(),
-            },
-        }
-    }
-
-    // #[allow(dead_code)]
-    // pub fn config(&self) -> &torrust_tracker_configuration::HttpTracker {
-    //     &self.state.http_server.cfg
-    // }
-
-    // #[allow(dead_code)]
-    // pub fn config_mut(&mut self) -> &mut torrust_tracker_configuration::HttpTracker {
-    //     &mut self.state.http_server.cfg
-    // }
-}
-
-impl TestEnvironment<Running> {
-    pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let test_env = StoppedTestEnvironment::new_stopped(cfg);
-
-        test_env.start().await
-    }
-
-    pub async fn stop(self) -> TestEnvironment<Stopped> {
-        TestEnvironment {
-            cfg: self.cfg,
-            tracker: self.tracker,
-            state: Stopped {
-                http_server: self.state.http_server.stop().await.unwrap(),
-            },
-        }
-    }
-
-    pub fn bind_address(&self) -> &std::net::SocketAddr {
-        &self.state.http_server.state.binding
-    }
-
-    // #[allow(dead_code)]
-    // pub fn config(&self) -> &torrust_tracker_configuration::HttpTracker {
-    //     &self.state.http_server.cfg
-    // }
-}
-
-#[allow(clippy::module_name_repetitions, dead_code)]
-pub fn stopped_test_environment(cfg: torrust_tracker_configuration::Configuration) -> StoppedTestEnvironment {
-    TestEnvironment::new_stopped(cfg)
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub async fn running_test_environment(cfg: torrust_tracker_configuration::Configuration) -> RunningTestEnvironment {
-    TestEnvironment::new_running(cfg).await
-}
-
-#[allow(dead_code)]
-pub fn http_server(launcher: Launcher) -> StoppedHttpServer {
-    HttpServer::new(launcher)
-}
diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs
index e394779ad..be285dcd7 100644
--- a/tests/servers/http/v1/contract.rs
+++ b/tests/servers/http/v1/contract.rs
@@ -1,12 +1,12 @@
 use torrust_tracker_test_helpers::configuration;
 
-use crate::servers::http::test_environment::running_test_environment;
+use crate::servers::http::Started;
 
 #[tokio::test]
-async fn test_environment_should_be_started_and_stopped() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+async fn environment_should_be_started_and_stopped() {
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    test_env.stop().await;
+    env.stop().await;
 }
 
 mod for_all_config_modes {
@@ -15,19 +15,19 @@ mod for_all_config_modes {
     use torrust_tracker_test_helpers::configuration;
 
     use crate::servers::http::client::Client;
-    use crate::servers::http::test_environment::running_test_environment;
+    use crate::servers::http::Started;
 
     #[tokio::test]
     async fn health_check_endpoint_should_return_ok_if_the_http_tracker_is_running() {
-        let test_env = running_test_environment(configuration::ephemeral_with_reverse_proxy()).await;
+        let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
 
-        let response = Client::new(*test_env.bind_address()).health_check().await;
+        let response = Client::new(*env.bind_address()).health_check().await;
 
         assert_eq!(response.status(), 200);
         assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
         assert_eq!(response.json::<Report>().await.unwrap(), Report { status: Status::Ok });
 
-        test_env.stop().await;
+        env.stop().await;
     }
 
     mod and_running_on_reverse_proxy {
@@ -36,37 +36,37 @@ mod for_all_config_modes {
         use crate::servers::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response;
         use crate::servers::http::client::Client;
         use crate::servers::http::requests::announce::QueryBuilder;
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::Started;
 
         #[tokio::test]
         async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() {
             // If the tracker is running behind a reverse proxy, the peer IP is the
             // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client.
 
-            let test_env = running_test_environment(configuration::ephemeral_with_reverse_proxy()).await;
+            let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
 
             let params = QueryBuilder::default().query().params();
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() {
-            let test_env = running_test_environment(configuration::ephemeral_with_reverse_proxy()).await;
+            let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
 
             let params = QueryBuilder::default().query().params();
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP")
                 .await;
 
             assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 
@@ -102,60 +102,59 @@ mod for_all_config_modes {
         };
         use crate::servers::http::client::Client;
         use crate::servers::http::requests::announce::{Compact, QueryBuilder};
-        use crate::servers::http::responses;
         use crate::servers::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer};
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::{responses, Started};
 
         #[tokio::test]
         async fn it_should_start_and_stop() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
-            test_env.stop().await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_respond_if_only_the_mandatory_fields_are_provided() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
             params.remove_optional_params();
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_is_announce_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_url_query_component_is_empty() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
-            let response = Client::new(*test_env.bind_address()).get("announce").await;
+            let response = Client::new(*env.bind_address()).get("announce").await;
 
             assert_missing_query_params_for_announce_request_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_url_query_parameters_are_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let invalid_query_param = "a=b=c";
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .get(&format!("announce?{invalid_query_param}"))
                 .await;
 
             assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_a_mandatory_field_is_missing() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             // Without `info_hash` param
 
@@ -163,7 +162,7 @@ mod for_all_config_modes {
 
             params.info_hash = None;
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_bad_announce_request_error_response(response, "missing param info_hash").await;
 
@@ -173,7 +172,7 @@ mod for_all_config_modes {
 
             params.peer_id = None;
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_bad_announce_request_error_response(response, "missing param peer_id").await;
 
@@ -183,28 +182,28 @@ mod for_all_config_modes {
 
             params.port = None;
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_bad_announce_request_error_response(response, "missing param port").await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_info_hash_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
             for invalid_value in &invalid_info_hashes() {
                 params.set("info_hash", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_cannot_parse_query_params_error_response(response, "").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -214,22 +213,22 @@ mod for_all_config_modes {
             // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP.
             // 2. If tracker is     running `on_reverse_proxy` from `X-Forwarded-For` request HTTP header.
 
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
             params.peer_addr = Some("INVALID-IP-ADDRESS".to_string());
 
-            let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+            let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
             assert_is_announce_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_downloaded_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -238,17 +237,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("downloaded", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_uploaded_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -257,17 +256,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("uploaded", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_peer_id_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -283,17 +282,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("peer_id", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_port_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -302,17 +301,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("port", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_left_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -321,17 +320,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("left", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_event_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -348,17 +347,17 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("event", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_compact_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral()).await;
+            let env = Started::new(&configuration::ephemeral().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
@@ -367,19 +366,19 @@ mod for_all_config_modes {
             for invalid_value in invalid_values {
                 params.set("compact", invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_bad_announce_request_error_response(response, "invalid param value").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap())
@@ -387,7 +386,7 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let announce_policy = test_env.tracker.get_announce_policy();
+            let announce_policy = env.tracker.get_announce_policy();
 
             assert_announce_response(
                 response,
@@ -401,12 +400,12 @@ mod for_all_config_modes {
             )
             .await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_list_of_previously_announced_peers() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
@@ -416,10 +415,10 @@ mod for_all_config_modes {
                 .build();
 
             // Add the Peer 1
-            test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
+            env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
 
             // Announce the new Peer 2. This new peer is non included on the response peer list
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_info_hash(&info_hash)
@@ -428,7 +427,7 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let announce_policy = test_env.tracker.get_announce_policy();
+            let announce_policy = env.tracker.get_announce_policy();
 
             // It should only contain the previously announced peer
             assert_announce_response(
@@ -443,12 +442,12 @@ mod for_all_config_modes {
             )
             .await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
@@ -457,7 +456,7 @@ mod for_all_config_modes {
                 .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
                 .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080))
                 .build();
-            test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await;
+            env.add_torrent_peer(&info_hash, &peer_using_ipv4).await;
 
             // Announce a peer using IPV6
             let peer_using_ipv6 = PeerBuilder::default()
@@ -467,10 +466,10 @@ mod for_all_config_modes {
                     8080,
                 ))
                 .build();
-            test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await;
+            env.add_torrent_peer(&info_hash, &peer_using_ipv6).await;
 
             // Announce the new Peer.
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_info_hash(&info_hash)
@@ -479,7 +478,7 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let announce_policy = test_env.tracker.get_announce_policy();
+            let announce_policy = env.tracker.get_announce_policy();
 
             // The newly announced peer is not included on the response peer list,
             // but all the previously announced peers should be included regardless the IP version they are using.
@@ -495,18 +494,18 @@ mod for_all_config_modes {
             )
             .await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
             let peer = PeerBuilder::default().build();
 
             // Add a peer
-            test_env.add_torrent_peer(&info_hash, &peer).await;
+            env.add_torrent_peer(&info_hash, &peer).await;
 
             let announce_query = QueryBuilder::default()
                 .with_info_hash(&info_hash)
@@ -515,11 +514,11 @@ mod for_all_config_modes {
 
             assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr);
 
-            let response = Client::new(*test_env.bind_address()).announce(&announce_query).await;
+            let response = Client::new(*env.bind_address()).announce(&announce_query).await;
 
             assert_empty_announce_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -527,7 +526,7 @@ mod for_all_config_modes {
             // Tracker Returns Compact Peer Lists
             // https://www.bittorrent.org/beps/bep_0023.html
 
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
@@ -537,10 +536,10 @@ mod for_all_config_modes {
                 .build();
 
             // Add the Peer 1
-            test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
+            env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
 
             // Announce the new Peer 2 accepting compact responses
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_info_hash(&info_hash)
@@ -560,7 +559,7 @@ mod for_all_config_modes {
 
             assert_compact_announce_response(response, &expected_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -568,7 +567,7 @@ mod for_all_config_modes {
             // code-review: the HTTP tracker does not return the compact response by default if the "compact"
             // param is not provided in the announce URL. The BEP 23 suggest to do so.
 
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
@@ -578,12 +577,12 @@ mod for_all_config_modes {
                 .build();
 
             // Add the Peer 1
-            test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
+            env.add_torrent_peer(&info_hash, &previously_announced_peer).await;
 
             // Announce the new Peer 2 without passing the "compact" param
             // By default it should respond with the compact peer list
             // https://www.bittorrent.org/beps/bep_0023.html
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_info_hash(&info_hash)
@@ -595,7 +594,7 @@ mod for_all_config_modes {
 
             assert!(!is_a_compact_announce_response(response).await);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         async fn is_a_compact_announce_response(response: Response) -> bool {
@@ -606,19 +605,19 @@ mod for_all_config_modes {
 
         #[tokio::test]
         async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
-            Client::new(*test_env.bind_address())
+            Client::new(*env.bind_address())
                 .announce(&QueryBuilder::default().query())
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp4_connections_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -630,28 +629,28 @@ mod for_all_config_modes {
                 return; // we cannot bind to a ipv6 socket, so we will skip this test
             }
 
-            let test_env = running_test_environment(configuration::ephemeral_ipv6()).await;
+            let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
 
-            Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap())
+            Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
                 .announce(&QueryBuilder::default().query())
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp6_connections_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() {
             // The tracker ignores the peer address in the request param. It uses the client remote ip address.
 
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
-            Client::new(*test_env.bind_address())
+            Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
@@ -659,30 +658,30 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp6_connections_handled, 0);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
-            Client::new(*test_env.bind_address())
+            Client::new(*env.bind_address())
                 .announce(&QueryBuilder::default().query())
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp4_announces_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -694,28 +693,28 @@ mod for_all_config_modes {
                 return; // we cannot bind to a ipv6 socket, so we will skip this test
             }
 
-            let test_env = running_test_environment(configuration::ephemeral_ipv6()).await;
+            let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
 
-            Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap())
+            Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
                 .announce(&QueryBuilder::default().query())
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp6_announces_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() {
             // The tracker ignores the peer address in the request param. It uses the client remote ip address.
 
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
-            Client::new(*test_env.bind_address())
+            Client::new(*env.bind_address())
                 .announce(
                     &QueryBuilder::default()
                         .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
@@ -723,18 +722,18 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp6_announces_handled, 0);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
             let client_ip = local_ip().unwrap();
@@ -745,19 +744,19 @@ mod for_all_config_modes {
                 .query();
 
             {
-                let client = Client::bind(*test_env.bind_address(), client_ip);
+                let client = Client::bind(*env.bind_address(), client_ip);
                 let status = client.announce(&announce_query).await.status();
 
                 assert_eq!(status, StatusCode::OK);
             }
 
-            let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
+            let peers = env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
             assert_eq!(peer_addr.ip(), client_ip);
             assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -768,11 +767,8 @@ mod for_all_config_modes {
                 client     <-> tracker                      <-> Internet
                 127.0.0.1      external_ip = "2.137.87.41"
             */
-
-            let test_env = running_test_environment(configuration::ephemeral_with_external_ip(
-                IpAddr::from_str("2.137.87.41").unwrap(),
-            ))
-            .await;
+            let env =
+                Started::new(&configuration::ephemeral_with_external_ip(IpAddr::from_str("2.137.87.41").unwrap()).into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
             let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap();
@@ -784,19 +780,19 @@ mod for_all_config_modes {
                 .query();
 
             {
-                let client = Client::bind(*test_env.bind_address(), client_ip);
+                let client = Client::bind(*env.bind_address(), client_ip);
                 let status = client.announce(&announce_query).await.status();
 
                 assert_eq!(status, StatusCode::OK);
             }
 
-            let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
+            let peers = env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
-            assert_eq!(peer_addr.ip(), test_env.tracker.get_maybe_external_ip().unwrap());
+            assert_eq!(peer_addr.ip(), env.tracker.get_maybe_external_ip().unwrap());
             assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -808,9 +804,10 @@ mod for_all_config_modes {
                ::1            external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5"
             */
 
-            let test_env = running_test_environment(configuration::ephemeral_with_external_ip(
-                IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(),
-            ))
+            let env = Started::new(
+                &configuration::ephemeral_with_external_ip(IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap())
+                    .into(),
+            )
             .await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
@@ -823,19 +820,19 @@ mod for_all_config_modes {
                 .query();
 
             {
-                let client = Client::bind(*test_env.bind_address(), client_ip);
+                let client = Client::bind(*env.bind_address(), client_ip);
                 let status = client.announce(&announce_query).await.status();
 
                 assert_eq!(status, StatusCode::OK);
             }
 
-            let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
+            let peers = env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
-            assert_eq!(peer_addr.ip(), test_env.tracker.get_maybe_external_ip().unwrap());
+            assert_eq!(peer_addr.ip(), env.tracker.get_maybe_external_ip().unwrap());
             assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -847,14 +844,14 @@ mod for_all_config_modes {
             145.254.214.256     X-Forwarded-For = 145.254.214.256    on_reverse_proxy = true       145.254.214.256
             */
 
-            let test_env = running_test_environment(configuration::ephemeral_with_reverse_proxy()).await;
+            let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
             let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query();
 
             {
-                let client = Client::new(*test_env.bind_address());
+                let client = Client::new(*env.bind_address());
                 let status = client
                     .announce_with_header(
                         &announce_query,
@@ -867,12 +864,12 @@ mod for_all_config_modes {
                 assert_eq!(status, StatusCode::OK);
             }
 
-            let peers = test_env.tracker.get_torrent_peers(&info_hash).await;
+            let peers = env.tracker.get_torrent_peers(&info_hash).await;
             let peer_addr = peers[0].peer_addr;
 
             assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap());
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 
@@ -901,56 +898,54 @@ mod for_all_config_modes {
             assert_scrape_response,
         };
         use crate::servers::http::client::Client;
-        use crate::servers::http::requests;
         use crate::servers::http::requests::scrape::QueryBuilder;
         use crate::servers::http::responses::scrape::{self, File, ResponseBuilder};
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::{requests, Started};
 
         //#[tokio::test]
         #[allow(dead_code)]
         async fn should_fail_when_the_request_is_empty() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
-            let response = Client::new(*test_env.bind_address()).get("scrape").await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
+            let response = Client::new(*env.bind_address()).get("scrape").await;
 
             assert_missing_query_params_for_scrape_request_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_when_the_info_hash_param_is_invalid() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let mut params = QueryBuilder::default().query().params();
 
             for invalid_value in &invalid_info_hashes() {
                 params.set_one_info_hash_param(invalid_value);
 
-                let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await;
+                let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
 
                 assert_cannot_parse_query_params_error_response(response, "").await;
             }
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -971,26 +966,25 @@ mod for_all_config_modes {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_no_bytes_pending_to_download()
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_no_bytes_pending_to_download()
+                    .build(),
+            )
+            .await;
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1011,16 +1005,16 @@ mod for_all_config_modes {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1030,17 +1024,17 @@ mod for_all_config_modes {
 
             assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_accept_multiple_infohashes() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
             let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap();
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .add_info_hash(&info_hash1)
@@ -1056,16 +1050,16 @@ mod for_all_config_modes {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_public()).await;
+            let env = Started::new(&configuration::ephemeral_mode_public().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            Client::new(*test_env.bind_address())
+            Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1073,13 +1067,13 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp4_scrapes_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -1091,11 +1085,11 @@ mod for_all_config_modes {
                 return; // we cannot bind to a ipv6 socket, so we will skip this test
             }
 
-            let test_env = running_test_environment(configuration::ephemeral_ipv6()).await;
+            let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap())
+            Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1103,13 +1097,13 @@ mod for_all_config_modes {
                 )
                 .await;
 
-            let stats = test_env.tracker.get_stats().await;
+            let stats = env.tracker.get_stats().await;
 
             assert_eq!(stats.tcp6_scrapes_handled, 1);
 
             drop(stats);
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 }
@@ -1125,42 +1119,41 @@ mod configured_as_whitelisted {
         use crate::servers::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response};
         use crate::servers::http::client::Client;
         use crate::servers::http::requests::announce::QueryBuilder;
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::Started;
 
         #[tokio::test]
         async fn should_fail_if_the_torrent_is_not_in_the_whitelist() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_whitelisted()).await;
+            let env = Started::new(&configuration::ephemeral_mode_whitelisted().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
                 .await;
 
             assert_torrent_not_in_whitelist_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_allow_announcing_a_whitelisted_torrent() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_whitelisted()).await;
+            let env = Started::new(&configuration::ephemeral_mode_whitelisted().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .tracker
+            env.tracker
                 .add_torrent_to_whitelist(&info_hash)
                 .await
                 .expect("should add the torrent to the whitelist");
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
                 .await;
 
             assert_is_announce_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 
@@ -1174,27 +1167,25 @@ mod configured_as_whitelisted {
 
         use crate::servers::http::asserts::assert_scrape_response;
         use crate::servers::http::client::Client;
-        use crate::servers::http::requests;
         use crate::servers::http::responses::scrape::{File, ResponseBuilder};
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::{requests, Started};
 
         #[tokio::test]
         async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_whitelisted()).await;
+            let env = Started::new(&configuration::ephemeral_mode_whitelisted().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1206,32 +1197,30 @@ mod configured_as_whitelisted {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_whitelisted()).await;
+            let env = Started::new(&configuration::ephemeral_mode_whitelisted().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
-            test_env
-                .tracker
+            env.tracker
                 .add_torrent_to_whitelist(&info_hash)
                 .await
                 .expect("should add the torrent to the whitelist");
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1252,7 +1241,7 @@ mod configured_as_whitelisted {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 }
@@ -1270,45 +1259,45 @@ mod configured_as_private {
         use crate::servers::http::asserts::{assert_authentication_error_response, assert_is_announce_response};
         use crate::servers::http::client::Client;
         use crate::servers::http::requests::announce::QueryBuilder;
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::Started;
 
         #[tokio::test]
         async fn should_respond_to_authenticated_peers() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
-            let expiring_key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap();
+            let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap();
 
-            let response = Client::authenticated(*test_env.bind_address(), expiring_key.key())
+            let response = Client::authenticated(*env.bind_address(), expiring_key.key())
                 .announce(&QueryBuilder::default().query())
                 .await;
 
             assert_is_announce_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
                 .await;
 
             assert_authentication_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_fail_if_the_key_query_param_cannot_be_parsed() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let invalid_key = "INVALID_KEY";
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                     .get(&format!(
                         "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0"
                     ))
@@ -1319,18 +1308,18 @@ mod configured_as_private {
 
         #[tokio::test]
         async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             // The tracker does not have this key
             let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap();
 
-            let response = Client::authenticated(*test_env.bind_address(), unregistered_key)
+            let response = Client::authenticated(*env.bind_address(), unregistered_key)
                 .announce(&QueryBuilder::default().query())
                 .await;
 
             assert_authentication_error_response(response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 
@@ -1347,17 +1336,16 @@ mod configured_as_private {
 
         use crate::servers::http::asserts::{assert_authentication_error_response, assert_scrape_response};
         use crate::servers::http::client::Client;
-        use crate::servers::http::requests;
         use crate::servers::http::responses::scrape::{File, ResponseBuilder};
-        use crate::servers::http::test_environment::running_test_environment;
+        use crate::servers::http::{requests, Started};
 
         #[tokio::test]
         async fn should_fail_if_the_key_query_param_cannot_be_parsed() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let invalid_key = "INVALID_KEY";
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .get(&format!(
                     "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"
                 ))
@@ -1368,21 +1356,20 @@ mod configured_as_private {
 
         #[tokio::test]
         async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
-            let response = Client::new(*test_env.bind_address())
+            let response = Client::new(*env.bind_address())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1394,28 +1381,27 @@ mod configured_as_private {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
         async fn should_return_the_real_file_stats_when_the_client_is_authenticated() {
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
-            let expiring_key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap();
+            let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap();
 
-            let response = Client::authenticated(*test_env.bind_address(), expiring_key.key())
+            let response = Client::authenticated(*env.bind_address(), expiring_key.key())
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1436,7 +1422,7 @@ mod configured_as_private {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
 
         #[tokio::test]
@@ -1444,23 +1430,22 @@ mod configured_as_private {
             // There is not authentication error
             // code-review: should this really be this way?
 
-            let test_env = running_test_environment(configuration::ephemeral_mode_private()).await;
+            let env = Started::new(&configuration::ephemeral_mode_private().into()).await;
 
             let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
 
-            test_env
-                .add_torrent_peer(
-                    &info_hash,
-                    &PeerBuilder::default()
-                        .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
-                        .with_bytes_pending_to_download(1)
-                        .build(),
-                )
-                .await;
+            env.add_torrent_peer(
+                &info_hash,
+                &PeerBuilder::default()
+                    .with_peer_id(&peer::Id(*b"-qB00000000000000001"))
+                    .with_bytes_pending_to_download(1)
+                    .build(),
+            )
+            .await;
 
             let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap();
 
-            let response = Client::authenticated(*test_env.bind_address(), false_key)
+            let response = Client::authenticated(*env.bind_address(), false_key)
                 .scrape(
                     &requests::scrape::QueryBuilder::default()
                         .with_one_info_hash(&info_hash)
@@ -1472,7 +1457,7 @@ mod configured_as_private {
 
             assert_scrape_response(response, &expected_scrape_response).await;
 
-            test_env.stop().await;
+            env.stop().await;
         }
     }
 }
diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs
index b16a47cd3..9ac585190 100644
--- a/tests/servers/udp/contract.rs
+++ b/tests/servers/udp/contract.rs
@@ -11,7 +11,7 @@ use torrust_tracker::shared::bit_torrent::tracker::udp::MAX_PACKET_SIZE;
 use torrust_tracker_test_helpers::configuration;
 
 use crate::servers::udp::asserts::is_error_response;
-use crate::servers::udp::test_environment::running_test_environment;
+use crate::servers::udp::Started;
 
 fn empty_udp_request() -> [u8; MAX_PACKET_SIZE] {
     [0; MAX_PACKET_SIZE]
@@ -36,9 +36,9 @@ async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrac
 
 #[tokio::test]
 async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() {
-    let test_env = running_test_environment(configuration::ephemeral()).await;
+    let env = Started::new(&configuration::ephemeral().into()).await;
 
-    let client = new_udp_client_connected(&test_env.bind_address().to_string()).await;
+    let client = new_udp_client_connected(&env.bind_address().to_string()).await;
 
     client.send(&empty_udp_request()).await;
 
@@ -55,13 +55,13 @@ mod receiving_a_connection_request {
     use torrust_tracker_test_helpers::configuration;
 
     use crate::servers::udp::asserts::is_connect_response;
-    use crate::servers::udp::test_environment::running_test_environment;
+    use crate::servers::udp::Started;
 
     #[tokio::test]
     async fn should_return_a_connect_response() {
-        let test_env = running_test_environment(configuration::ephemeral()).await;
+        let env = Started::new(&configuration::ephemeral().into()).await;
 
-        let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await;
+        let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await;
 
         let connect_request = ConnectRequest {
             transaction_id: TransactionId(123),
@@ -87,13 +87,13 @@ mod receiving_an_announce_request {
 
     use crate::servers::udp::asserts::is_ipv4_announce_response;
     use crate::servers::udp::contract::send_connection_request;
-    use crate::servers::udp::test_environment::running_test_environment;
+    use crate::servers::udp::Started;
 
     #[tokio::test]
     async fn should_return_an_announce_response() {
-        let test_env = running_test_environment(configuration::ephemeral()).await;
+        let env = Started::new(&configuration::ephemeral().into()).await;
 
-        let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await;
+        let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await;
 
         let connection_id = send_connection_request(TransactionId(123), &client).await;
 
@@ -129,13 +129,13 @@ mod receiving_an_scrape_request {
 
     use crate::servers::udp::asserts::is_scrape_response;
     use crate::servers::udp::contract::send_connection_request;
-    use crate::servers::udp::test_environment::running_test_environment;
+    use crate::servers::udp::Started;
 
     #[tokio::test]
     async fn should_return_a_scrape_response() {
-        let test_env = running_test_environment(configuration::ephemeral()).await;
+        let env = Started::new(&configuration::ephemeral().into()).await;
 
-        let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await;
+        let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await;
 
         let connection_id = send_connection_request(TransactionId(123), &client).await;
 
diff --git a/tests/servers/udp/environment.rs b/tests/servers/udp/environment.rs
new file mode 100644
index 000000000..26a47987e
--- /dev/null
+++ b/tests/servers/udp/environment.rs
@@ -0,0 +1,78 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use torrust_tracker::bootstrap::app::initialize_with_configuration;
+use torrust_tracker::core::peer::Peer;
+use torrust_tracker::core::Tracker;
+use torrust_tracker::servers::registar::Registar;
+use torrust_tracker::servers::udp::server::{Launcher, Running, Stopped, UdpServer};
+use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
+use torrust_tracker_configuration::{Configuration, UdpTracker};
+
+pub struct Environment<S> {
+    pub config: Arc<UdpTracker>,
+    pub tracker: Arc<Tracker>,
+    pub registar: Registar,
+    pub server: UdpServer<S>,
+}
+
+impl<S> Environment<S> {
+    /// Add a torrent to the tracker
+    #[allow(dead_code)]
+    pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) {
+        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
+    }
+}
+
+impl Environment<Stopped> {
+    #[allow(dead_code)]
+    pub fn new(configuration: &Arc<Configuration>) -> Self {
+        let tracker = initialize_with_configuration(configuration);
+
+        let config = Arc::new(configuration.udp_trackers[0].clone());
+
+        let bind_to = config
+            .bind_address
+            .parse::<std::net::SocketAddr>()
+            .expect("Tracker API bind_address invalid.");
+
+        let server = UdpServer::new(Launcher::new(bind_to));
+
+        Self {
+            config,
+            tracker,
+            registar: Registar::default(),
+            server,
+        }
+    }
+
+    #[allow(dead_code)]
+    pub async fn start(self) -> Environment<Running> {
+        Environment {
+            config: self.config,
+            tracker: self.tracker.clone(),
+            registar: self.registar.clone(),
+            server: self.server.start(self.tracker, self.registar.give_form()).await.unwrap(),
+        }
+    }
+}
+
+impl Environment<Running> {
+    pub async fn new(configuration: &Arc<Configuration>) -> Self {
+        Environment::<Stopped>::new(configuration).start().await
+    }
+
+    #[allow(dead_code)]
+    pub async fn stop(self) -> Environment<Stopped> {
+        Environment {
+            config: self.config,
+            tracker: self.tracker,
+            registar: Registar::default(),
+            server: self.server.stop().await.unwrap(),
+        }
+    }
+
+    pub fn bind_address(&self) -> SocketAddr {
+        self.server.state.binding
+    }
+}
diff --git a/tests/servers/udp/mod.rs b/tests/servers/udp/mod.rs
index 4759350dc..b13b82240 100644
--- a/tests/servers/udp/mod.rs
+++ b/tests/servers/udp/mod.rs
@@ -1,3 +1,7 @@
+use torrust_tracker::servers::udp::server;
+
 pub mod asserts;
 pub mod contract;
-pub mod test_environment;
+pub mod environment;
+
+pub type Started = environment::Environment<server::Running>;
diff --git a/tests/servers/udp/test_environment.rs b/tests/servers/udp/test_environment.rs
deleted file mode 100644
index f272b6dd3..000000000
--- a/tests/servers/udp/test_environment.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use std::net::SocketAddr;
-use std::sync::Arc;
-
-use torrust_tracker::core::peer::Peer;
-use torrust_tracker::core::Tracker;
-use torrust_tracker::servers::registar::Registar;
-use torrust_tracker::servers::udp::server::{Launcher, RunningUdpServer, StoppedUdpServer, UdpServer};
-use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
-
-use crate::common::app::setup_with_configuration;
-
-#[allow(clippy::module_name_repetitions, dead_code)]
-pub type StoppedTestEnvironment = TestEnvironment<Stopped>;
-#[allow(clippy::module_name_repetitions)]
-pub type RunningTestEnvironment = TestEnvironment<Running>;
-
-pub struct TestEnvironment<S> {
-    pub cfg: Arc<torrust_tracker_configuration::Configuration>,
-    pub tracker: Arc<Tracker>,
-    pub state: S,
-}
-
-#[allow(dead_code)]
-pub struct Stopped {
-    udp_server: StoppedUdpServer,
-}
-
-pub struct Running {
-    udp_server: RunningUdpServer,
-}
-
-impl<S> TestEnvironment<S> {
-    /// Add a torrent to the tracker
-    #[allow(dead_code)]
-    pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) {
-        self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await;
-    }
-}
-
-impl TestEnvironment<Stopped> {
-    #[allow(dead_code)]
-    pub fn new_stopped(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        let cfg = Arc::new(cfg);
-
-        let tracker = setup_with_configuration(&cfg);
-
-        let udp_cfg = cfg.udp_trackers[0].clone();
-
-        let bind_to = udp_cfg
-            .bind_address
-            .parse::<std::net::SocketAddr>()
-            .expect("Tracker API bind_address invalid.");
-
-        let udp_server = udp_server(Launcher::new(bind_to));
-
-        Self {
-            cfg,
-            tracker,
-            state: Stopped { udp_server },
-        }
-    }
-
-    #[allow(dead_code)]
-    pub async fn start(self) -> TestEnvironment<Running> {
-        let register = &Registar::default();
-
-        TestEnvironment {
-            cfg: self.cfg,
-            tracker: self.tracker.clone(),
-            state: Running {
-                udp_server: self.state.udp_server.start(self.tracker, register.give_form()).await.unwrap(),
-            },
-        }
-    }
-}
-
-impl TestEnvironment<Running> {
-    pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self {
-        StoppedTestEnvironment::new_stopped(cfg).start().await
-    }
-
-    #[allow(dead_code)]
-    pub async fn stop(self) -> TestEnvironment<Stopped> {
-        TestEnvironment {
-            cfg: self.cfg,
-            tracker: self.tracker,
-            state: Stopped {
-                udp_server: self.state.udp_server.stop().await.unwrap(),
-            },
-        }
-    }
-
-    pub fn bind_address(&self) -> SocketAddr {
-        self.state.udp_server.state.binding
-    }
-}
-
-#[allow(clippy::module_name_repetitions, dead_code)]
-pub fn stopped_test_environment(cfg: torrust_tracker_configuration::Configuration) -> StoppedTestEnvironment {
-    TestEnvironment::new_stopped(cfg)
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub async fn running_test_environment(cfg: torrust_tracker_configuration::Configuration) -> RunningTestEnvironment {
-    TestEnvironment::new_running(cfg).await
-}
-
-pub fn udp_server(launcher: Launcher) -> StoppedUdpServer {
-    UdpServer::new(launcher)
-}