diff --git a/crates/irtt-client/src/client/tests/config.rs b/crates/irtt-client/src/client/tests/config.rs index 76f9120..f7560e6 100644 --- a/crates/irtt-client/src/client/tests/config.rs +++ b/crates/irtt-client/src/client/tests/config.rs @@ -85,6 +85,54 @@ fn params_from_config_encodes_continuous_duration_as_zero() { assert_eq!(params_from_config(&config).unwrap().duration_ns, 0); } +#[test] +fn params_from_config_rejects_zero_finite_duration() { + let config = ClientConfig { + duration: Some(Duration::ZERO), + ..ClientConfig::default() + }; + assert!(matches!( + params_from_config(&config), + Err(ClientError::InvalidConfig { reason }) + if reason == "duration must be greater than zero; use None for continuous mode" + )); +} + +#[test] +fn params_from_config_rejects_zero_interval() { + let config = ClientConfig { + interval: Duration::ZERO, + ..ClientConfig::default() + }; + assert!(matches!( + params_from_config(&config), + Err(ClientError::InvalidConfig { reason }) if reason == "interval must be greater than zero" + )); +} + +#[test] +fn params_from_config_rejects_nanosecond_encoding_overflow_as_invalid_config() { + let config = ClientConfig { + duration: Some(Duration::from_nanos(i64::MAX as u64) + Duration::from_nanos(1)), + ..ClientConfig::default() + }; + assert!(matches!( + params_from_config(&config), + Err(ClientError::InvalidConfig { reason }) + if reason == "duration is too large to encode as nanoseconds" + )); + + let config = ClientConfig { + interval: Duration::from_nanos(i64::MAX as u64) + Duration::from_nanos(1), + ..ClientConfig::default() + }; + assert!(matches!( + params_from_config(&config), + Err(ClientError::InvalidConfig { reason }) + if reason == "interval is too large to encode as nanoseconds" + )); +} + #[test] fn params_from_config_accepts_max_dscp_codepoint() { let config = ClientConfig { diff --git a/crates/irtt-client/src/client/tests/negotiation.rs b/crates/irtt-client/src/client/tests/negotiation.rs index 337e76f..0a1d8cf 100644 --- a/crates/irtt-client/src/client/tests/negotiation.rs +++ b/crates/irtt-client/src/client/tests/negotiation.rs @@ -29,6 +29,82 @@ fn loose_negotiation_accepts_server_restricted_params() { assert!(validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose).is_ok()); } +#[test] +fn loose_negotiation_rejects_non_positive_returned_interval() { + let config = ClientConfig::default(); + let requested = params_from_config(&config).unwrap(); + + for interval_ns in [0, -1] { + let mut returned = requested.clone(); + returned.interval_ns = interval_ns; + assert!(matches!( + validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose), + Err(ClientError::NegotiationRejected { reason }) + if reason == "interval must be positive" + )); + } +} + +#[test] +fn loose_negotiation_accepts_documented_duration_combinations() { + let finite_config = ClientConfig::default(); + let requested_finite = params_from_config(&finite_config).unwrap(); + + let returned_same_finite = requested_finite.clone(); + assert!(validate_negotiated_params( + &requested_finite, + &returned_same_finite, + NegotiationPolicy::Loose + ) + .is_ok()); + + let mut returned_shorter_finite = requested_finite.clone(); + returned_shorter_finite.duration_ns /= 2; + assert!(validate_negotiated_params( + &requested_finite, + &returned_shorter_finite, + NegotiationPolicy::Loose + ) + .is_ok()); + + let continuous_config = ClientConfig { + duration: None, + ..ClientConfig::default() + }; + let requested_continuous = params_from_config(&continuous_config).unwrap(); + + let returned_continuous = requested_continuous.clone(); + assert!(validate_negotiated_params( + &requested_continuous, + &returned_continuous, + NegotiationPolicy::Loose + ) + .is_ok()); + + let mut returned_finite = requested_continuous.clone(); + returned_finite.duration_ns = 1_000_000_000; + assert!(validate_negotiated_params( + &requested_continuous, + &returned_finite, + NegotiationPolicy::Loose + ) + .is_ok()); +} + +#[test] +fn loose_negotiation_rejects_finite_request_returned_continuous() { + let config = ClientConfig::default(); + let requested = params_from_config(&config).unwrap(); + let mut returned = requested.clone(); + returned.duration_ns = 0; + + assert!(matches!( + validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose), + Err(ClientError::NegotiationRejected { reason }) + if reason == "server returned continuous duration for finite request" + )); +} + #[test] fn loose_negotiation_rejects_negative_returned_length() { let config = ClientConfig::default();