Skip to main content

torrust_tracker_deployer_lib/domain/tracker/config/
mod.rs

1//! Tracker configuration domain types
2//!
3//! This module contains the main tracker configuration and component types
4//! used for deploying the Torrust Tracker.
5
6use std::collections::HashMap;
7use std::fmt;
8use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
9
10use serde::{Deserialize, Serialize};
11
12use super::{BindingAddress, Protocol};
13use crate::domain::topology::{
14    EnabledServices, Network, NetworkDerivation, PortBinding, PortDerivation, Service,
15};
16use crate::shared::docker_image::DockerImage;
17use crate::shared::DomainName;
18
19/// Docker image repository for the Torrust Tracker container
20pub const TRACKER_DOCKER_IMAGE_REPOSITORY: &str = "torrust/tracker";
21
22/// Docker image tag for the Torrust Tracker container
23pub const TRACKER_DOCKER_IMAGE_TAG: &str = "develop";
24
25mod core;
26mod health_check_api;
27mod http;
28mod http_api;
29mod udp;
30
31pub use core::{
32    DatabaseConfig, MysqlConfig, MysqlConfigError, SqliteConfig, SqliteConfigError,
33    TrackerCoreConfig,
34};
35pub use health_check_api::{HealthCheckApiConfig, HealthCheckApiConfigError};
36pub use http::{HttpTrackerConfig, HttpTrackerConfigError};
37pub use http_api::{HttpApiConfig, HttpApiConfigError};
38pub use udp::{UdpTrackerConfig, UdpTrackerConfigError};
39
40/// Checks if a socket address is bound to localhost (127.0.0.1 or `::1`).
41///
42/// This is used to validate that TLS-enabled services don't bind to localhost,
43/// since Caddy runs in a separate container and cannot reach localhost addresses.
44///
45/// # Returns
46///
47/// `true` if the address is IPv4 localhost (127.0.0.1) or IPv6 localhost (`::1`),
48/// `false` otherwise.
49///
50/// # Note
51///
52/// This intentionally checks only exact localhost addresses (127.0.0.1 and `::1`),
53/// not the entire 127.0.0.0/8 loopback range, as per design decision.
54#[must_use]
55pub fn is_localhost(addr: &SocketAddr) -> bool {
56    match addr.ip() {
57        IpAddr::V4(ipv4) => ipv4 == Ipv4Addr::LOCALHOST,
58        IpAddr::V6(ipv6) => ipv6 == Ipv6Addr::LOCALHOST,
59    }
60}
61
62/// Tracker deployment configuration
63///
64/// This structure mirrors the real tracker configuration but only includes
65/// user-configurable fields that are exposed via the environment.json file.
66///
67/// # Examples
68///
69/// ```rust
70/// use torrust_tracker_deployer_lib::domain::tracker::{
71///     TrackerConfig, TrackerCoreConfig, DatabaseConfig, SqliteConfig,
72///     UdpTrackerConfig, HttpTrackerConfig, HttpApiConfig, HealthCheckApiConfig
73/// };
74///
75/// let tracker_config = TrackerConfig::new(
76///     TrackerCoreConfig::new(
77///         DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
78///         false,
79///     ),
80///     vec![UdpTrackerConfig::new("0.0.0.0:6969".parse().unwrap(), None).unwrap()],
81///     vec![HttpTrackerConfig::new("0.0.0.0:7070".parse().unwrap(), None, false).unwrap()],
82///     HttpApiConfig::new(
83///         "0.0.0.0:1212".parse().unwrap(),
84///         "MyAccessToken".to_string().into(),
85///         None,
86///         false,
87///     ).expect("valid config"),
88///     HealthCheckApiConfig::new(
89///         "127.0.0.1:1313".parse().unwrap(),
90///         None,
91///         false,
92///     ).expect("valid config"),
93/// ).expect("valid config");
94/// ```
95#[derive(Debug, Clone, Serialize, PartialEq)]
96pub struct TrackerConfig {
97    /// Core tracker configuration
98    core: TrackerCoreConfig,
99
100    /// UDP tracker instances
101    udp_trackers: Vec<UdpTrackerConfig>,
102
103    /// HTTP tracker instances
104    http_trackers: Vec<HttpTrackerConfig>,
105
106    /// HTTP API configuration
107    http_api: HttpApiConfig,
108
109    /// Health Check API configuration
110    health_check_api: HealthCheckApiConfig,
111}
112
113/// Error type for tracker configuration validation failures
114#[derive(Debug, Clone, PartialEq)]
115pub enum TrackerConfigError {
116    /// Multiple services attempting to bind to the same socket address
117    DuplicateSocketAddress {
118        /// The conflicting socket address
119        address: SocketAddr,
120        /// The protocol (UDP or TCP)
121        protocol: Protocol,
122        /// Names of services attempting to bind to this address
123        services: Vec<String>,
124    },
125}
126
127impl fmt::Display for TrackerConfigError {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Self::DuplicateSocketAddress {
131                address,
132                protocol,
133                services,
134            } => {
135                let services_list = services
136                    .iter()
137                    .map(|s| format!("'{s}'"))
138                    .collect::<Vec<_>>()
139                    .join(", ");
140                write!(
141                    f,
142                    "Socket address conflict: {services_list} cannot bind to {address} ({protocol})\n\
143                    Tip: Assign different port numbers to each service"
144                )
145            }
146        }
147    }
148}
149
150impl std::error::Error for TrackerConfigError {}
151
152impl TrackerConfigError {
153    /// Get detailed troubleshooting guidance for this error
154    ///
155    /// This method provides comprehensive troubleshooting steps that can be
156    /// displayed to users when they need more help resolving the error.
157    #[must_use]
158    pub fn help(&self) -> String {
159        match self {
160            Self::DuplicateSocketAddress {
161                address,
162                protocol,
163                services,
164            } => {
165                use std::fmt::Write;
166
167                let mut help =
168                    String::from("Socket Address Conflict - Detailed Troubleshooting:\n\n");
169
170                help.push_str("Conflicting services:\n");
171                for service in services {
172                    let _ = writeln!(help, "  - {service}: {address} ({protocol})");
173                }
174                help.push('\n');
175
176                help.push_str("Why this fails:\n");
177                let _ = write!(
178                    help,
179                    "Two services using the same protocol ({protocol}) cannot bind to the same\n\
180                    IP address and port number. The second service will fail with\n\
181                    \"Address already in use\" error.\n\n"
182                );
183
184                help.push_str("How to fix:\n");
185                help.push_str(
186                    "1. Assign different port numbers to each service\n\
187                    2. Or configure only one service to use this address\n\n",
188                );
189
190                help.push_str("Note:\n");
191                help.push_str(
192                    "Services using different protocols (UDP vs TCP) CAN share the same port.\n\
193                    See: docs/external-issues/tracker/udp-tcp-port-sharing-allowed.md\n",
194                );
195
196                help
197            }
198        }
199    }
200}
201
202impl TrackerConfig {
203    /// Creates a new `TrackerConfig` with validated aggregate invariants.
204    ///
205    /// This constructor validates that no socket address conflicts exist
206    /// (multiple services binding to the same IP:port:protocol combination).
207    ///
208    /// # Errors
209    ///
210    /// Returns `TrackerConfigError::DuplicateSocketAddress` if multiple services
211    /// using the same protocol attempt to bind to the same socket address.
212    ///
213    /// # Note
214    ///
215    /// Individual component validation (port != 0, TLS requires domain, localhost
216    /// cannot use TLS) is enforced by the child config types at their construction
217    /// time. This constructor only validates aggregate-level invariants.
218    ///
219    /// # Examples
220    ///
221    /// ```rust
222    /// use torrust_tracker_deployer_lib::domain::tracker::{
223    ///     TrackerConfig, TrackerCoreConfig, DatabaseConfig, SqliteConfig,
224    ///     UdpTrackerConfig, HttpTrackerConfig, HttpApiConfig, HealthCheckApiConfig
225    /// };
226    ///
227    /// let config = TrackerConfig::new(
228    ///     TrackerCoreConfig::new(
229    ///         DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
230    ///         false,
231    ///     ),
232    ///     vec![UdpTrackerConfig::new("0.0.0.0:6969".parse().unwrap(), None).unwrap()],
233    ///     vec![HttpTrackerConfig::new("0.0.0.0:7070".parse().unwrap(), None, false).unwrap()],
234    ///     HttpApiConfig::new(
235    ///         "0.0.0.0:1212".parse().unwrap(),
236    ///         "MyAccessToken".to_string().into(),
237    ///         None,
238    ///         false,
239    ///     ).unwrap(),
240    ///     HealthCheckApiConfig::new(
241    ///         "127.0.0.1:1313".parse().unwrap(),
242    ///         None,
243    ///         false,
244    ///     ).unwrap(),
245    /// ).expect("valid config");
246    /// ```
247    pub fn new(
248        core: TrackerCoreConfig,
249        udp_trackers: Vec<UdpTrackerConfig>,
250        http_trackers: Vec<HttpTrackerConfig>,
251        http_api: HttpApiConfig,
252        health_check_api: HealthCheckApiConfig,
253    ) -> Result<Self, TrackerConfigError> {
254        let config = Self {
255            core,
256            udp_trackers,
257            http_trackers,
258            http_api,
259            health_check_api,
260        };
261
262        // Validate aggregate-level invariants
263        // (Child components are already validated at their construction)
264        config.check_socket_address_conflicts()?;
265
266        Ok(config)
267    }
268
269    /// Returns the core tracker configuration.
270    #[must_use]
271    pub fn core(&self) -> &TrackerCoreConfig {
272        &self.core
273    }
274
275    /// Returns the UDP tracker configurations.
276    #[must_use]
277    pub fn udp_trackers(&self) -> &[UdpTrackerConfig] {
278        &self.udp_trackers
279    }
280
281    /// Returns the HTTP tracker configurations.
282    #[must_use]
283    pub fn http_trackers(&self) -> &[HttpTrackerConfig] {
284        &self.http_trackers
285    }
286
287    /// Returns the HTTP API configuration.
288    #[must_use]
289    pub fn http_api(&self) -> &HttpApiConfig {
290        &self.http_api
291    }
292
293    /// Returns the Health Check API configuration.
294    #[must_use]
295    pub fn health_check_api(&self) -> &HealthCheckApiConfig {
296        &self.health_check_api
297    }
298
299    /// Returns whether the tracker is configured to use `MySQL` database.
300    ///
301    /// This is useful for determining if MySQL-related infrastructure
302    /// (like storage directories) needs to be created.
303    ///
304    /// # Examples
305    ///
306    /// ```rust
307    /// use torrust_tracker_deployer_lib::domain::tracker::{
308    ///     TrackerConfig, TrackerCoreConfig, DatabaseConfig, SqliteConfig,
309    ///     UdpTrackerConfig, HttpTrackerConfig, HttpApiConfig, HealthCheckApiConfig
310    /// };
311    ///
312    /// let tracker_config = TrackerConfig::new(
313    ///     TrackerCoreConfig::new(
314    ///         DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
315    ///         false,
316    ///     ),
317    ///     vec![UdpTrackerConfig::new("0.0.0.0:6969".parse().unwrap(), None).unwrap()],
318    ///     vec![HttpTrackerConfig::new("0.0.0.0:7070".parse().unwrap(), None, false).unwrap()],
319    ///     HttpApiConfig::new(
320    ///         "0.0.0.0:1212".parse().unwrap(),
321    ///         "MyAccessToken".to_string().into(),
322    ///         None,
323    ///         false,
324    ///     ).expect("valid config"),
325    ///     HealthCheckApiConfig::new(
326    ///         "127.0.0.1:1313".parse().unwrap(),
327    ///         None,
328    ///         false,
329    ///     ).expect("valid config"),
330    /// ).expect("valid config");
331    ///
332    /// // SQLite config -> not MySQL
333    /// assert!(!tracker_config.uses_mysql());
334    /// ```
335    #[must_use]
336    pub fn uses_mysql(&self) -> bool {
337        matches!(self.core.database(), DatabaseConfig::Mysql(_))
338    }
339
340    /// Returns the Docker image used for the tracker service.
341    ///
342    /// This is a pinned constant — not user-configurable.
343    ///
344    /// # Examples
345    ///
346    /// ```rust
347    /// use torrust_tracker_deployer_lib::domain::tracker::TrackerConfig;
348    ///
349    /// let image = TrackerConfig::docker_image();
350    /// assert_eq!(image.full_reference(), "torrust/tracker:develop");
351    /// ```
352    #[must_use]
353    pub fn docker_image() -> DockerImage {
354        DockerImage::new(TRACKER_DOCKER_IMAGE_REPOSITORY, TRACKER_DOCKER_IMAGE_TAG)
355    }
356
357    /// Checks for socket address conflicts
358    ///
359    /// Validates that no two services using the same protocol attempt to bind
360    /// to the same socket address (IP + port).
361    fn check_socket_address_conflicts(&self) -> Result<(), TrackerConfigError> {
362        let bindings = self.collect_bindings();
363        Self::check_for_conflicts(bindings)
364    }
365
366    /// Checks for socket address conflicts in the collected bindings
367    ///
368    /// Examines the binding map to find any addresses that have multiple
369    /// services attempting to use them with the same protocol.
370    ///
371    /// # Errors
372    ///
373    /// Returns `TrackerConfigError::DuplicateSocketAddress` if any binding
374    /// address is shared by multiple services.
375    fn check_for_conflicts(
376        bindings: HashMap<BindingAddress, Vec<String>>,
377    ) -> Result<(), TrackerConfigError> {
378        for (binding, services) in bindings {
379            if services.len() > 1 {
380                return Err(TrackerConfigError::DuplicateSocketAddress {
381                    address: *binding.socket(),
382                    protocol: binding.protocol(),
383                    services,
384                });
385            }
386        }
387
388        Ok(())
389    }
390
391    /// Collects all binding addresses with their service names
392    ///
393    /// Creates a map of binding addresses (socket + protocol) to service names.
394    /// This allows identifying which services are attempting to bind to the same
395    /// socket address with the same protocol.
396    fn collect_bindings(&self) -> HashMap<BindingAddress, Vec<String>> {
397        let mut bindings: HashMap<BindingAddress, Vec<String>> = HashMap::new();
398
399        // Add UDP trackers
400        Self::register_trackers(
401            &mut bindings,
402            &self.udp_trackers,
403            Protocol::Udp,
404            "UDP Tracker",
405        );
406
407        // Add HTTP trackers
408        Self::register_trackers(
409            &mut bindings,
410            &self.http_trackers,
411            Protocol::Tcp,
412            "HTTP Tracker",
413        );
414
415        // Add HTTP API
416        Self::register_binding(
417            &mut bindings,
418            self.http_api.bind_address(),
419            Protocol::Tcp,
420            "HTTP API",
421        );
422
423        // Add Health Check API
424        Self::register_binding(
425            &mut bindings,
426            self.health_check_api.bind_address(),
427            Protocol::Tcp,
428            "Health Check API",
429        );
430
431        bindings
432    }
433
434    /// Registers multiple tracker instances in the bindings map
435    ///
436    /// Creates numbered service names for each tracker instance (e.g., "UDP Tracker #1").
437    fn register_trackers<T>(
438        bindings: &mut HashMap<BindingAddress, Vec<String>>,
439        trackers: &[T],
440        protocol: Protocol,
441        service_name: &str,
442    ) where
443        T: HasBindAddress,
444    {
445        for (i, tracker) in trackers.iter().enumerate() {
446            let service_label = format!("{service_name} #{}", i + 1);
447            Self::register_binding(bindings, tracker.bind_address(), protocol, &service_label);
448        }
449    }
450
451    /// Registers a single binding in the bindings map
452    ///
453    /// Associates the given service name with the socket address and protocol.
454    fn register_binding(
455        bindings: &mut HashMap<BindingAddress, Vec<String>>,
456        address: SocketAddr,
457        protocol: Protocol,
458        service_name: &str,
459    ) {
460        let binding = BindingAddress::new(address, protocol);
461        bindings
462            .entry(binding)
463            .or_default()
464            .push(service_name.to_string());
465    }
466
467    /// Returns the HTTP API TLS domain if configured
468    #[must_use]
469    pub fn http_api_tls_domain(&self) -> Option<&str> {
470        self.http_api.tls_domain().map(DomainName::as_str)
471    }
472
473    /// Returns the HTTP API port number
474    #[must_use]
475    pub fn http_api_port(&self) -> u16 {
476        self.http_api.bind_address().port()
477    }
478
479    /// Returns the Health Check API TLS domain if configured
480    #[must_use]
481    pub fn health_check_api_tls_domain(&self) -> Option<&str> {
482        self.health_check_api.tls_domain()
483    }
484
485    /// Returns the Health Check API port number
486    #[must_use]
487    pub fn health_check_api_port(&self) -> u16 {
488        self.health_check_api.bind_address().port()
489    }
490
491    /// Returns HTTP trackers that have TLS proxy enabled
492    ///
493    /// Returns a vector of tuples containing (domain, port) for each
494    /// HTTP tracker that has `use_tls_proxy: true` and a domain configured.
495    #[must_use]
496    pub fn http_trackers_with_tls(&self) -> Vec<(&str, u16)> {
497        self.http_trackers
498            .iter()
499            .filter(|tracker| tracker.use_tls_proxy())
500            .filter_map(|tracker| {
501                tracker
502                    .domain()
503                    .map(|domain| (domain.as_str(), tracker.bind_address().port()))
504            })
505            .collect()
506    }
507
508    /// Returns true if any HTTP tracker has `use_tls_proxy: true`
509    ///
510    /// This is used to determine if the tracker's global `on_reverse_proxy`
511    /// setting should be enabled in the tracker configuration template.
512    #[must_use]
513    pub fn any_http_tracker_uses_tls_proxy(&self) -> bool {
514        self.http_trackers
515            .iter()
516            .any(http::HttpTrackerConfig::use_tls_proxy)
517    }
518
519    /// Returns true if any service has TLS proxy configured
520    ///
521    /// Checks if at least one of the following services has TLS enabled:
522    /// - HTTP API (`use_tls_proxy: true`)
523    /// - Any HTTP tracker (`use_tls_proxy: true`)
524    /// - Health Check API (`use_tls_proxy: true`)
525    ///
526    /// This is used for cross-service validation to ensure that when the HTTPS
527    /// section is defined, at least one service actually uses TLS.
528    #[must_use]
529    pub fn has_any_tls_configured(&self) -> bool {
530        self.http_api.use_tls_proxy()
531            || self
532                .http_trackers
533                .iter()
534                .any(http::HttpTrackerConfig::use_tls_proxy)
535            || self.health_check_api.use_tls_proxy()
536    }
537}
538
539impl PortDerivation for TrackerConfig {
540    /// Derives port bindings for the Tracker service
541    ///
542    /// Implements PORT-01 through PORT-06:
543    /// - PORT-01: Tracker needs ports if UDP OR HTTP without TLS OR API without TLS
544    /// - PORT-02: UDP ports always exposed (UDP doesn't use TLS)
545    /// - PORT-03: HTTP ports WITHOUT TLS exposed directly
546    /// - PORT-04: HTTP ports WITH TLS NOT exposed (Caddy handles)
547    /// - PORT-05: API port exposed only when no TLS
548    /// - PORT-06: API port NOT exposed when TLS
549    fn derive_ports(&self) -> Vec<PortBinding> {
550        let mut ports = Vec::new();
551
552        // PORT-02: UDP ports always exposed (UDP doesn't use TLS)
553        for udp_tracker in &self.udp_trackers {
554            ports.push(PortBinding::udp(
555                udp_tracker.bind_address().port(),
556                "BitTorrent UDP announce",
557            ));
558        }
559
560        // PORT-03: HTTP ports WITHOUT TLS exposed directly
561        // PORT-04: HTTP ports WITH TLS NOT exposed (Caddy handles)
562        for http_tracker in &self.http_trackers {
563            if !http_tracker.use_tls_proxy() {
564                ports.push(PortBinding::tcp(
565                    http_tracker.bind_address().port(),
566                    "HTTP tracker announce",
567                ));
568            }
569        }
570
571        // PORT-05: API exposed only when no TLS
572        // PORT-06: API NOT exposed when TLS
573        if !self.http_api.use_tls_proxy() {
574            ports.push(PortBinding::tcp(
575                self.http_api.bind_address().port(),
576                "HTTP API (stats/whitelist)",
577            ));
578        }
579
580        ports
581    }
582}
583
584impl NetworkDerivation for TrackerConfig {
585    /// Derives network assignments for the Tracker service
586    ///
587    /// Implements NET-01 through NET-03:
588    /// - NET-01: Metrics network if Prometheus enabled
589    /// - NET-02: Database network if `MySQL` enabled
590    /// - NET-03: Proxy network if Caddy enabled
591    fn derive_networks(&self, enabled_services: &EnabledServices) -> Vec<Network> {
592        let mut networks = Vec::new();
593
594        // NET-01: Metrics network if Prometheus enabled
595        if enabled_services.has(Service::Prometheus) {
596            networks.push(Network::Metrics);
597        }
598
599        // NET-02: Database network if MySQL enabled
600        if enabled_services.has(Service::MySQL) {
601            networks.push(Network::Database);
602        }
603
604        // NET-03: Proxy network if Caddy enabled
605        if enabled_services.has(Service::Caddy) {
606            networks.push(Network::Proxy);
607        }
608
609        networks
610    }
611}
612
613/// Trait for types that have a bind address
614///
615/// Used for generic tracker registration in validation logic.
616trait HasBindAddress {
617    /// Returns the socket address this service binds to
618    fn bind_address(&self) -> SocketAddr;
619}
620
621impl HasBindAddress for UdpTrackerConfig {
622    fn bind_address(&self) -> SocketAddr {
623        UdpTrackerConfig::bind_address(self)
624    }
625}
626
627impl HasBindAddress for HttpTrackerConfig {
628    fn bind_address(&self) -> SocketAddr {
629        HttpTrackerConfig::bind_address(self)
630    }
631}
632
633impl Default for TrackerConfig {
634    /// Returns a default tracker configuration suitable for development and testing
635    ///
636    /// # Default Values
637    ///
638    /// - Database: `SQLite` with filename "tracker.db"
639    /// - Mode: Public tracker (private = false)
640    /// - UDP trackers: One instance on port 6969
641    /// - HTTP trackers: One instance on port 7070
642    /// - HTTP API: Bind address 0.0.0.0:1212
643    /// - Admin token: `MyAccessToken`
644    fn default() -> Self {
645        Self::new(
646            TrackerCoreConfig::new(
647                DatabaseConfig::Sqlite(
648                    SqliteConfig::new("tracker.db").expect("default sqlite config is valid"),
649                ),
650                false,
651            ),
652            vec![
653                UdpTrackerConfig::new("0.0.0.0:6969".parse().expect("valid address"), None)
654                    .expect("default UdpTrackerConfig values are always valid"),
655            ],
656            vec![HttpTrackerConfig::new(
657                "0.0.0.0:7070".parse().expect("valid address"),
658                None,
659                false,
660            )
661            .expect("default HttpTrackerConfig values are always valid")],
662            HttpApiConfig::new(
663                "0.0.0.0:1212".parse().expect("valid address"),
664                "MyAccessToken".to_string().into(),
665                None,
666                false,
667            )
668            .expect("default HttpApiConfig values are always valid"),
669            HealthCheckApiConfig::new(
670                "127.0.0.1:1313".parse().expect("valid address"),
671                None,
672                false,
673            )
674            .expect("default HealthCheckApiConfig values are always valid"),
675        )
676        .expect("default TrackerConfig values have no socket address conflicts")
677    }
678}
679
680pub(crate) fn serialize_socket_addr<S>(addr: &SocketAddr, serializer: S) -> Result<S::Ok, S::Error>
681where
682    S: serde::Serializer,
683{
684    serializer.serialize_str(&addr.to_string())
685}
686
687pub(crate) fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
688where
689    D: serde::Deserializer<'de>,
690{
691    let s = String::deserialize(deserializer)?;
692    s.parse().map_err(serde::de::Error::custom)
693}
694
695/// Raw struct for deserializing `TrackerConfig` before validation.
696#[derive(Deserialize)]
697struct TrackerConfigRaw {
698    core: TrackerCoreConfig,
699    udp_trackers: Vec<UdpTrackerConfig>,
700    http_trackers: Vec<HttpTrackerConfig>,
701    http_api: HttpApiConfig,
702    health_check_api: HealthCheckApiConfig,
703}
704
705impl<'de> Deserialize<'de> for TrackerConfig {
706    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
707    where
708        D: serde::Deserializer<'de>,
709    {
710        let raw = TrackerConfigRaw::deserialize(deserializer)?;
711        TrackerConfig::new(
712            raw.core,
713            raw.udp_trackers,
714            raw.http_trackers,
715            raw.http_api,
716            raw.health_check_api,
717        )
718        .map_err(serde::de::Error::custom)
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    /// Test helper to create a `UdpTrackerConfig` with default values.
727    /// Uses the validated constructor, making tests more realistic.
728    fn test_udp_tracker_config(bind_address: &str) -> UdpTrackerConfig {
729        UdpTrackerConfig::new(bind_address.parse().expect("valid address"), None)
730            .expect("test values should be valid")
731    }
732
733    /// Test helper to create a `UdpTrackerConfig` with domain.
734    #[allow(dead_code)]
735    fn test_udp_tracker_config_with_domain(
736        bind_address: &str,
737        domain: DomainName,
738    ) -> UdpTrackerConfig {
739        UdpTrackerConfig::new(bind_address.parse().expect("valid address"), Some(domain))
740            .expect("test values should be valid")
741    }
742
743    /// Test helper to create an `HttpTrackerConfig` with default values (no TLS).
744    fn test_http_tracker_config(bind_address: &str) -> HttpTrackerConfig {
745        HttpTrackerConfig::new(bind_address.parse().expect("valid address"), None, false)
746            .expect("test values should be valid")
747    }
748
749    /// Test helper with TLS options for HTTP trackers.
750    #[allow(dead_code)]
751    fn test_http_tracker_config_with_tls(
752        bind_address: &str,
753        domain: Option<DomainName>,
754        use_tls_proxy: bool,
755    ) -> HttpTrackerConfig {
756        HttpTrackerConfig::new(
757            bind_address.parse().expect("valid address"),
758            domain,
759            use_tls_proxy,
760        )
761        .expect("test values should be valid")
762    }
763
764    /// Test helper to create a `HealthCheckApiConfig` with default values (no TLS).
765    fn test_health_check_api_config(bind_address: &str) -> HealthCheckApiConfig {
766        HealthCheckApiConfig::new(bind_address.parse().expect("valid address"), None, false)
767            .expect("test values should be valid")
768    }
769
770    /// Test helper with TLS options for Health Check API.
771    #[allow(dead_code)]
772    fn test_health_check_api_config_with_tls(
773        bind_address: &str,
774        domain: Option<DomainName>,
775        use_tls_proxy: bool,
776    ) -> HealthCheckApiConfig {
777        HealthCheckApiConfig::new(
778            bind_address.parse().expect("valid address"),
779            domain,
780            use_tls_proxy,
781        )
782        .expect("test values should be valid")
783    }
784
785    /// Test helper to create an `HttpApiConfig` with default or custom values.
786    /// Uses the validated constructor, making tests more realistic.
787    fn test_http_api_config(bind_address: &str, admin_token: &str) -> HttpApiConfig {
788        HttpApiConfig::new(
789            bind_address.parse().expect("valid address"),
790            admin_token.to_string().into(),
791            None,
792            false,
793        )
794        .expect("test values should be valid")
795    }
796
797    /// Test helper with TLS options
798    fn test_http_api_config_with_tls(
799        bind_address: &str,
800        admin_token: &str,
801        domain: Option<DomainName>,
802        use_tls_proxy: bool,
803    ) -> HttpApiConfig {
804        HttpApiConfig::new(
805            bind_address.parse().expect("valid address"),
806            admin_token.to_string().into(),
807            domain,
808            use_tls_proxy,
809        )
810        .expect("test values should be valid")
811    }
812
813    /// Test helper to create a `TrackerConfig` with default values.
814    /// Uses the validated constructor, making tests more realistic.
815    fn test_tracker_config(
816        udp_trackers: Vec<UdpTrackerConfig>,
817        http_trackers: Vec<HttpTrackerConfig>,
818        http_api: HttpApiConfig,
819        health_check_api: HealthCheckApiConfig,
820    ) -> TrackerConfig {
821        TrackerConfig::new(
822            TrackerCoreConfig::new(
823                DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
824                false,
825            ),
826            udp_trackers,
827            http_trackers,
828            http_api,
829            health_check_api,
830        )
831        .expect("test values should be valid")
832    }
833
834    /// Test helper to create a `TrackerConfig` with custom core config.
835    fn test_tracker_config_with_core(
836        core: TrackerCoreConfig,
837        udp_trackers: Vec<UdpTrackerConfig>,
838        http_trackers: Vec<HttpTrackerConfig>,
839        http_api: HttpApiConfig,
840        health_check_api: HealthCheckApiConfig,
841    ) -> TrackerConfig {
842        TrackerConfig::new(
843            core,
844            udp_trackers,
845            http_trackers,
846            http_api,
847            health_check_api,
848        )
849        .expect("test values should be valid")
850    }
851
852    /// Test helper to create a private core config (`SQLite`)
853    fn test_private_core_config() -> TrackerCoreConfig {
854        TrackerCoreConfig::new(
855            DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
856            true,
857        )
858    }
859
860    /// Test helper to create a core config with custom database name
861    fn test_core_config_with_db(database_name: &str) -> TrackerCoreConfig {
862        TrackerCoreConfig::new(
863            DatabaseConfig::Sqlite(SqliteConfig::new(database_name).unwrap()),
864            false,
865        )
866    }
867
868    mod is_localhost_tests {
869        use super::*;
870
871        #[test]
872        fn it_should_detect_ipv4_localhost() {
873            let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
874            assert!(is_localhost(&addr));
875        }
876
877        #[test]
878        fn it_should_detect_ipv6_localhost() {
879            let addr: SocketAddr = "[::1]:8080".parse().unwrap();
880            assert!(is_localhost(&addr));
881        }
882
883        #[test]
884        fn it_should_not_detect_all_interfaces_ipv4() {
885            let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
886            assert!(!is_localhost(&addr));
887        }
888
889        #[test]
890        fn it_should_not_detect_all_interfaces_ipv6() {
891            let addr: SocketAddr = "[::]:8080".parse().unwrap();
892            assert!(!is_localhost(&addr));
893        }
894
895        #[test]
896        fn it_should_not_detect_specific_ip() {
897            let addr: SocketAddr = "10.0.0.1:8080".parse().unwrap();
898            assert!(!is_localhost(&addr));
899        }
900
901        #[test]
902        fn it_should_not_detect_other_127_x_addresses() {
903            // Only 127.0.0.1 is considered localhost, not the entire 127.0.0.0/8 range
904            let addr: SocketAddr = "127.0.0.2:8080".parse().unwrap();
905            assert!(!is_localhost(&addr));
906        }
907    }
908
909    #[test]
910    fn it_should_create_tracker_config() {
911        let config = test_tracker_config_with_core(
912            test_private_core_config(),
913            vec![test_udp_tracker_config("0.0.0.0:6868")],
914            vec![test_http_tracker_config("0.0.0.0:7070")],
915            test_http_api_config("0.0.0.0:1212", "test_token"),
916            test_health_check_api_config("127.0.0.1:1313"),
917        );
918
919        assert_eq!(config.core().database().database_name(), "tracker.db");
920        assert!(config.core().private());
921        assert_eq!(config.udp_trackers().len(), 1);
922        assert_eq!(config.http_trackers().len(), 1);
923    }
924
925    #[test]
926    fn it_should_serialize_tracker_config() {
927        let config = test_tracker_config_with_core(
928            test_core_config_with_db("test.db"),
929            vec![],
930            vec![],
931            test_http_api_config("0.0.0.0:1212", "token123"),
932            test_health_check_api_config("127.0.0.1:1313"),
933        );
934
935        let json = serde_json::to_value(&config).unwrap();
936        assert_eq!(json["core"]["private"], false);
937        assert_eq!(json["http_api"]["admin_token"], "token123");
938    }
939
940    #[test]
941    fn it_should_create_default_tracker_config() {
942        let config = TrackerConfig::default();
943
944        // Verify default database configuration
945        assert_eq!(config.core().database().database_name(), "tracker.db");
946        assert_eq!(config.core().database().driver_name(), "sqlite3");
947
948        // Verify public tracker mode
949        assert!(!config.core().private());
950
951        // Verify UDP trackers (1 instance)
952        assert_eq!(config.udp_trackers().len(), 1);
953        assert_eq!(
954            config.udp_trackers()[0].bind_address(),
955            "0.0.0.0:6969".parse::<SocketAddr>().unwrap()
956        );
957
958        // Verify HTTP trackers (1 instance)
959        assert_eq!(config.http_trackers().len(), 1);
960        assert_eq!(
961            config.http_trackers()[0].bind_address(),
962            "0.0.0.0:7070".parse::<SocketAddr>().unwrap()
963        );
964
965        // Verify HTTP API configuration
966        assert_eq!(
967            config.http_api().bind_address(),
968            "0.0.0.0:1212".parse::<SocketAddr>().unwrap()
969        );
970        assert_eq!(
971            config.http_api().admin_token().expose_secret(),
972            "MyAccessToken"
973        );
974    }
975
976    mod validation {
977        use super::*;
978
979        #[test]
980        fn it_should_accept_valid_configuration_with_unique_addresses() {
981            let result = TrackerConfig::new(
982                TrackerCoreConfig::new(
983                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
984                    false,
985                ),
986                vec![test_udp_tracker_config("0.0.0.0:6969")],
987                vec![test_http_tracker_config("0.0.0.0:7070")],
988                test_http_api_config("0.0.0.0:1212", "token"),
989                test_health_check_api_config("127.0.0.1:1313"),
990            );
991
992            assert!(result.is_ok());
993        }
994
995        #[test]
996        fn it_should_reject_duplicate_udp_tracker_ports() {
997            let result = TrackerConfig::new(
998                TrackerCoreConfig::new(
999                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1000                    false,
1001                ),
1002                vec![
1003                    test_udp_tracker_config("0.0.0.0:7070"),
1004                    test_udp_tracker_config("0.0.0.0:7070"),
1005                ],
1006                vec![],
1007                test_http_api_config("0.0.0.0:1212", "token"),
1008                test_health_check_api_config("127.0.0.1:1313"),
1009            );
1010
1011            assert!(result.is_err());
1012
1013            if let Err(TrackerConfigError::DuplicateSocketAddress {
1014                address,
1015                protocol,
1016                services,
1017            }) = result
1018            {
1019                assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1020                assert_eq!(protocol, Protocol::Udp);
1021                assert_eq!(services.len(), 2);
1022                assert!(services.contains(&"UDP Tracker #1".to_string()));
1023                assert!(services.contains(&"UDP Tracker #2".to_string()));
1024            } else {
1025                panic!("Expected DuplicateSocketAddress error");
1026            }
1027        }
1028
1029        #[test]
1030        fn it_should_reject_duplicate_http_tracker_ports() {
1031            let result = TrackerConfig::new(
1032                TrackerCoreConfig::new(
1033                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1034                    false,
1035                ),
1036                vec![],
1037                vec![
1038                    test_http_tracker_config("0.0.0.0:7070"),
1039                    test_http_tracker_config("0.0.0.0:7070"),
1040                ],
1041                test_http_api_config("0.0.0.0:1212", "token"),
1042                test_health_check_api_config("127.0.0.1:1313"),
1043            );
1044
1045            assert!(result.is_err());
1046
1047            if let Err(TrackerConfigError::DuplicateSocketAddress {
1048                address,
1049                protocol,
1050                services,
1051            }) = result
1052            {
1053                assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1054                assert_eq!(protocol, Protocol::Tcp);
1055                assert_eq!(services.len(), 2);
1056            } else {
1057                panic!("Expected DuplicateSocketAddress error");
1058            }
1059        }
1060
1061        #[test]
1062        fn it_should_reject_http_tracker_and_api_conflict() {
1063            let result = TrackerConfig::new(
1064                TrackerCoreConfig::new(
1065                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1066                    false,
1067                ),
1068                vec![],
1069                vec![test_http_tracker_config("0.0.0.0:7070")],
1070                test_http_api_config("0.0.0.0:7070", "token"),
1071                test_health_check_api_config("127.0.0.1:1313"),
1072            );
1073
1074            assert!(result.is_err());
1075
1076            if let Err(TrackerConfigError::DuplicateSocketAddress {
1077                address,
1078                protocol,
1079                services,
1080            }) = result
1081            {
1082                assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1083                assert_eq!(protocol, Protocol::Tcp);
1084                assert_eq!(services.len(), 2);
1085                assert!(services.contains(&"HTTP Tracker #1".to_string()));
1086                assert!(services.contains(&"HTTP API".to_string()));
1087            } else {
1088                panic!("Expected DuplicateSocketAddress error");
1089            }
1090        }
1091
1092        #[test]
1093        fn it_should_reject_http_tracker_and_health_check_api_conflict() {
1094            let result = TrackerConfig::new(
1095                TrackerCoreConfig::new(
1096                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1097                    false,
1098                ),
1099                vec![],
1100                vec![test_http_tracker_config("0.0.0.0:1313")],
1101                test_http_api_config("0.0.0.0:1212", "token"),
1102                test_health_check_api_config("0.0.0.0:1313"),
1103            );
1104
1105            assert!(result.is_err());
1106
1107            if let Err(TrackerConfigError::DuplicateSocketAddress {
1108                address,
1109                protocol,
1110                services,
1111            }) = result
1112            {
1113                assert_eq!(address, "0.0.0.0:1313".parse::<SocketAddr>().unwrap());
1114                assert_eq!(protocol, Protocol::Tcp);
1115                assert_eq!(services.len(), 2);
1116                assert!(services.contains(&"HTTP Tracker #1".to_string()));
1117                assert!(services.contains(&"Health Check API".to_string()));
1118            } else {
1119                panic!("Expected DuplicateSocketAddress error");
1120            }
1121        }
1122
1123        #[test]
1124        fn it_should_allow_udp_and_http_on_same_port() {
1125            // This is valid because UDP and TCP use separate port spaces
1126            let result = TrackerConfig::new(
1127                TrackerCoreConfig::new(
1128                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1129                    false,
1130                ),
1131                vec![test_udp_tracker_config("0.0.0.0:7070")],
1132                vec![test_http_tracker_config("0.0.0.0:7070")],
1133                test_http_api_config("0.0.0.0:1212", "token"),
1134                test_health_check_api_config("127.0.0.1:1313"),
1135            );
1136
1137            assert!(result.is_ok());
1138        }
1139
1140        #[test]
1141        fn it_should_allow_same_port_different_ips() {
1142            let result = TrackerConfig::new(
1143                TrackerCoreConfig::new(
1144                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1145                    false,
1146                ),
1147                vec![],
1148                vec![
1149                    test_http_tracker_config("192.168.1.10:7070"),
1150                    test_http_tracker_config("192.168.1.20:7070"),
1151                ],
1152                test_http_api_config("0.0.0.0:1212", "token"),
1153                test_health_check_api_config("127.0.0.1:1313"),
1154            );
1155
1156            assert!(result.is_ok());
1157        }
1158
1159        #[test]
1160        fn it_should_provide_clear_error_message_with_fix_instructions() {
1161            let result = TrackerConfig::new(
1162                TrackerCoreConfig::new(
1163                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1164                    false,
1165                ),
1166                vec![],
1167                vec![test_http_tracker_config("0.0.0.0:7070")],
1168                test_http_api_config("0.0.0.0:7070", "token"),
1169                test_health_check_api_config("127.0.0.1:1313"),
1170            );
1171
1172            let error = result.unwrap_err();
1173            let error_message = error.to_string();
1174
1175            // Verify brief error message contains essential information
1176            assert!(error_message.contains("Socket address conflict"));
1177            assert!(error_message.contains("'HTTP Tracker #1'"));
1178            assert!(error_message.contains("'HTTP API'"));
1179            assert!(error_message.contains("0.0.0.0:7070"));
1180            assert!(error_message.contains("TCP"));
1181            assert!(error_message.contains("Tip: Assign different port numbers"));
1182
1183            // Verify detailed help contains comprehensive troubleshooting
1184            let help = error.help();
1185            assert!(help.contains("Socket Address Conflict - Detailed Troubleshooting"));
1186            assert!(help.contains("Conflicting services:"));
1187            assert!(help.contains("HTTP Tracker #1"));
1188            assert!(help.contains("HTTP API"));
1189            assert!(help.contains("Why this fails:"));
1190            assert!(help.contains("How to fix:"));
1191            assert!(help.contains("docs/external-issues/tracker/udp-tcp-port-sharing-allowed.md"));
1192        }
1193    }
1194
1195    mod localhost_with_tls_validation {
1196        use super::*;
1197
1198        fn base_config() -> TrackerConfig {
1199            test_tracker_config(
1200                vec![],
1201                vec![],
1202                test_http_api_config("0.0.0.0:1212", "token"),
1203                test_health_check_api_config("127.0.0.1:1313"),
1204            )
1205        }
1206
1207        // NOTE: Tests for localhost + TLS rejection have been moved to the individual
1208        // config type tests (health_check_api.rs, http.rs, http_api.rs) because
1209        // validation is now enforced at construction time by their respective ::new()
1210        // methods. TrackerConfig::new() no longer needs to check for localhost + TLS
1211        // as it's impossible to construct invalid child configs.
1212
1213        #[test]
1214        fn it_should_allow_localhost_without_tls() {
1215            // base_config has http_api on 0.0.0.0 and health_check_api on 127.0.0.1 without TLS
1216            let config = base_config();
1217            // If we got here without error, the config is valid
1218            assert_eq!(config.http_api().bind_address().port(), 1212);
1219        }
1220
1221        #[test]
1222        fn it_should_allow_non_localhost_with_tls() {
1223            let domain = crate::shared::DomainName::new("api.tracker.local").unwrap();
1224            let config = TrackerConfig::new(
1225                TrackerCoreConfig::new(
1226                    DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1227                    false,
1228                ),
1229                vec![],
1230                vec![],
1231                test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(domain), true),
1232                test_health_check_api_config("127.0.0.1:1313"),
1233            )
1234            .expect("valid config");
1235
1236            assert!(config.http_api().use_tls_proxy());
1237        }
1238    }
1239
1240    // =========================================================================
1241    // Port derivation tests (PORT-01 through PORT-06)
1242    // =========================================================================
1243
1244    mod port_derivation {
1245        use super::*;
1246
1247        fn default_core() -> TrackerCoreConfig {
1248            TrackerCoreConfig::new(
1249                DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1250                false,
1251            )
1252        }
1253
1254        #[test]
1255        fn it_should_expose_udp_ports_always() {
1256            // PORT-02: UDP ports always exposed (UDP doesn't use TLS)
1257            let config = TrackerConfig::new(
1258                default_core(),
1259                vec![
1260                    test_udp_tracker_config("0.0.0.0:6969"),
1261                    test_udp_tracker_config("0.0.0.0:6868"),
1262                ],
1263                vec![],
1264                test_http_api_config("0.0.0.0:1212", "token"),
1265                test_health_check_api_config("127.0.0.1:1313"),
1266            )
1267            .unwrap();
1268
1269            let ports = config.derive_ports();
1270            let udp_ports: Vec<_> = ports
1271                .iter()
1272                .filter(|p| p.protocol() == Protocol::Udp)
1273                .collect();
1274
1275            assert_eq!(udp_ports.len(), 2);
1276            assert_eq!(udp_ports[0].host_port(), 6969);
1277            assert_eq!(udp_ports[1].host_port(), 6868);
1278        }
1279
1280        #[test]
1281        fn it_should_expose_http_ports_without_tls() {
1282            // PORT-03: HTTP ports WITHOUT TLS exposed directly
1283            let config = TrackerConfig::new(
1284                default_core(),
1285                vec![],
1286                vec![
1287                    test_http_tracker_config("0.0.0.0:7070"),
1288                    test_http_tracker_config("0.0.0.0:8080"),
1289                ],
1290                test_http_api_config("0.0.0.0:1212", "token"),
1291                test_health_check_api_config("127.0.0.1:1313"),
1292            )
1293            .unwrap();
1294
1295            let ports = config.derive_ports();
1296            // Should have 2 HTTP ports + 1 API port
1297            let tcp_ports: Vec<_> = ports
1298                .iter()
1299                .filter(|p| p.protocol() == Protocol::Tcp)
1300                .collect();
1301
1302            assert_eq!(tcp_ports.len(), 3);
1303            assert!(tcp_ports.iter().any(|p| p.host_port() == 7070));
1304            assert!(tcp_ports.iter().any(|p| p.host_port() == 8080));
1305        }
1306
1307        #[test]
1308        fn it_should_not_expose_http_ports_with_tls() {
1309            // PORT-04: HTTP ports WITH TLS NOT exposed (Caddy handles)
1310            let domain = crate::shared::DomainName::new("tracker.example.com").unwrap();
1311            let config = TrackerConfig::new(
1312                default_core(),
1313                vec![],
1314                vec![test_http_tracker_config_with_tls(
1315                    "0.0.0.0:7070",
1316                    Some(domain),
1317                    true,
1318                )],
1319                test_http_api_config("0.0.0.0:1212", "token"),
1320                test_health_check_api_config("127.0.0.1:1313"),
1321            )
1322            .unwrap();
1323
1324            let ports = config.derive_ports();
1325            // Should only have API port (7070 is hidden behind TLS)
1326            assert!(ports.iter().all(|p| p.host_port() != 7070));
1327        }
1328
1329        #[test]
1330        fn it_should_expose_api_port_without_tls() {
1331            // PORT-05: API exposed only when no TLS
1332            let config = TrackerConfig::new(
1333                default_core(),
1334                vec![],
1335                vec![],
1336                test_http_api_config("0.0.0.0:1212", "token"),
1337                test_health_check_api_config("127.0.0.1:1313"),
1338            )
1339            .unwrap();
1340
1341            let ports = config.derive_ports();
1342            let api_port = ports.iter().find(|p| p.host_port() == 1212);
1343
1344            assert!(api_port.is_some());
1345            assert_eq!(
1346                api_port.unwrap().description(),
1347                "HTTP API (stats/whitelist)"
1348            );
1349        }
1350
1351        #[test]
1352        fn it_should_not_expose_api_port_with_tls() {
1353            // PORT-06: API NOT exposed when TLS
1354            let domain = crate::shared::DomainName::new("api.example.com").unwrap();
1355            let config = TrackerConfig::new(
1356                default_core(),
1357                vec![],
1358                vec![],
1359                test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(domain), true),
1360                test_health_check_api_config("127.0.0.1:1313"),
1361            )
1362            .unwrap();
1363
1364            let ports = config.derive_ports();
1365            // API port should not be exposed when TLS is enabled
1366            assert!(ports.iter().all(|p| p.host_port() != 1212));
1367        }
1368
1369        #[test]
1370        fn it_should_return_empty_when_all_ports_hidden_by_tls() {
1371            // All services behind TLS = no exposed ports from tracker
1372            let api_domain = crate::shared::DomainName::new("api.example.com").unwrap();
1373            let tracker_domain = crate::shared::DomainName::new("tracker.example.com").unwrap();
1374
1375            let config = TrackerConfig::new(
1376                default_core(),
1377                vec![], // No UDP
1378                vec![test_http_tracker_config_with_tls(
1379                    "0.0.0.0:7070",
1380                    Some(tracker_domain),
1381                    true,
1382                )],
1383                test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(api_domain), true),
1384                test_health_check_api_config("127.0.0.1:1313"),
1385            )
1386            .unwrap();
1387
1388            let ports = config.derive_ports();
1389            assert!(ports.is_empty());
1390        }
1391
1392        #[test]
1393        fn it_should_include_descriptions_for_all_ports() {
1394            let config = TrackerConfig::default();
1395
1396            let ports = config.derive_ports();
1397
1398            for port in &ports {
1399                assert!(!port.description().is_empty());
1400            }
1401        }
1402    }
1403}