waitup/
lib.rs

1#![allow(
2    clippy::blanket_clippy_restriction_lints,
3    clippy::pub_use,
4    clippy::question_mark_used,
5    clippy::single_call_fn,
6    clippy::mod_module_files,
7    clippy::implicit_return,
8    clippy::separated_literal_suffix,
9    clippy::unseparated_literal_suffix,
10    clippy::pub_with_shorthand,
11    clippy::pub_without_shorthand,
12    clippy::allow_attributes,
13    clippy::arbitrary_source_item_ordering,
14    clippy::min_ident_chars,
15    clippy::exhaustive_structs,
16    clippy::exhaustive_enums,
17    clippy::indexing_slicing,
18    clippy::redundant_test_prefix,
19    clippy::assertions_on_result_states,
20    clippy::absolute_paths,
21    clippy::default_numeric_fallback,
22    clippy::integer_division,
23    clippy::integer_division_remainder_used,
24    clippy::float_arithmetic,
25    clippy::as_conversions,
26    clippy::shadow_unrelated,
27    clippy::std_instead_of_core,
28    clippy::unwrap_used,
29    clippy::expect_used,
30    clippy::panic,
31    clippy::todo,
32    clippy::unimplemented,
33    clippy::dbg_macro,
34    clippy::print_stdout,
35    clippy::print_stderr,
36    clippy::missing_docs_in_private_items,
37    clippy::multiple_inherent_impl,
38    clippy::missing_trait_methods,
39    clippy::pattern_type_mismatch,
40    clippy::ref_patterns,
41    clippy::self_named_constructors,
42    clippy::wildcard_enum_match_arm,
43    clippy::string_slice,
44    clippy::arithmetic_side_effects,
45    clippy::cast_possible_truncation,
46    clippy::cast_sign_loss,
47    clippy::cast_possible_wrap,
48    clippy::cast_precision_loss,
49    clippy::unreachable,
50    clippy::else_if_without_else,
51    clippy::match_wildcard_for_single_variants,
52    clippy::match_same_arms,
53    clippy::single_match_else,
54    clippy::match_bool,
55    clippy::std_instead_of_alloc,
56    clippy::unused_trait_names,
57    clippy::str_to_string,
58    clippy::string_to_string,
59    clippy::iter_over_hash_type,
60    clippy::infinite_loop,
61    clippy::little_endian_bytes,
62    clippy::big_endian_bytes,
63    clippy::host_endian_bytes,
64    clippy::partial_pub_fields,
65    clippy::pub_without_shorthand,
66    clippy::pub_with_shorthand,
67    clippy::error_impl_error,
68    clippy::lossy_float_literal,
69    clippy::float_cmp,
70    clippy::float_cmp_const,
71    clippy::redundant_type_annotations,
72    clippy::field_reassign_with_default,
73    clippy::clone_on_ref_ptr,
74    clippy::rc_buffer,
75    clippy::rc_mutex,
76    clippy::multiple_unsafe_ops_per_block,
77    clippy::undocumented_unsafe_blocks,
78    clippy::mixed_read_write_in_expression,
79    clippy::tuple_array_conversions,
80    clippy::format_push_string,
81    clippy::tests_outside_test_module,
82    clippy::as_underscore,
83    clippy::deref_by_slicing,
84    clippy::empty_line_after_outer_attr,
85    clippy::empty_structs_with_brackets,
86    clippy::enum_glob_use,
87    clippy::exit,
88    clippy::filetype_is_file,
89    clippy::fn_to_numeric_cast_any,
90    clippy::format_collect,
91    clippy::four_forward_slashes,
92    clippy::get_unwrap,
93    clippy::impl_trait_in_params,
94    clippy::let_underscore_must_use,
95    clippy::let_underscore_untyped,
96    clippy::map_err_ignore,
97    clippy::mem_forget,
98    clippy::missing_assert_message,
99    clippy::missing_asserts_for_indexing,
100    clippy::mixed_attributes_style,
101    clippy::mutex_atomic,
102    clippy::needless_raw_strings,
103    clippy::needless_raw_string_hashes,
104    clippy::non_ascii_literal,
105    clippy::panic_in_result_fn,
106    clippy::partial_pub_fields,
107    clippy::print_literal,
108    clippy::rest_pat_in_fully_bound_structs,
109    clippy::same_name_method,
110    clippy::semicolon_outside_block,
111    clippy::shadow_same,
112    clippy::shadow_reuse,
113    clippy::single_char_lifetime_names,
114    clippy::string_lit_chars_any,
115    clippy::string_lit_as_bytes,
116    clippy::try_err,
117    clippy::undocumented_unsafe_blocks,
118    clippy::unneeded_field_pattern,
119    clippy::unseparated_literal_suffix,
120    clippy::separated_literal_suffix,
121    clippy::use_debug,
122    clippy::verbose_file_reads,
123    clippy::wildcard_dependencies,
124    clippy::module_name_repetitions,
125    clippy::missing_inline_in_public_items,
126    clippy::missing_trait_methods,
127    clippy::missing_docs_in_private_items,
128    clippy::single_call_fn,
129    clippy::cognitive_complexity,
130    clippy::too_many_lines,
131    clippy::type_complexity,
132    clippy::similar_names,
133    clippy::many_single_char_names,
134    clippy::items_after_statements,
135    clippy::cast_lossless,
136    clippy::cast_possible_truncation,
137    clippy::cast_possible_wrap,
138    clippy::cast_precision_loss,
139    clippy::cast_sign_loss,
140    clippy::fn_params_excessive_bools,
141    clippy::struct_excessive_bools,
142    clippy::if_not_else,
143    clippy::inline_always,
144    clippy::must_use_candidate,
145    clippy::option_if_let_else,
146    clippy::redundant_closure_for_method_calls,
147    clippy::similar_names,
148    clippy::single_match_else,
149    clippy::too_many_arguments,
150    clippy::unreadable_literal,
151    clippy::unused_self,
152    clippy::used_underscore_binding,
153    clippy::wildcard_imports,
154    clippy::doc_markdown,
155    clippy::missing_errors_doc,
156    clippy::missing_panics_doc,
157    clippy::return_self_not_must_use,
158    clippy::semicolon_if_nothing_returned,
159    clippy::should_implement_trait,
160    clippy::trivially_copy_pass_by_ref,
161    clippy::unnecessary_wraps,
162    clippy::naive_bytecount,
163    clippy::needless_collect,
164    clippy::needless_pass_by_value,
165    clippy::option_option,
166    clippy::ptr_as_ptr,
167    clippy::range_minus_one,
168    clippy::range_plus_one,
169    clippy::redundant_else,
170    clippy::stable_sort_primitive,
171    clippy::string_add_assign,
172    clippy::unicode_not_nfc,
173    clippy::unnested_or_patterns,
174    clippy::unreadable_literal,
175    clippy::unused_async,
176    clippy::use_self,
177    clippy::used_underscore_binding,
178    clippy::verbose_bit_mask,
179    clippy::inconsistent_struct_constructor,
180    clippy::index_refutable_slice,
181    clippy::inefficient_to_string,
182    clippy::implicit_clone,
183    clippy::cloned_instead_of_copied,
184    clippy::copy_iterator,
185    clippy::default_trait_access,
186    clippy::empty_enum,
187    clippy::enum_variant_names,
188    clippy::expl_impl_clone_on_copy,
189    clippy::explicit_deref_methods,
190    clippy::explicit_into_iter_loop,
191    clippy::explicit_iter_loop,
192    clippy::fallible_impl_from,
193    clippy::filter_map_next,
194    clippy::flat_map_option,
195    clippy::float_cmp_const,
196    clippy::fn_params_excessive_bools,
197    clippy::from_iter_instead_of_collect,
198    clippy::if_let_mutex,
199    clippy::ignored_unit_patterns,
200    clippy::implicit_clone,
201    clippy::implicit_hasher,
202    clippy::imprecise_flops,
203    clippy::inconsistent_struct_constructor,
204    clippy::index_refutable_slice,
205    clippy::inefficient_to_string,
206    clippy::inline_fn_without_body,
207    clippy::into_iter_on_ref,
208    clippy::invalid_upcast_comparisons,
209    clippy::iter_not_returning_iterator,
210    clippy::large_digit_groups,
211    clippy::large_stack_arrays,
212    clippy::large_types_passed_by_value,
213    clippy::let_unit_value,
214    clippy::linkedlist,
215    clippy::lossy_float_literal,
216    clippy::macro_use_imports,
217    clippy::manual_ok_or,
218    clippy::map_flatten,
219    clippy::map_unwrap_or,
220    clippy::match_same_arms,
221    clippy::match_wild_err_arm,
222    clippy::match_wildcard_for_single_variants,
223    clippy::mem_forget,
224    unexpected_cfgs,
225    clippy::missing_enforced_import_renames,
226    clippy::missing_errors_doc,
227    clippy::missing_panics_doc,
228    clippy::module_name_repetitions,
229    clippy::must_use_candidate,
230    clippy::mut_mut,
231    clippy::mutex_integer,
232    clippy::needless_continue,
233    clippy::needless_for_each,
234    clippy::needless_pass_by_value,
235    clippy::negative_feature_names,
236    clippy::no_effect_underscore_binding,
237    clippy::option_option,
238    clippy::path_buf_push_overwrite,
239    clippy::ptr_as_ptr,
240    clippy::ptr_cast_constness,
241    clippy::range_minus_one,
242    clippy::range_plus_one,
243    clippy::rc_buffer,
244    clippy::rc_mutex,
245    clippy::redundant_feature_names,
246    clippy::ref_option_ref,
247    clippy::rest_pat_in_fully_bound_structs,
248    clippy::same_functions_in_if_condition,
249    clippy::semicolon_if_nothing_returned,
250    clippy::should_implement_trait,
251    clippy::single_match_else,
252    clippy::stable_sort_primitive,
253    clippy::string_add,
254    clippy::string_add_assign,
255    clippy::string_lit_as_bytes,
256    clippy::string_to_string,
257    clippy::struct_excessive_bools,
258    clippy::suboptimal_flops,
259    clippy::suspicious_operation_groupings,
260    clippy::trait_duplication_in_bounds,
261    clippy::transmute_ptr_to_ptr,
262    clippy::trivially_copy_pass_by_ref,
263    clippy::type_repetition_in_bounds,
264    clippy::unchecked_duration_subtraction,
265    clippy::unicode_not_nfc,
266    clippy::unimplemented,
267    clippy::unnested_or_patterns,
268    clippy::unreadable_literal,
269    clippy::unsafe_derive_deserialize,
270    clippy::unused_async,
271    clippy::unused_self,
272    clippy::use_debug,
273    clippy::use_self,
274    clippy::used_underscore_binding,
275    clippy::useless_transmute,
276    clippy::verbose_bit_mask,
277    clippy::wildcard_dependencies,
278    clippy::wildcard_enum_match_arm,
279    clippy::wildcard_imports,
280    clippy::zero_sized_map_values,
281    clippy::else_if_without_else,
282    clippy::non_ascii_literal,
283    clippy::question_mark_used,
284    unfulfilled_lint_expectations,
285    reason = "restriction lints contain contradictory rules and overly pedantic restrictions"
286)]
287
288//! A robust library for waiting until TCP ports, HTTP endpoints, and services become available.
289//!
290//! This library provides functionality for testing network connectivity and service availability,
291//! with support for DNS resolution, exponential backoff, and multiple connection strategies.
292//! Perfect for Docker, Kubernetes, CI/CD pipelines, and microservices orchestration.
293//!
294//! # Features
295//!
296//! - **Type Safety**: `NewType` wrappers for ports and hostnames with validation
297//! - **Multiple Protocols**: TCP socket connections and HTTP/HTTPS requests
298//! - **Flexible Configuration**: Timeouts, retry limits, exponential backoff
299//! - **Concurrency Strategies**: Wait for all targets or any target
300//! - **Graceful Cancellation**: Cancellation token support for clean shutdown
301//! - **Rich Error Context**: Detailed error information with contextual messages
302//! - **High Performance**: Optimized for minimal allocations and fast execution
303//! - **Comprehensive Testing**: Property-based and parameterized test coverage
304//!
305//! # Quick Start
306//!
307//! Add to your `Cargo.toml`:
308//!
309//! ```toml
310//! [dependencies]
311//! waitup = "1.0"
312//! tokio = { version = "1.0", features = ["full"] }
313//! ```
314//!
315//! # Examples
316//!
317//! ## Basic TCP Connection Check
318//!
319//! ```rust,no_run
320//! use waitup::{Target, WaitConfig, wait_for_connection};
321//! use std::time::Duration;
322//!
323//! #[tokio::main]
324//! async fn main() -> Result<(), waitup::WaitForError> {
325//!     let target = Target::tcp("localhost", 8080)?;
326//!     let config = WaitConfig::builder()
327//!         .timeout(Duration::from_secs(30))
328//!         .interval(Duration::from_secs(1))
329//!         .build();
330//!
331//!     wait_for_connection(&[target], &config).await?;
332//!     println!("Service is ready!");
333//!     Ok(())
334//! }
335//! ```
336//!
337//! ## HTTP Health Check with Custom Headers
338//!
339//! ```rust,no_run
340//! use waitup::Target;
341//! use url::Url;
342//!
343//! #[tokio::main]
344//! async fn main() -> Result<(), waitup::WaitForError> {
345//!     let target = Target::http_builder(Url::parse("https://api.example.com/health")?)
346//!         .status(200)
347//!         .auth_bearer("your-api-token")
348//!         .content_type("application/json")
349//!         .build()?;
350//!
351//!     let config = waitup::WaitConfig::builder()
352//!         .timeout(std::time::Duration::from_secs(60))
353//!         .build();
354//!
355//!     waitup::wait_for_connection(&[target], &config).await?;
356//!     println!("API is healthy!");
357//!     Ok(())
358//! }
359//! ```
360//!
361//! ## Multiple Services with Different Strategies
362//!
363//! ```rust,no_run
364//! use waitup::{Target, WaitConfig, wait_for_connection};
365//! use std::time::Duration;
366//!
367//! #[tokio::main]
368//! async fn main() -> Result<(), waitup::WaitForError> {
369//!     let targets = vec![
370//!         Target::tcp("database", 5432)?,
371//!         Target::tcp("cache", 6379)?,
372//!         Target::http_url("https://api.example.com/health", 200)?,
373//!     ];
374//!
375//!     // Wait for ALL services to be ready
376//!     let config = WaitConfig::builder()
377//!         .timeout(Duration::from_secs(120))
378//!         .wait_for_any(false)
379//!         .max_retries(Some(20))
380//!         .build();
381//!
382//!     wait_for_connection(&targets, &config).await?;
383//!     println!("All services are ready!");
384//!     Ok(())
385//! }
386//! ```
387//!
388//! ## Advanced Configuration with Cancellation
389//!
390//! ```rust,no_run
391//! use waitup::{Target, WaitConfig, wait_for_connection};
392//! use std::time::Duration;
393//! use tokio::time::sleep;
394//!
395//! #[tokio::main]
396//! async fn main() -> Result<(), waitup::WaitForError> {
397//!     let target = Target::tcp("slow-service", 8080)?;
398//!
399//!     let (builder, cancel_token) = WaitConfig::builder()
400//!         .timeout(Duration::from_secs(60))
401//!         .interval(Duration::from_millis(500))
402//!         .with_cancellation();
403//!
404//!     let config = builder.build();
405//!
406//!     // Cancel after 10 seconds
407//!     let cancel_handle = {
408//!         let token = cancel_token.clone();
409//!         tokio::spawn(async move {
410//!             sleep(Duration::from_secs(10)).await;
411//!             token.cancel();
412//!         })
413//!     };
414//!
415//!     match wait_for_connection(&[target], &config).await {
416//!         Ok(_) => println!("Service is ready!"),
417//!         Err(waitup::WaitForError::Cancelled) => println!("Operation was cancelled"),
418//!         Err(e) => println!("Error: {}", e),
419//!     }
420//!
421//!     cancel_handle.abort(); // Clean up the cancel task
422//!     Ok(())
423//! }
424//! ```
425//!
426//! ## Docker Compose Integration
427//!
428//! ```rust,no_run
429//! use waitup::{Target, WaitConfig, wait_for_connection};
430//! use std::time::Duration;
431//!
432//! /// Wait for services defined in docker-compose.yml
433//! #[tokio::main]
434//! async fn main() -> Result<(), waitup::WaitForError> {
435//!     let services = vec![
436//!         Target::tcp("postgres", 5432)?,     // Database
437//!         Target::tcp("redis", 6379)?,        // Cache
438//!         Target::tcp("elasticsearch", 9200)?, // Search
439//!         Target::http_url("http://web:8000/health", 200)?, // Web app
440//!     ];
441//!
442//!     let config = WaitConfig::builder()
443//!         .timeout(Duration::from_secs(300))  // 5 minutes for Docker startup
444//!         .interval(Duration::from_secs(2))   // Check every 2 seconds
445//!         .max_interval(Duration::from_secs(10)) // Max 10 seconds between retries
446//!         .connection_timeout(Duration::from_secs(5)) // 5 second connection timeout
447//!         .wait_for_any(false)               // Wait for ALL services
448//!         .build();
449//!
450//!     println!("Waiting for services to be ready...");
451//!     wait_for_connection(&services, &config).await?;
452//!     println!("All services are ready! Starting application...");
453//!     Ok(())
454//! }
455//! ```
456
457// Module declarations
458pub mod async_traits;
459pub mod config;
460pub mod connection;
461pub mod error;
462pub mod iterators;
463#[macro_use]
464pub mod macros;
465pub mod presets;
466pub mod security;
467pub mod target;
468pub mod types;
469pub mod zero_cost;
470
471// Re-export commonly used types for convenient public API
472pub use async_traits::{
473    AsyncConnectionStrategy, AsyncRetryStrategy, AsyncTargetChecker, ConcurrentProgressStrategy,
474    DefaultTargetChecker, ExponentialBackoffStrategy, LinearBackoffStrategy, WaitForAllStrategy,
475    WaitForAnyStrategy,
476};
477pub use config::WaitConfigBuilder;
478pub use connection::{wait_for_connection, wait_for_single_target};
479pub use error::{Result, ResultExt, WaitForError};
480pub use iterators::{ResultSummary, TargetIterExt, TargetResultIterExt};
481pub use security::{RateLimiter, SecurityValidator};
482pub use target::{HttpTargetBuilder, TcpTargetBuilder};
483pub use types::{
484    ConnectionError, Hostname, HttpError, Port, Target, TargetResult, WaitConfig, WaitResult,
485};
486pub use zero_cost::{
487    ConstRetryStrategy, DynamicPort, LazyFormat, RegisteredPort, SmallString, StringBuilder,
488    TargetDisplay, ValidatedPort, WellKnownPort,
489};
490
491// Re-export error_messages for internal use
492pub(crate) use error::error_messages;
493
494#[cfg(test)]
495#[expect(
496    clippy::unwrap_used,
497    clippy::panic,
498    clippy::match_wildcard_for_single_variants,
499    reason = "test code where panics are acceptable"
500)]
501mod tests {
502    use super::*;
503    use proptest::prelude::*;
504    use std::time::Duration;
505    use test_case::test_case;
506    use url::Url;
507
508    #[test]
509    fn test_target_parse_tcp() {
510        let target = Target::parse("localhost:8080", 200).unwrap();
511        match target {
512            Target::Tcp { host, port } => {
513                assert_eq!(host.as_str(), "localhost");
514                assert_eq!(port.get(), 8080);
515            }
516            _ => panic!("Expected TCP target"),
517        }
518    }
519
520    #[test]
521    fn test_target_parse_http() {
522        let target = Target::parse("https://example.com/health", 200).unwrap();
523        match target {
524            Target::Http {
525                url,
526                expected_status,
527                ..
528            } => {
529                assert_eq!(url.to_string(), "https://example.com/health");
530                assert_eq!(expected_status, 200);
531            }
532            _ => panic!("Expected HTTP target"),
533        }
534    }
535
536    #[test]
537    fn test_target_display() {
538        let tcp_target = Target::tcp("localhost", 8080).unwrap();
539        assert_eq!(tcp_target.display(), "localhost:8080");
540
541        let url = Url::parse("https://example.com/health").unwrap();
542        let http_target = Target::http(url, 200).unwrap();
543        assert_eq!(http_target.display(), "https://example.com/health");
544    }
545
546    #[test]
547    fn test_wait_config_builder() {
548        let config = WaitConfig::builder()
549            .timeout(Duration::from_secs(60))
550            .interval(Duration::from_secs(2))
551            .max_interval(Duration::from_secs(30))
552            .wait_for_any(true)
553            .max_retries(Some(10))
554            .build();
555
556        assert_eq!(config.timeout, Duration::from_secs(60));
557        assert_eq!(config.initial_interval, Duration::from_secs(2));
558        assert_eq!(config.max_interval, Duration::from_secs(30));
559        assert!(config.wait_for_any);
560        assert_eq!(config.max_retries, Some(10));
561    }
562
563    #[test]
564    fn test_calculate_next_interval() {
565        use connection::calculate_next_interval;
566
567        let current = Duration::from_secs(1);
568        let max = Duration::from_secs(30);
569
570        let next = calculate_next_interval(current, max);
571        assert_eq!(next, Duration::from_millis(1500));
572
573        let large_current = Duration::from_secs(25);
574        let next = calculate_next_interval(large_current, max);
575        assert_eq!(next, max);
576    }
577
578    // Property-based tests for Port validation
579    proptest! {
580        #[test]
581        fn test_port_new_valid_range(port in 1u16..=65535) {
582            let result = Port::new(port);
583            assert!(result.is_some());
584            assert_eq!(result.unwrap().get(), port);
585        }
586
587        #[test]
588        fn test_port_new_zero_invalid(port in 0u16..=0) {
589            let result = Port::new(port);
590            assert!(result.is_none());
591        }
592
593        #[test]
594        fn test_port_well_known_valid_range(port in 1u16..=1023) {
595            let result = Port::well_known(port);
596            assert!(result.is_some());
597            assert_eq!(result.unwrap().get(), port);
598        }
599
600        #[test]
601        fn test_port_well_known_invalid_range(port in 1024u16..=65535) {
602            let result = Port::well_known(port);
603            assert!(result.is_none());
604        }
605
606        #[test]
607        fn test_port_registered_valid_range(port in 1024u16..=49151) {
608            let result = Port::registered(port);
609            assert!(result.is_some());
610            assert_eq!(result.unwrap().get(), port);
611        }
612
613        #[test]
614        fn test_port_registered_invalid_low_range(port in 1u16..=1023) {
615            let result = Port::registered(port);
616            assert!(result.is_none());
617        }
618
619        #[test]
620        fn test_port_registered_invalid_high_range(port in 49152u16..=65535) {
621            let result = Port::registered(port);
622            assert!(result.is_none());
623        }
624
625        #[test]
626        fn test_port_dynamic_valid_range(port in 49152u16..=65535) {
627            let result = Port::dynamic(port);
628            assert!(result.is_some());
629            assert_eq!(result.unwrap().get(), port);
630        }
631
632        #[test]
633        fn test_port_dynamic_invalid_range(port in 1u16..=49151) {
634            let result = Port::dynamic(port);
635            assert!(result.is_none());
636        }
637
638        #[test]
639        fn test_hostname_validation_alphanumeric(
640            hostname in "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]"
641        ) {
642            let result = Hostname::new(hostname);
643            assert!(result.is_ok());
644        }
645
646        #[test]
647        fn test_hostname_validation_too_long(
648            hostname in "[a-zA-Z]{254,300}"
649        ) {
650            let result = Hostname::new(hostname);
651            assert!(result.is_err());
652        }
653
654        #[test]
655        fn test_target_tcp_creation(
656            hostname in "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,30}[a-zA-Z0-9]",
657            port in 1u16..=65535
658        ) {
659            let result = Target::tcp(hostname, port);
660            assert!(result.is_ok());
661        }
662
663        #[test]
664        fn test_calculate_next_interval_property(
665            current_ms in 1u64..=60000,
666            max_ms in 60_000u64..=300_000
667        ) {
668            let current = Duration::from_millis(current_ms);
669            let max = Duration::from_millis(max_ms);
670
671            let next = connection::calculate_next_interval(current, max);
672
673            // Next interval should be greater than current (due to exponential backoff)
674            assert!(next >= current);
675            // Next interval should not exceed max
676            assert!(next <= max);
677        }
678    }
679
680    // Parameterized tests using test-case
681    #[test_case("localhost", 80; "http port")]
682    #[test_case("example.com", 443; "https port")]
683    #[test_case("127.0.0.1", 22; "ssh port")]
684    #[test_case("db.example.com", 5432; "postgres port")]
685    fn test_tcp_target_creation(hostname: &str, port: u16) {
686        let target = Target::tcp(hostname, port).unwrap();
687        match target {
688            Target::Tcp { host, port: p } => {
689                assert_eq!(host.as_str(), hostname);
690                assert_eq!(p.get(), port);
691            }
692            _ => panic!("Expected TCP target"),
693        }
694    }
695
696    #[test_case(80, Port::http(); "http port constant")]
697    #[test_case(443, Port::https(); "https port constant")]
698    #[test_case(22, Port::ssh(); "ssh port constant")]
699    #[test_case(5432, Port::postgres(); "postgres port constant")]
700    #[test_case(3306, Port::mysql(); "mysql port constant")]
701    #[test_case(6379, Port::redis(); "redis port constant")]
702    fn test_port_constants(expected: u16, port: Port) {
703        assert_eq!(port.get(), expected);
704    }
705
706    #[test_case("http://example.com/", 200; "http url")]
707    #[test_case("https://api.example.com/health", 200; "https health endpoint")]
708    #[test_case("https://example.com:8080/status", 204; "custom port and status")]
709    fn test_http_target_parsing(url_str: &str, status: u16) {
710        let target = Target::parse(url_str, status).unwrap();
711        match target {
712            Target::Http {
713                url,
714                expected_status,
715                ..
716            } => {
717                assert_eq!(url.to_string(), url_str);
718                assert_eq!(expected_status, status);
719            }
720            _ => panic!("Expected HTTP target"),
721        }
722    }
723
724    #[test_case(""; "empty string")]
725    #[test_case("invalid-target"; "missing port")]
726    #[test_case("host:"; "empty port")]
727    #[test_case("host:abc"; "non-numeric port")]
728    #[test_case("host:0"; "zero port")]
729    #[test_case("host:65536"; "port too high")]
730    fn test_invalid_target_parsing(target_str: &str) {
731        let result = Target::parse(target_str, 200);
732        assert!(result.is_err());
733    }
734
735    #[test_case(""; "empty hostname")]
736    #[test_case("-example.com"; "starts with hyphen")]
737    #[test_case("example.com-"; "ends with hyphen")]
738    #[test_case("ex..ample.com"; "empty label")]
739    #[test_case(&"a".repeat(254); "too long")]
740    fn test_invalid_hostname_validation(hostname: &str) {
741        let result = Hostname::new(hostname);
742        assert!(result.is_err());
743    }
744
745    #[test_case("192.168.1.1"; "valid ipv4")]
746    #[test_case("10.0.0.1"; "valid private ip")]
747    #[test_case("255.255.255.255"; "max ipv4")]
748    fn test_valid_ipv4_hostname(ip: &str) {
749        let result = Hostname::ipv4(ip);
750        assert!(result.is_ok());
751        assert_eq!(result.unwrap().as_str(), ip);
752    }
753
754    #[test_case("192.168.1"; "incomplete ipv4")]
755    #[test_case("192.168.1.1.1"; "too many parts")]
756    #[test_case("192.168.256.1"; "octet too high")]
757    #[test_case("192.168.abc.1"; "invalid octet")]
758    fn test_invalid_ipv4_hostname(ip: &str) {
759        let result = Hostname::ipv4(ip);
760        assert!(result.is_err());
761    }
762
763    #[test]
764    fn test_hostname_const_constructors() {
765        let localhost = Hostname::localhost();
766        assert_eq!(localhost.as_str(), "localhost");
767
768        let loopback = Hostname::loopback();
769        assert_eq!(loopback.as_str(), "127.0.0.1");
770
771        let loopback_v6 = Hostname::loopback_v6();
772        assert_eq!(loopback_v6.as_str(), "::1");
773
774        let any = Hostname::any();
775        assert_eq!(any.as_str(), "0.0.0.0");
776    }
777
778    #[test]
779    fn test_target_convenience_constructors() {
780        let localhost_target = Target::localhost(8080).unwrap();
781        assert_eq!(localhost_target.hostname(), "localhost");
782        assert_eq!(localhost_target.port(), Some(8080));
783
784        let loopback_target = Target::loopback(3000).unwrap();
785        assert_eq!(loopback_target.hostname(), "127.0.0.1");
786        assert_eq!(loopback_target.port(), Some(3000));
787
788        let loopback_v6_target = Target::loopback_v6(9090).unwrap();
789        assert_eq!(loopback_v6_target.hostname(), "::1");
790        assert_eq!(loopback_v6_target.port(), Some(9090));
791    }
792
793    #[test]
794    fn test_tcp_builder_fluent_interface() {
795        // Test that builder methods return Self for fluent chaining
796        let target = Target::tcp_builder("example.com")
797            .unwrap()
798            .registered_port(8080)
799            .build()
800            .unwrap();
801
802        assert_eq!(target.hostname(), "example.com");
803        assert_eq!(target.port(), Some(8080));
804    }
805
806    #[test]
807    fn test_tcp_builder_error_deferred() {
808        // Test that validation errors are deferred until build()
809        let result = Target::tcp_builder("example.com")
810            .unwrap()
811            .well_known_port(8080) // Invalid for well-known range
812            .build();
813
814        assert!(result.is_err());
815    }
816
817    #[test]
818    fn safe_tcp_targets_macro() {
819        // Test the new safe tcp_targets! macro
820        let result = tcp_targets![
821            "localhost" => 8080,
822            "example.com" => 443,
823        ];
824
825        assert!(result.is_ok());
826        let targets = result.unwrap();
827        assert_eq!(targets.len(), 2);
828        assert_eq!(targets[0].hostname(), "localhost");
829        assert_eq!(targets[0].port(), Some(8080));
830    }
831
832    #[test]
833    fn safe_tcp_targets_macro_error() {
834        // Test that the macro properly propagates errors
835        let result = tcp_targets![
836            "localhost" => 8080,
837            "example.com" => 0, // Invalid port
838        ];
839
840        assert!(result.is_err());
841    }
842
843    #[test]
844    fn safe_http_targets_macro() {
845        // Test the new safe http_targets! macro
846        let result = http_targets![
847            "https://example.com" => 200,
848            "http://localhost:8080" => 204,
849        ];
850
851        assert!(result.is_ok());
852        let targets = result.unwrap();
853        assert_eq!(targets.len(), 2);
854    }
855
856    #[test]
857    fn safe_http_targets_macro_error() {
858        // Test that the macro properly propagates errors
859        let result = http_targets![
860            "https://example.com" => 200,
861            "invalid-url" => 200, // Invalid URL
862        ];
863
864        result.unwrap_err();
865    }
866}