1use crate::RuntimeOptions;
10
11pub fn into_runtime_options(value: RuntimeOptions) -> shipper_types::RuntimeOptions {
16 shipper_types::RuntimeOptions {
17 allow_dirty: value.allow_dirty,
18 skip_ownership_check: value.skip_ownership_check,
19 strict_ownership: value.strict_ownership,
20 no_verify: value.no_verify,
21 max_attempts: value.max_attempts,
22 base_delay: value.base_delay,
23 max_delay: value.max_delay,
24 retry_strategy: value.retry_strategy,
25 retry_jitter: value.retry_jitter,
26 retry_per_error: value.retry_per_error,
27 verify_timeout: value.verify_timeout,
28 verify_poll_interval: value.verify_poll_interval,
29 state_dir: value.state_dir,
30 force_resume: value.force_resume,
31 policy: value.policy,
32 verify_mode: value.verify_mode,
33 readiness: value.readiness,
34 output_lines: value.output_lines,
35 force: value.force,
36 lock_timeout: value.lock_timeout,
37 parallel: value.parallel,
38 webhook: value.webhook,
39 encryption: value.encryption,
40 registries: value.registries,
41 resume_from: value.resume_from,
42 rehearsal_registry: value.rehearsal_registry,
43 rehearsal_skip: value.rehearsal_skip,
44 rehearsal_smoke_install: value.rehearsal_smoke_install,
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use crate::{
52 EncryptionConfig, ParallelConfig, PublishPolicy, ReadinessConfig, ReadinessMethod,
53 Registry, VerifyMode, WebhookConfig,
54 };
55 use proptest::prelude::*;
56 use shipper_types as expected_types;
57 use std::path::PathBuf;
58 use std::time::Duration;
59
60 fn sample_runtime_options() -> RuntimeOptions {
61 RuntimeOptions {
62 allow_dirty: true,
63 skip_ownership_check: false,
64 strict_ownership: true,
65 no_verify: false,
66 max_attempts: 8,
67 base_delay: Duration::from_secs(2),
68 max_delay: Duration::from_secs(45),
69 retry_strategy: shipper_retry::RetryStrategyType::Exponential,
70 retry_jitter: 0.25,
71 retry_per_error: shipper_retry::PerErrorConfig::default(),
72 verify_timeout: Duration::from_secs(120),
73 verify_poll_interval: Duration::from_secs(3),
74 state_dir: PathBuf::from("target/.shipper-tests"),
75 force_resume: false,
76 policy: PublishPolicy::Balanced,
77 verify_mode: VerifyMode::Package,
78 readiness: ReadinessConfig {
79 enabled: true,
80 method: ReadinessMethod::Both,
81 initial_delay: Duration::from_millis(150),
82 max_delay: Duration::from_secs(30),
83 max_total_wait: Duration::from_secs(300),
84 poll_interval: Duration::from_secs(3),
85 jitter_factor: 0.4,
86 index_path: Some(PathBuf::from("ci-index")),
87 prefer_index: true,
88 },
89 output_lines: 777,
90 force: true,
91 lock_timeout: Duration::from_secs(4_800),
92 parallel: ParallelConfig {
93 enabled: true,
94 max_concurrent: 6,
95 per_package_timeout: Duration::from_secs(180),
96 },
97 webhook: WebhookConfig {
98 url: "https://example.internal/webhook".to_string(),
99 secret: Some("shh".to_string()),
100 timeout_secs: 15,
101 ..WebhookConfig::default()
102 },
103 encryption: EncryptionConfig {
104 enabled: true,
105 passphrase: Some("password".to_string()),
106 env_var: Some("SHIPPER_ENCRYPT_KEY".to_string()),
107 },
108 registries: vec![
109 Registry {
110 name: "crates-io".to_string(),
111 api_base: "https://crates.io".to_string(),
112 index_base: Some("https://index.crates.io".to_string()),
113 },
114 Registry {
115 name: "mirror".to_string(),
116 api_base: "https://mirror.example.local".to_string(),
117 index_base: None,
118 },
119 ],
120 resume_from: Some("my-crate".to_string()),
121 rehearsal_registry: None,
122 rehearsal_skip: false,
123 rehearsal_smoke_install: None,
124 }
125 }
126
127 #[test]
128 fn maps_simple_discriminants() {
129 assert_eq!(PublishPolicy::Fast, expected_types::PublishPolicy::Fast);
130 assert_eq!(VerifyMode::Package, expected_types::VerifyMode::Package);
131 assert_eq!(
132 ReadinessMethod::Index,
133 expected_types::ReadinessMethod::Index
134 );
135 }
136
137 #[test]
138 fn maps_nested_structures_and_webhook_payload_fields() {
139 let source = sample_runtime_options();
140 let converted = into_runtime_options(source);
141
142 assert_eq!(converted.policy, expected_types::PublishPolicy::Balanced);
143 assert_eq!(converted.verify_mode, expected_types::VerifyMode::Package);
144 assert_eq!(
145 converted.readiness.method,
146 expected_types::ReadinessMethod::Both
147 );
148 assert_eq!(converted.parallel.max_concurrent, 6);
149 assert_eq!(converted.webhook.url, "https://example.internal/webhook");
150 assert_eq!(converted.webhook.secret.as_deref(), Some("shh"));
151 assert_eq!(converted.webhook.timeout_secs, 15);
152 assert!(converted.encryption.enabled);
153 assert_eq!(converted.encryption.passphrase.as_deref(), Some("password"));
154 assert_eq!(converted.registries.len(), 2);
155 }
156
157 #[test]
158 fn maps_readiness_config_fields() {
159 let converted = sample_runtime_options().readiness;
160
161 assert!(converted.enabled);
162 assert!(converted.prefer_index);
163 assert_eq!(
164 converted.index_path.as_deref(),
165 Some(std::path::Path::new("ci-index"))
166 );
167 }
168
169 #[test]
170 fn maps_parallel_config() {
171 let converted = sample_runtime_options().parallel;
172
173 assert!(converted.enabled);
174 assert_eq!(converted.max_concurrent, 6);
175 assert_eq!(converted.per_package_timeout, Duration::from_secs(180));
176 }
177
178 #[test]
179 fn maps_registry() {
180 let converted = sample_runtime_options().registries[0].clone();
181
182 assert_eq!(converted.name, "crates-io");
183 assert_eq!(converted.api_base, "https://crates.io");
184 }
185
186 fn registry_count_strategy() -> impl Strategy<Value = usize> {
187 0usize..4usize
188 }
189
190 fn webhook_url_strategy() -> impl Strategy<Value = String> {
191 prop::collection::vec(prop::char::range('a', 'z'), 0..32)
192 .prop_map(|chars| chars.into_iter().collect())
193 }
194
195 proptest! {
196 #[test]
197 fn fuzz_like_values_roundtrip_without_panic(
198 allow_dirty in any::<bool>(),
199 skip_ownership_check in any::<bool>(),
200 strict_ownership in any::<bool>(),
201 no_verify in any::<bool>(),
202 max_attempts in 1u32..20,
203 base_delay_ms in 0u64..5_000,
204 max_delay_ms in 0u64..10_000,
205 output_lines in 1usize..2000,
206 policy in prop_oneof![
207 Just(PublishPolicy::Safe),
208 Just(PublishPolicy::Balanced),
209 Just(PublishPolicy::Fast),
210 ],
211 verify_mode in prop_oneof![
212 Just(VerifyMode::Workspace),
213 Just(VerifyMode::Package),
214 Just(VerifyMode::None),
215 ],
216 readiness_method in prop_oneof![
217 Just(ReadinessMethod::Api),
218 Just(ReadinessMethod::Index),
219 Just(ReadinessMethod::Both),
220 ],
221 webhook_url in webhook_url_strategy(),
222 use_secret in any::<bool>(),
223 registry_count in registry_count_strategy(),
224 ) {
225 let webhook = WebhookConfig {
226 url: webhook_url.clone(),
227 secret: if use_secret { Some("secret".to_string()) } else { None },
228 ..WebhookConfig::default()
229 };
230
231 let encryption = EncryptionConfig {
232 enabled: true,
233 passphrase: if use_secret { Some("secret-pass".to_string()) } else { None },
234 ..EncryptionConfig::default()
235 };
236
237 let registries = (0..registry_count)
238 .map(|idx| Registry {
239 name: format!("r-{idx}"),
240 api_base: format!("https://registry{idx}.example"),
241 index_base: Some(format!("https://registry{idx}.example/index")),
242 })
243 .collect();
244
245 let input = RuntimeOptions {
246 allow_dirty,
247 skip_ownership_check,
248 strict_ownership,
249 no_verify,
250 max_attempts,
251 base_delay: Duration::from_millis(base_delay_ms),
252 max_delay: Duration::from_millis(max_delay_ms.max(base_delay_ms + 1)),
253 retry_strategy: shipper_retry::RetryStrategyType::Exponential,
254 retry_jitter: 0.25,
255 retry_per_error: shipper_retry::PerErrorConfig::default(),
256 verify_timeout: Duration::from_secs(30),
257 verify_poll_interval: Duration::from_secs(1),
258 state_dir: PathBuf::from(".shipper"),
259 force_resume: false,
260 policy,
261 verify_mode,
262 readiness: ReadinessConfig {
263 enabled: true,
264 method: readiness_method,
265 initial_delay: Duration::from_millis(10),
266 max_delay: Duration::from_secs(1),
267 max_total_wait: Duration::from_secs(60),
268 poll_interval: Duration::from_millis(250),
269 jitter_factor: 0.4,
270 index_path: None,
271 prefer_index: false,
272 },
273 output_lines,
274 force: false,
275 lock_timeout: Duration::from_secs(300),
276 parallel: ParallelConfig {
277 enabled: true,
278 max_concurrent: 4,
279 per_package_timeout: Duration::from_secs(120),
280 },
281 webhook,
282 encryption,
283 registries,
284 resume_from: None,
285 rehearsal_registry: None,
286 rehearsal_skip: false,
287 rehearsal_smoke_install: None,
288 };
289
290 let converted = into_runtime_options(input);
291
292 prop_assert_eq!(converted.allow_dirty, allow_dirty);
293 prop_assert_eq!(converted.skip_ownership_check, skip_ownership_check);
294 prop_assert_eq!(converted.strict_ownership, strict_ownership);
295 prop_assert_eq!(converted.no_verify, no_verify);
296 prop_assert_eq!(converted.max_attempts, max_attempts);
297 prop_assert_eq!(converted.policy, policy);
298 prop_assert_eq!(converted.verify_mode, verify_mode);
299 prop_assert!(converted.readiness.enabled);
300 prop_assert_eq!(converted.readiness.method, readiness_method);
301 prop_assert_eq!(converted.webhook.url, webhook_url);
302 prop_assert_eq!(converted.webhook.secret.is_some(), use_secret);
303 prop_assert_eq!(converted.registries.len(), registry_count);
304 }
305 }
306
307 #[test]
309 fn empty_registries_list() {
310 let mut opts = sample_runtime_options();
311 opts.registries = vec![];
312 let converted = into_runtime_options(opts);
313 assert!(converted.registries.is_empty());
314 }
315
316 #[test]
318 fn none_webhook_secret() {
319 let mut opts = sample_runtime_options();
320 opts.webhook.secret = None;
321 let converted = into_runtime_options(opts);
322 assert!(converted.webhook.secret.is_none());
323 }
324
325 #[test]
326 fn none_encryption_passphrase() {
327 let mut opts = sample_runtime_options();
328 opts.encryption.passphrase = None;
329 let converted = into_runtime_options(opts);
330 assert!(converted.encryption.passphrase.is_none());
331 }
332
333 #[test]
334 fn none_encryption_env_var() {
335 let mut opts = sample_runtime_options();
336 opts.encryption.env_var = None;
337 let converted = into_runtime_options(opts);
338 assert!(converted.encryption.env_var.is_none());
339 }
340
341 #[test]
342 fn none_resume_from() {
343 let mut opts = sample_runtime_options();
344 opts.resume_from = None;
345 let converted = into_runtime_options(opts);
346 assert!(converted.resume_from.is_none());
347 }
348
349 #[test]
350 fn none_index_path() {
351 let mut opts = sample_runtime_options();
352 opts.readiness.index_path = None;
353 let converted = into_runtime_options(opts);
354 assert!(converted.readiness.index_path.is_none());
355 }
356
357 #[test]
358 fn none_index_base_in_registry() {
359 let mut opts = sample_runtime_options();
360 opts.registries = vec![Registry {
361 name: "test".to_string(),
362 api_base: "https://example.com".to_string(),
363 index_base: None,
364 }];
365 let converted = into_runtime_options(opts);
366 assert!(converted.registries[0].index_base.is_none());
367 }
368
369 #[test]
371 fn zero_duration_base_delay() {
372 let mut opts = sample_runtime_options();
373 opts.base_delay = Duration::ZERO;
374 let converted = into_runtime_options(opts);
375 assert_eq!(converted.base_delay, Duration::ZERO);
376 }
377
378 #[test]
379 fn zero_duration_max_delay() {
380 let mut opts = sample_runtime_options();
381 opts.max_delay = Duration::ZERO;
382 let converted = into_runtime_options(opts);
383 assert_eq!(converted.max_delay, Duration::ZERO);
384 }
385
386 #[test]
387 fn zero_duration_verify_timeout() {
388 let mut opts = sample_runtime_options();
389 opts.verify_timeout = Duration::ZERO;
390 let converted = into_runtime_options(opts);
391 assert_eq!(converted.verify_timeout, Duration::ZERO);
392 }
393
394 #[test]
395 fn zero_duration_lock_timeout() {
396 let mut opts = sample_runtime_options();
397 opts.lock_timeout = Duration::ZERO;
398 let converted = into_runtime_options(opts);
399 assert_eq!(converted.lock_timeout, Duration::ZERO);
400 }
401
402 #[test]
403 fn very_small_duration_one_nanosecond() {
404 let mut opts = sample_runtime_options();
405 opts.base_delay = Duration::from_nanos(1);
406 opts.verify_poll_interval = Duration::from_nanos(1);
407 let converted = into_runtime_options(opts);
408 assert_eq!(converted.base_delay, Duration::from_nanos(1));
409 assert_eq!(converted.verify_poll_interval, Duration::from_nanos(1));
410 }
411
412 #[test]
413 fn very_large_duration() {
414 let large = Duration::from_secs(u64::MAX / 2);
415 let mut opts = sample_runtime_options();
416 opts.max_delay = large;
417 opts.lock_timeout = large;
418 let converted = into_runtime_options(opts);
419 assert_eq!(converted.max_delay, large);
420 assert_eq!(converted.lock_timeout, large);
421 }
422
423 #[test]
424 fn sub_millisecond_readiness_delays() {
425 let mut opts = sample_runtime_options();
426 opts.readiness.initial_delay = Duration::from_micros(500);
427 opts.readiness.poll_interval = Duration::from_micros(100);
428 let converted = into_runtime_options(opts);
429 assert_eq!(
430 converted.readiness.initial_delay,
431 Duration::from_micros(500)
432 );
433 assert_eq!(
434 converted.readiness.poll_interval,
435 Duration::from_micros(100)
436 );
437 }
438
439 #[test]
440 fn zero_per_package_timeout() {
441 let mut opts = sample_runtime_options();
442 opts.parallel.per_package_timeout = Duration::ZERO;
443 let converted = into_runtime_options(opts);
444 assert_eq!(converted.parallel.per_package_timeout, Duration::ZERO);
445 }
446
447 #[test]
449 fn maps_allow_dirty() {
450 for val in [true, false] {
451 let mut opts = sample_runtime_options();
452 opts.allow_dirty = val;
453 assert_eq!(into_runtime_options(opts).allow_dirty, val);
454 }
455 }
456
457 #[test]
458 fn maps_skip_ownership_check() {
459 for val in [true, false] {
460 let mut opts = sample_runtime_options();
461 opts.skip_ownership_check = val;
462 assert_eq!(into_runtime_options(opts).skip_ownership_check, val);
463 }
464 }
465
466 #[test]
467 fn maps_strict_ownership() {
468 for val in [true, false] {
469 let mut opts = sample_runtime_options();
470 opts.strict_ownership = val;
471 assert_eq!(into_runtime_options(opts).strict_ownership, val);
472 }
473 }
474
475 #[test]
476 fn maps_no_verify() {
477 for val in [true, false] {
478 let mut opts = sample_runtime_options();
479 opts.no_verify = val;
480 assert_eq!(into_runtime_options(opts).no_verify, val);
481 }
482 }
483
484 #[test]
485 fn maps_max_attempts() {
486 let mut opts = sample_runtime_options();
487 opts.max_attempts = 42;
488 assert_eq!(into_runtime_options(opts).max_attempts, 42);
489 }
490
491 #[test]
492 fn maps_base_delay() {
493 let mut opts = sample_runtime_options();
494 opts.base_delay = Duration::from_millis(999);
495 assert_eq!(
496 into_runtime_options(opts).base_delay,
497 Duration::from_millis(999)
498 );
499 }
500
501 #[test]
502 fn maps_max_delay() {
503 let mut opts = sample_runtime_options();
504 opts.max_delay = Duration::from_secs(9999);
505 assert_eq!(
506 into_runtime_options(opts).max_delay,
507 Duration::from_secs(9999)
508 );
509 }
510
511 #[test]
512 fn maps_retry_strategy() {
513 for strategy in [
514 shipper_retry::RetryStrategyType::Immediate,
515 shipper_retry::RetryStrategyType::Exponential,
516 shipper_retry::RetryStrategyType::Linear,
517 shipper_retry::RetryStrategyType::Constant,
518 ] {
519 let mut opts = sample_runtime_options();
520 opts.retry_strategy = strategy;
521 assert_eq!(into_runtime_options(opts).retry_strategy, strategy);
522 }
523 }
524
525 #[test]
526 fn maps_retry_jitter() {
527 let mut opts = sample_runtime_options();
528 opts.retry_jitter = 0.99;
529 let converted = into_runtime_options(opts);
530 assert!((converted.retry_jitter - 0.99).abs() < f64::EPSILON);
531 }
532
533 #[test]
534 fn maps_verify_timeout() {
535 let mut opts = sample_runtime_options();
536 opts.verify_timeout = Duration::from_secs(555);
537 assert_eq!(
538 into_runtime_options(opts).verify_timeout,
539 Duration::from_secs(555)
540 );
541 }
542
543 #[test]
544 fn maps_verify_poll_interval() {
545 let mut opts = sample_runtime_options();
546 opts.verify_poll_interval = Duration::from_millis(750);
547 assert_eq!(
548 into_runtime_options(opts).verify_poll_interval,
549 Duration::from_millis(750)
550 );
551 }
552
553 #[test]
554 fn maps_state_dir() {
555 let mut opts = sample_runtime_options();
556 opts.state_dir = PathBuf::from("/tmp/custom-state");
557 assert_eq!(
558 into_runtime_options(opts).state_dir,
559 PathBuf::from("/tmp/custom-state")
560 );
561 }
562
563 #[test]
564 fn maps_force_resume() {
565 for val in [true, false] {
566 let mut opts = sample_runtime_options();
567 opts.force_resume = val;
568 assert_eq!(into_runtime_options(opts).force_resume, val);
569 }
570 }
571
572 #[test]
573 fn maps_policy_variants() {
574 for policy in [
575 PublishPolicy::Safe,
576 PublishPolicy::Balanced,
577 PublishPolicy::Fast,
578 ] {
579 let mut opts = sample_runtime_options();
580 opts.policy = policy;
581 assert_eq!(into_runtime_options(opts).policy, policy);
582 }
583 }
584
585 #[test]
586 fn maps_verify_mode_variants() {
587 for mode in [VerifyMode::Workspace, VerifyMode::Package, VerifyMode::None] {
588 let mut opts = sample_runtime_options();
589 opts.verify_mode = mode;
590 assert_eq!(into_runtime_options(opts).verify_mode, mode);
591 }
592 }
593
594 #[test]
595 fn maps_output_lines() {
596 let mut opts = sample_runtime_options();
597 opts.output_lines = 0;
598 assert_eq!(into_runtime_options(opts).output_lines, 0);
599 }
600
601 #[test]
602 fn maps_force() {
603 for val in [true, false] {
604 let mut opts = sample_runtime_options();
605 opts.force = val;
606 assert_eq!(into_runtime_options(opts).force, val);
607 }
608 }
609
610 #[test]
611 fn maps_lock_timeout() {
612 let mut opts = sample_runtime_options();
613 opts.lock_timeout = Duration::from_secs(12345);
614 assert_eq!(
615 into_runtime_options(opts).lock_timeout,
616 Duration::from_secs(12345)
617 );
618 }
619
620 #[test]
621 fn maps_resume_from_some() {
622 let mut opts = sample_runtime_options();
623 opts.resume_from = Some("specific-crate".to_string());
624 assert_eq!(
625 into_runtime_options(opts).resume_from.as_deref(),
626 Some("specific-crate")
627 );
628 }
629
630 #[test]
631 fn maps_readiness_method_variants() {
632 for method in [
633 ReadinessMethod::Api,
634 ReadinessMethod::Index,
635 ReadinessMethod::Both,
636 ] {
637 let mut opts = sample_runtime_options();
638 opts.readiness.method = method;
639 assert_eq!(into_runtime_options(opts).readiness.method, method);
640 }
641 }
642
643 #[test]
644 fn maps_readiness_jitter_factor() {
645 let mut opts = sample_runtime_options();
646 opts.readiness.jitter_factor = 0.0;
647 let converted = into_runtime_options(opts);
648 assert!((converted.readiness.jitter_factor - 0.0).abs() < f64::EPSILON);
649 }
650
651 #[test]
652 fn maps_readiness_prefer_index() {
653 for val in [true, false] {
654 let mut opts = sample_runtime_options();
655 opts.readiness.prefer_index = val;
656 assert_eq!(into_runtime_options(opts).readiness.prefer_index, val);
657 }
658 }
659
660 #[test]
661 fn maps_parallel_max_concurrent() {
662 let mut opts = sample_runtime_options();
663 opts.parallel.max_concurrent = 1;
664 assert_eq!(into_runtime_options(opts).parallel.max_concurrent, 1);
665 }
666
667 #[test]
668 fn maps_parallel_enabled() {
669 for val in [true, false] {
670 let mut opts = sample_runtime_options();
671 opts.parallel.enabled = val;
672 assert_eq!(into_runtime_options(opts).parallel.enabled, val);
673 }
674 }
675
676 #[test]
677 fn maps_webhook_url() {
678 let mut opts = sample_runtime_options();
679 opts.webhook.url = "https://hooks.example.com/notify".to_string();
680 assert_eq!(
681 into_runtime_options(opts).webhook.url,
682 "https://hooks.example.com/notify"
683 );
684 }
685
686 #[test]
687 fn maps_webhook_timeout() {
688 let mut opts = sample_runtime_options();
689 opts.webhook.timeout_secs = 99;
690 assert_eq!(into_runtime_options(opts).webhook.timeout_secs, 99);
691 }
692
693 #[test]
694 fn maps_encryption_enabled() {
695 for val in [true, false] {
696 let mut opts = sample_runtime_options();
697 opts.encryption.enabled = val;
698 assert_eq!(into_runtime_options(opts).encryption.enabled, val);
699 }
700 }
701
702 #[test]
704 fn special_chars_in_webhook_url() {
705 let mut opts = sample_runtime_options();
706 opts.webhook.url =
707 "https://hooks.example.com/path?key=val&foo=bar#fragment%20encoded".to_string();
708 assert_eq!(
709 into_runtime_options(opts).webhook.url,
710 "https://hooks.example.com/path?key=val&foo=bar#fragment%20encoded"
711 );
712 }
713
714 #[test]
715 fn unicode_in_state_dir() {
716 let mut opts = sample_runtime_options();
717 opts.state_dir = PathBuf::from("/tmp/工作目录/shipper-状态");
718 assert_eq!(
719 into_runtime_options(opts).state_dir,
720 PathBuf::from("/tmp/工作目录/shipper-状态")
721 );
722 }
723
724 #[test]
725 fn special_chars_in_registry_fields() {
726 let mut opts = sample_runtime_options();
727 opts.registries = vec![Registry {
728 name: "my-org/private-reg".to_string(),
729 api_base: "https://registry.example.com:8443/api/v1?token=abc&scope=all".to_string(),
730 index_base: Some("https://index.example.com/path with spaces/".to_string()),
731 }];
732 let converted = into_runtime_options(opts);
733 assert_eq!(converted.registries[0].name, "my-org/private-reg");
734 assert_eq!(
735 converted.registries[0].api_base,
736 "https://registry.example.com:8443/api/v1?token=abc&scope=all"
737 );
738 assert_eq!(
739 converted.registries[0].index_base.as_deref(),
740 Some("https://index.example.com/path with spaces/")
741 );
742 }
743
744 #[test]
745 fn special_chars_in_resume_from() {
746 let mut opts = sample_runtime_options();
747 opts.resume_from = Some("my-crate_v2.0.0-rc.1".to_string());
748 assert_eq!(
749 into_runtime_options(opts).resume_from.as_deref(),
750 Some("my-crate_v2.0.0-rc.1")
751 );
752 }
753
754 #[test]
755 fn special_chars_in_encryption_env_var() {
756 let mut opts = sample_runtime_options();
757 opts.encryption.env_var = Some("MY_APP__ENCRYPT_KEY_2".to_string());
758 assert_eq!(
759 into_runtime_options(opts).encryption.env_var.as_deref(),
760 Some("MY_APP__ENCRYPT_KEY_2")
761 );
762 }
763
764 #[test]
765 fn empty_string_webhook_url() {
766 let mut opts = sample_runtime_options();
767 opts.webhook.url = String::new();
768 assert_eq!(into_runtime_options(opts).webhook.url, "");
769 }
770
771 #[test]
772 fn special_chars_in_readiness_index_path() {
773 let mut opts = sample_runtime_options();
774 opts.readiness.index_path = Some(PathBuf::from("C:\\Users\\build agent\\index (2)"));
775 assert_eq!(
776 into_runtime_options(opts).readiness.index_path,
777 Some(PathBuf::from("C:\\Users\\build agent\\index (2)"))
778 );
779 }
780
781 #[test]
783 fn single_registry() {
784 let mut opts = sample_runtime_options();
785 opts.registries = vec![Registry {
786 name: "only".to_string(),
787 api_base: "https://only.example.com".to_string(),
788 index_base: None,
789 }];
790 let converted = into_runtime_options(opts);
791 assert_eq!(converted.registries.len(), 1);
792 assert_eq!(converted.registries[0].name, "only");
793 }
794
795 #[test]
796 fn many_registries() {
797 let mut opts = sample_runtime_options();
798 opts.registries = (0..20)
799 .map(|i| Registry {
800 name: format!("reg-{i}"),
801 api_base: format!("https://reg{i}.example.com"),
802 index_base: if i % 2 == 0 {
803 Some(format!("https://index{i}.example.com"))
804 } else {
805 None
806 },
807 })
808 .collect();
809 let converted = into_runtime_options(opts);
810 assert_eq!(converted.registries.len(), 20);
811 assert!(converted.registries[0].index_base.is_some());
812 assert!(converted.registries[1].index_base.is_none());
813 }
814
815 #[test]
817 fn max_attempts_one() {
818 let mut opts = sample_runtime_options();
819 opts.max_attempts = 1;
820 assert_eq!(into_runtime_options(opts).max_attempts, 1);
821 }
822
823 #[test]
824 fn max_attempts_u32_max() {
825 let mut opts = sample_runtime_options();
826 opts.max_attempts = u32::MAX;
827 assert_eq!(into_runtime_options(opts).max_attempts, u32::MAX);
828 }
829
830 #[test]
831 fn output_lines_max() {
832 let mut opts = sample_runtime_options();
833 opts.output_lines = usize::MAX;
834 assert_eq!(into_runtime_options(opts).output_lines, usize::MAX);
835 }
836
837 #[test]
838 fn parallel_max_concurrent_zero() {
839 let mut opts = sample_runtime_options();
840 opts.parallel.max_concurrent = 0;
841 assert_eq!(into_runtime_options(opts).parallel.max_concurrent, 0);
842 }
843
844 #[test]
845 fn retry_jitter_zero() {
846 let mut opts = sample_runtime_options();
847 opts.retry_jitter = 0.0;
848 let converted = into_runtime_options(opts);
849 assert!((converted.retry_jitter).abs() < f64::EPSILON);
850 }
851
852 #[test]
853 fn retry_jitter_one() {
854 let mut opts = sample_runtime_options();
855 opts.retry_jitter = 1.0;
856 let converted = into_runtime_options(opts);
857 assert!((converted.retry_jitter - 1.0).abs() < f64::EPSILON);
858 }
859
860 #[test]
861 fn webhook_timeout_zero() {
862 let mut opts = sample_runtime_options();
863 opts.webhook.timeout_secs = 0;
864 assert_eq!(into_runtime_options(opts).webhook.timeout_secs, 0);
865 }
866
867 mod snapshot_tests {
868 use super::*;
869 use insta::assert_debug_snapshot;
870
871 fn default_config_runtime() -> RuntimeOptions {
872 RuntimeOptions {
873 allow_dirty: false,
874 skip_ownership_check: false,
875 strict_ownership: false,
876 no_verify: false,
877 max_attempts: 3,
878 base_delay: Duration::from_secs(5),
879 max_delay: Duration::from_secs(300),
880 retry_strategy: shipper_retry::RetryStrategyType::Exponential,
881 retry_jitter: 0.25,
882 retry_per_error: shipper_retry::PerErrorConfig::default(),
883 verify_timeout: Duration::from_secs(60),
884 verify_poll_interval: Duration::from_secs(5),
885 state_dir: PathBuf::from(".shipper"),
886 force_resume: false,
887 policy: PublishPolicy::Safe,
888 verify_mode: VerifyMode::Workspace,
889 readiness: ReadinessConfig {
890 enabled: false,
891 method: ReadinessMethod::Api,
892 initial_delay: Duration::from_millis(100),
893 max_delay: Duration::from_secs(60),
894 max_total_wait: Duration::from_secs(300),
895 poll_interval: Duration::from_secs(5),
896 jitter_factor: 0.25,
897 prefer_index: false,
898 index_path: None,
899 },
900 output_lines: 20,
901 force: false,
902 lock_timeout: Duration::from_secs(30),
903 parallel: ParallelConfig {
904 enabled: false,
905 max_concurrent: 4,
906 per_package_timeout: Duration::from_secs(120),
907 },
908 webhook: WebhookConfig {
909 url: String::new(),
910 secret: None,
911 ..WebhookConfig::default()
912 },
913 encryption: EncryptionConfig {
914 enabled: false,
915 passphrase: None,
916 ..EncryptionConfig::default()
917 },
918 registries: vec![],
919 resume_from: None,
920 rehearsal_registry: None,
921 rehearsal_skip: false,
922 rehearsal_smoke_install: None,
923 }
924 }
925
926 #[test]
927 fn snapshot_default_conversion() {
928 let cfg = default_config_runtime();
929 let converted = into_runtime_options(cfg);
930 assert_debug_snapshot!(converted);
931 }
932
933 #[test]
934 fn snapshot_all_flags_enabled() {
935 let mut cfg = default_config_runtime();
936 cfg.allow_dirty = true;
937 cfg.skip_ownership_check = true;
938 cfg.strict_ownership = true;
939 cfg.no_verify = true;
940 cfg.force_resume = true;
941 cfg.force = true;
942 cfg.parallel.enabled = true;
943 cfg.readiness.enabled = true;
944 cfg.encryption.enabled = true;
945 let converted = into_runtime_options(cfg);
946 assert_debug_snapshot!(converted);
947 }
948
949 #[test]
950 fn snapshot_with_registries() {
951 let mut cfg = default_config_runtime();
952 cfg.registries = vec![
953 Registry {
954 name: "crates-io".to_string(),
955 api_base: "https://crates.io".to_string(),
956 index_base: Some("https://index.crates.io".to_string()),
957 },
958 Registry {
959 name: "private".to_string(),
960 api_base: "https://my-registry.example.com".to_string(),
961 index_base: None,
962 },
963 ];
964 let converted = into_runtime_options(cfg);
965 assert_debug_snapshot!(converted);
966 }
967
968 #[test]
969 fn snapshot_fast_policy_no_verify() {
970 let mut cfg = default_config_runtime();
971 cfg.policy = PublishPolicy::Fast;
972 cfg.verify_mode = VerifyMode::None;
973 cfg.no_verify = true;
974 cfg.max_attempts = 1;
975 cfg.base_delay = Duration::ZERO;
976 cfg.max_delay = Duration::ZERO;
977 let converted = into_runtime_options(cfg);
978 assert_debug_snapshot!(converted);
979 }
980
981 #[test]
982 fn snapshot_full_readiness_config() {
983 let mut cfg = default_config_runtime();
984 cfg.readiness = ReadinessConfig {
985 enabled: true,
986 method: ReadinessMethod::Both,
987 initial_delay: Duration::from_millis(500),
988 max_delay: Duration::from_secs(120),
989 max_total_wait: Duration::from_secs(600),
990 poll_interval: Duration::from_secs(10),
991 jitter_factor: 0.5,
992 prefer_index: true,
993 index_path: Some(PathBuf::from("/custom/index")),
994 };
995 let converted = into_runtime_options(cfg);
996 assert_debug_snapshot!(converted);
997 }
998
999 #[test]
1000 fn snapshot_parallel_heavy() {
1001 let mut cfg = default_config_runtime();
1002 cfg.parallel = ParallelConfig {
1003 enabled: true,
1004 max_concurrent: 16,
1005 per_package_timeout: Duration::from_secs(3600),
1006 };
1007 cfg.lock_timeout = Duration::from_secs(7200);
1008 let converted = into_runtime_options(cfg);
1009 assert_debug_snapshot!(converted);
1010 }
1011
1012 #[test]
1013 fn snapshot_webhook_with_secret() {
1014 let mut cfg = default_config_runtime();
1015 cfg.webhook = WebhookConfig {
1016 url: "https://hooks.slack.com/services/T00/B00/xxxx".to_string(),
1017 secret: Some("hmac-secret-key".to_string()),
1018 timeout_secs: 5,
1019 ..WebhookConfig::default()
1020 };
1021 let converted = into_runtime_options(cfg);
1022 assert_debug_snapshot!(converted);
1023 }
1024
1025 #[test]
1026 fn snapshot_encryption_with_env_var() {
1027 let mut cfg = default_config_runtime();
1028 cfg.encryption = EncryptionConfig {
1029 enabled: true,
1030 passphrase: None,
1031 env_var: Some("CI_ENCRYPT_KEY".to_string()),
1032 };
1033 let converted = into_runtime_options(cfg);
1034 assert_debug_snapshot!(converted);
1035 }
1036
1037 #[test]
1038 fn snapshot_linear_retry_strategy() {
1039 let mut cfg = default_config_runtime();
1040 cfg.retry_strategy = shipper_retry::RetryStrategyType::Linear;
1041 cfg.retry_jitter = 0.0;
1042 cfg.max_attempts = 10;
1043 cfg.base_delay = Duration::from_millis(100);
1044 cfg.max_delay = Duration::from_secs(10);
1045 let converted = into_runtime_options(cfg);
1046 assert_debug_snapshot!(converted);
1047 }
1048
1049 #[test]
1050 fn snapshot_resume_from_set() {
1051 let mut cfg = default_config_runtime();
1052 cfg.resume_from = Some("my-sub-crate".to_string());
1053 cfg.force_resume = true;
1054 let converted = into_runtime_options(cfg);
1055 assert_debug_snapshot!(converted);
1056 }
1057
1058 #[test]
1059 fn snapshot_balanced_policy_with_partial_config() {
1060 let mut cfg = default_config_runtime();
1061 cfg.policy = PublishPolicy::Balanced;
1062 cfg.max_attempts = 5;
1063 cfg.parallel.enabled = true;
1064 cfg.parallel.max_concurrent = 2;
1065 cfg.readiness.enabled = true;
1066 cfg.readiness.method = ReadinessMethod::Api;
1067 let converted = into_runtime_options(cfg);
1068 assert_debug_snapshot!(converted);
1069 }
1070
1071 #[test]
1072 fn snapshot_safe_policy_max_safety() {
1073 let mut cfg = default_config_runtime();
1074 cfg.policy = PublishPolicy::Safe;
1075 cfg.verify_mode = VerifyMode::Workspace;
1076 cfg.readiness = ReadinessConfig {
1077 enabled: true,
1078 method: ReadinessMethod::Both,
1079 initial_delay: Duration::from_secs(1),
1080 max_delay: Duration::from_secs(120),
1081 max_total_wait: Duration::from_secs(600),
1082 poll_interval: Duration::from_secs(5),
1083 jitter_factor: 0.5,
1084 prefer_index: true,
1085 index_path: Some(PathBuf::from("/ci/index")),
1086 };
1087 cfg.max_attempts = 10;
1088 cfg.retry_strategy = shipper_retry::RetryStrategyType::Exponential;
1089 cfg.retry_jitter = 0.5;
1090 let converted = into_runtime_options(cfg);
1091 assert_debug_snapshot!(converted);
1092 }
1093
1094 #[test]
1095 fn snapshot_alternative_registry_only() {
1096 let mut cfg = default_config_runtime();
1097 cfg.registries = vec![Registry {
1098 name: "my-private-registry".to_string(),
1099 api_base: "https://registry.internal.corp:8443".to_string(),
1100 index_base: Some("https://index.internal.corp:8443".to_string()),
1101 }];
1102 let converted = into_runtime_options(cfg);
1103 assert_debug_snapshot!(converted);
1104 }
1105 }
1106
1107 mod flag_precedence {
1109 use super::*;
1110
1111 #[test]
1112 fn cli_true_overrides_source_false_for_allow_dirty() {
1113 let mut opts = sample_runtime_options();
1114 opts.allow_dirty = false;
1115 let converted = into_runtime_options(opts);
1116 assert!(!converted.allow_dirty);
1117
1118 let mut opts2 = sample_runtime_options();
1119 opts2.allow_dirty = true;
1120 let converted2 = into_runtime_options(opts2);
1121 assert!(converted2.allow_dirty);
1122 }
1123
1124 #[test]
1125 fn policy_field_is_faithfully_forwarded() {
1126 for policy in [
1127 PublishPolicy::Safe,
1128 PublishPolicy::Balanced,
1129 PublishPolicy::Fast,
1130 ] {
1131 let mut opts = sample_runtime_options();
1132 opts.policy = policy;
1133 let converted = into_runtime_options(opts);
1134 assert_eq!(converted.policy, policy, "policy mismatch for {policy:?}");
1135 }
1136 }
1137
1138 #[test]
1139 fn verify_mode_field_is_faithfully_forwarded() {
1140 for mode in [VerifyMode::Workspace, VerifyMode::Package, VerifyMode::None] {
1141 let mut opts = sample_runtime_options();
1142 opts.verify_mode = mode;
1143 let converted = into_runtime_options(opts);
1144 assert_eq!(
1145 converted.verify_mode, mode,
1146 "verify_mode mismatch for {mode:?}"
1147 );
1148 }
1149 }
1150
1151 #[test]
1152 fn all_boolean_flags_independently_toggled() {
1153 let flag_indices = [0, 1, 2, 3, 4, 5];
1156
1157 for idx in flag_indices {
1158 let mut opts = sample_runtime_options();
1159 opts.allow_dirty = false;
1160 opts.skip_ownership_check = false;
1161 opts.strict_ownership = false;
1162 opts.no_verify = false;
1163 opts.force = false;
1164 opts.force_resume = false;
1165 match idx {
1166 0 => opts.allow_dirty = true,
1167 1 => opts.skip_ownership_check = true,
1168 2 => opts.strict_ownership = true,
1169 3 => opts.no_verify = true,
1170 4 => opts.force = true,
1171 5 => opts.force_resume = true,
1172 _ => unreachable!(),
1173 }
1174 let converted = into_runtime_options(opts);
1175 let count = [
1176 converted.allow_dirty,
1177 converted.skip_ownership_check,
1178 converted.strict_ownership,
1179 converted.no_verify,
1180 converted.force,
1181 converted.force_resume,
1182 ]
1183 .iter()
1184 .filter(|&&v| v)
1185 .count();
1186 assert_eq!(
1187 count, 1,
1188 "exactly one boolean flag should be true at idx {idx}"
1189 );
1190 }
1191 }
1192
1193 #[test]
1194 fn retry_strategy_variants_all_pass_through() {
1195 for strategy in [
1196 shipper_retry::RetryStrategyType::Immediate,
1197 shipper_retry::RetryStrategyType::Exponential,
1198 shipper_retry::RetryStrategyType::Linear,
1199 shipper_retry::RetryStrategyType::Constant,
1200 ] {
1201 let mut opts = sample_runtime_options();
1202 opts.retry_strategy = strategy;
1203 assert_eq!(into_runtime_options(opts).retry_strategy, strategy);
1204 }
1205 }
1206 }
1207
1208 mod default_value_tests {
1210 use super::*;
1211
1212 fn minimal_defaults() -> RuntimeOptions {
1213 RuntimeOptions {
1214 allow_dirty: false,
1215 skip_ownership_check: false,
1216 strict_ownership: false,
1217 no_verify: false,
1218 max_attempts: 3,
1219 base_delay: Duration::from_secs(1),
1220 max_delay: Duration::from_secs(60),
1221 retry_strategy: shipper_retry::RetryStrategyType::Exponential,
1222 retry_jitter: 0.25,
1223 retry_per_error: shipper_retry::PerErrorConfig::default(),
1224 verify_timeout: Duration::from_secs(60),
1225 verify_poll_interval: Duration::from_secs(5),
1226 state_dir: PathBuf::from(".shipper"),
1227 force_resume: false,
1228 policy: PublishPolicy::Safe,
1229 verify_mode: VerifyMode::Workspace,
1230 readiness: ReadinessConfig::default(),
1231 output_lines: 50,
1232 force: false,
1233 lock_timeout: Duration::from_secs(3600),
1234 parallel: ParallelConfig::default(),
1235 webhook: WebhookConfig::default(),
1236 encryption: EncryptionConfig::default(),
1237 registries: vec![],
1238 resume_from: None,
1239 rehearsal_registry: None,
1240 rehearsal_skip: false,
1241 rehearsal_smoke_install: None,
1242 }
1243 }
1244
1245 #[test]
1246 fn defaults_no_config_no_flags_all_booleans_false() {
1247 let converted = into_runtime_options(minimal_defaults());
1248 assert!(!converted.allow_dirty);
1249 assert!(!converted.skip_ownership_check);
1250 assert!(!converted.strict_ownership);
1251 assert!(!converted.no_verify);
1252 assert!(!converted.force);
1253 assert!(!converted.force_resume);
1254 }
1255
1256 #[test]
1257 fn defaults_policy_is_safe() {
1258 let converted = into_runtime_options(minimal_defaults());
1259 assert_eq!(converted.policy, expected_types::PublishPolicy::Safe);
1260 }
1261
1262 #[test]
1263 fn defaults_verify_mode_is_workspace() {
1264 let converted = into_runtime_options(minimal_defaults());
1265 assert_eq!(converted.verify_mode, expected_types::VerifyMode::Workspace);
1266 }
1267
1268 #[test]
1269 fn defaults_registries_empty() {
1270 let converted = into_runtime_options(minimal_defaults());
1271 assert!(converted.registries.is_empty());
1272 }
1273
1274 #[test]
1275 fn defaults_resume_from_none() {
1276 let converted = into_runtime_options(minimal_defaults());
1277 assert!(converted.resume_from.is_none());
1278 }
1279
1280 #[test]
1281 fn defaults_parallel_disabled() {
1282 let converted = into_runtime_options(minimal_defaults());
1283 assert!(!converted.parallel.enabled);
1284 }
1285
1286 #[test]
1287 fn defaults_encryption_disabled() {
1288 let converted = into_runtime_options(minimal_defaults());
1289 assert!(!converted.encryption.enabled);
1290 assert!(converted.encryption.passphrase.is_none());
1291 assert!(converted.encryption.env_var.is_none());
1292 }
1293
1294 #[test]
1295 fn defaults_webhook_empty() {
1296 let converted = into_runtime_options(minimal_defaults());
1297 assert!(converted.webhook.url.is_empty());
1298 assert!(converted.webhook.secret.is_none());
1299 }
1300 }
1301
1302 mod partial_config_tests {
1304 use super::*;
1305
1306 #[test]
1307 fn partial_only_policy_set() {
1308 let mut opts = sample_runtime_options();
1309 opts.policy = PublishPolicy::Fast;
1310 let converted = into_runtime_options(opts);
1312 assert_eq!(converted.policy, expected_types::PublishPolicy::Fast);
1313 assert!(converted.allow_dirty);
1315 assert_eq!(converted.max_attempts, 8);
1316 }
1317
1318 #[test]
1319 fn partial_only_readiness_set() {
1320 let mut opts = sample_runtime_options();
1321 opts.readiness = ReadinessConfig {
1322 enabled: true,
1323 method: ReadinessMethod::Index,
1324 initial_delay: Duration::from_millis(200),
1325 max_delay: Duration::from_secs(15),
1326 max_total_wait: Duration::from_secs(120),
1327 poll_interval: Duration::from_secs(2),
1328 jitter_factor: 0.1,
1329 index_path: None,
1330 prefer_index: true,
1331 };
1332 let converted = into_runtime_options(opts);
1333 assert!(converted.readiness.enabled);
1334 assert_eq!(
1335 converted.readiness.method,
1336 expected_types::ReadinessMethod::Index
1337 );
1338 assert!(converted.readiness.prefer_index);
1339 assert!(converted.readiness.index_path.is_none());
1340 assert_eq!(converted.policy, expected_types::PublishPolicy::Balanced);
1342 }
1343
1344 #[test]
1345 fn partial_only_parallel_set() {
1346 let mut opts = sample_runtime_options();
1347 opts.parallel = ParallelConfig {
1348 enabled: true,
1349 max_concurrent: 12,
1350 per_package_timeout: Duration::from_secs(600),
1351 };
1352 let converted = into_runtime_options(opts);
1353 assert!(converted.parallel.enabled);
1354 assert_eq!(converted.parallel.max_concurrent, 12);
1355 assert_eq!(
1356 converted.parallel.per_package_timeout,
1357 Duration::from_secs(600)
1358 );
1359 }
1360
1361 #[test]
1362 fn partial_only_encryption_set() {
1363 let mut opts = sample_runtime_options();
1364 opts.encryption = EncryptionConfig {
1365 enabled: true,
1366 passphrase: Some("partial-pass".to_string()),
1367 env_var: None,
1368 };
1369 let converted = into_runtime_options(opts);
1370 assert!(converted.encryption.enabled);
1371 assert_eq!(
1372 converted.encryption.passphrase.as_deref(),
1373 Some("partial-pass")
1374 );
1375 assert!(converted.encryption.env_var.is_none());
1376 }
1377
1378 #[test]
1379 fn partial_only_webhook_set() {
1380 let mut opts = sample_runtime_options();
1381 opts.webhook = WebhookConfig {
1382 url: "https://partial.example/hook".to_string(),
1383 secret: None,
1384 timeout_secs: 10,
1385 ..WebhookConfig::default()
1386 };
1387 let converted = into_runtime_options(opts);
1388 assert_eq!(converted.webhook.url, "https://partial.example/hook");
1389 assert!(converted.webhook.secret.is_none());
1390 assert_eq!(converted.webhook.timeout_secs, 10);
1391 }
1392 }
1393
1394 mod policy_combination_tests {
1396 use super::*;
1397 use insta::assert_debug_snapshot;
1398
1399 fn opts_with_policy(policy: PublishPolicy) -> RuntimeOptions {
1400 let mut opts = RuntimeOptions {
1401 allow_dirty: false,
1402 skip_ownership_check: false,
1403 strict_ownership: false,
1404 no_verify: false,
1405 max_attempts: 5,
1406 base_delay: Duration::from_secs(2),
1407 max_delay: Duration::from_secs(60),
1408 retry_strategy: shipper_retry::RetryStrategyType::Exponential,
1409 retry_jitter: 0.25,
1410 retry_per_error: shipper_retry::PerErrorConfig::default(),
1411 verify_timeout: Duration::from_secs(120),
1412 verify_poll_interval: Duration::from_secs(5),
1413 state_dir: PathBuf::from(".shipper"),
1414 force_resume: false,
1415 policy,
1416 verify_mode: VerifyMode::Workspace,
1417 readiness: ReadinessConfig::default(),
1418 output_lines: 50,
1419 force: false,
1420 lock_timeout: Duration::from_secs(3600),
1421 parallel: ParallelConfig::default(),
1422 webhook: WebhookConfig::default(),
1423 encryption: EncryptionConfig::default(),
1424 registries: vec![],
1425 resume_from: None,
1426 rehearsal_registry: None,
1427 rehearsal_skip: false,
1428 rehearsal_smoke_install: None,
1429 };
1430 match policy {
1432 PublishPolicy::Safe => {
1433 opts.verify_mode = VerifyMode::Workspace;
1434 opts.readiness.enabled = true;
1435 }
1436 PublishPolicy::Balanced => {
1437 opts.verify_mode = VerifyMode::Package;
1438 opts.readiness.enabled = true;
1439 }
1440 PublishPolicy::Fast => {
1441 opts.verify_mode = VerifyMode::None;
1442 opts.no_verify = true;
1443 opts.readiness.enabled = false;
1444 }
1445 }
1446 opts
1447 }
1448
1449 #[test]
1450 fn safe_policy_produces_correct_options() {
1451 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Safe));
1452 assert_eq!(converted.policy, expected_types::PublishPolicy::Safe);
1453 assert_eq!(converted.verify_mode, expected_types::VerifyMode::Workspace);
1454 assert!(converted.readiness.enabled);
1455 assert!(!converted.no_verify);
1456 }
1457
1458 #[test]
1459 fn balanced_policy_produces_correct_options() {
1460 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Balanced));
1461 assert_eq!(converted.policy, expected_types::PublishPolicy::Balanced);
1462 assert_eq!(converted.verify_mode, expected_types::VerifyMode::Package);
1463 assert!(converted.readiness.enabled);
1464 assert!(!converted.no_verify);
1465 }
1466
1467 #[test]
1468 fn fast_policy_produces_correct_options() {
1469 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Fast));
1470 assert_eq!(converted.policy, expected_types::PublishPolicy::Fast);
1471 assert_eq!(converted.verify_mode, expected_types::VerifyMode::None);
1472 assert!(!converted.readiness.enabled);
1473 assert!(converted.no_verify);
1474 }
1475
1476 #[test]
1477 fn snapshot_safe_policy_typical() {
1478 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Safe));
1479 assert_debug_snapshot!(converted);
1480 }
1481
1482 #[test]
1483 fn snapshot_balanced_policy_typical() {
1484 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Balanced));
1485 assert_debug_snapshot!(converted);
1486 }
1487
1488 #[test]
1489 fn snapshot_fast_policy_typical() {
1490 let converted = into_runtime_options(opts_with_policy(PublishPolicy::Fast));
1491 assert_debug_snapshot!(converted);
1492 }
1493 }
1494
1495 mod registry_tests {
1497 use super::*;
1498
1499 #[test]
1500 fn multiple_alternative_registries_preserved() {
1501 let mut opts = sample_runtime_options();
1502 opts.registries = vec![
1503 Registry {
1504 name: "crates-io".to_string(),
1505 api_base: "https://crates.io".to_string(),
1506 index_base: Some("https://index.crates.io".to_string()),
1507 },
1508 Registry {
1509 name: "private-npm".to_string(),
1510 api_base: "https://npm.internal.corp".to_string(),
1511 index_base: None,
1512 },
1513 Registry {
1514 name: "staging".to_string(),
1515 api_base: "https://staging.registry.example.com".to_string(),
1516 index_base: Some("https://staging-index.registry.example.com".to_string()),
1517 },
1518 ];
1519 let converted = into_runtime_options(opts);
1520 assert_eq!(converted.registries.len(), 3);
1521 assert_eq!(converted.registries[0].name, "crates-io");
1522 assert_eq!(converted.registries[1].name, "private-npm");
1523 assert!(converted.registries[1].index_base.is_none());
1524 assert_eq!(converted.registries[2].name, "staging");
1525 assert!(converted.registries[2].index_base.is_some());
1526 }
1527
1528 #[test]
1529 fn registry_with_port_and_path() {
1530 let mut opts = sample_runtime_options();
1531 opts.registries = vec![Registry {
1532 name: "local-dev".to_string(),
1533 api_base: "http://localhost:8080/api/v1".to_string(),
1534 index_base: Some("http://localhost:8080/index".to_string()),
1535 }];
1536 let converted = into_runtime_options(opts);
1537 assert_eq!(
1538 converted.registries[0].api_base,
1539 "http://localhost:8080/api/v1"
1540 );
1541 assert_eq!(
1542 converted.registries[0].index_base.as_deref(),
1543 Some("http://localhost:8080/index")
1544 );
1545 }
1546
1547 #[test]
1548 fn registry_order_is_preserved() {
1549 let names: Vec<String> = (0..10).map(|i| format!("reg-{i}")).collect();
1550 let mut opts = sample_runtime_options();
1551 opts.registries = names
1552 .iter()
1553 .map(|n| Registry {
1554 name: n.clone(),
1555 api_base: format!("https://{n}.example.com"),
1556 index_base: None,
1557 })
1558 .collect();
1559 let converted = into_runtime_options(opts);
1560 let converted_names: Vec<&str> = converted
1561 .registries
1562 .iter()
1563 .map(|r| r.name.as_str())
1564 .collect();
1565 let expected_names: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
1566 assert_eq!(converted_names, expected_names);
1567 }
1568
1569 #[test]
1570 fn registry_with_all_fields_populated() {
1571 let mut opts = sample_runtime_options();
1572 opts.registries = vec![Registry {
1573 name: "full-config".to_string(),
1574 api_base: "https://full.example.com/api".to_string(),
1575 index_base: Some("https://full.example.com/index".to_string()),
1576 }];
1577 let converted = into_runtime_options(opts);
1578 assert_eq!(converted.registries.len(), 1);
1579 let r = &converted.registries[0];
1580 assert_eq!(r.name, "full-config");
1581 assert_eq!(r.api_base, "https://full.example.com/api");
1582 assert_eq!(
1583 r.index_base.as_deref(),
1584 Some("https://full.example.com/index")
1585 );
1586 }
1587 }
1588
1589 mod proptest_hardened {
1591 use super::*;
1592
1593 proptest! {
1594 #[test]
1595 fn arbitrary_durations_survive_conversion(
1596 base_ms in 0u64..100_000,
1597 max_ms in 0u64..100_000,
1598 verify_ms in 0u64..100_000,
1599 poll_ms in 0u64..100_000,
1600 lock_ms in 0u64..100_000,
1601 pkg_timeout_ms in 0u64..100_000,
1602 ) {
1603 let mut opts = sample_runtime_options();
1604 opts.base_delay = Duration::from_millis(base_ms);
1605 opts.max_delay = Duration::from_millis(max_ms);
1606 opts.verify_timeout = Duration::from_millis(verify_ms);
1607 opts.verify_poll_interval = Duration::from_millis(poll_ms);
1608 opts.lock_timeout = Duration::from_millis(lock_ms);
1609 opts.parallel.per_package_timeout = Duration::from_millis(pkg_timeout_ms);
1610
1611 let converted = into_runtime_options(opts);
1612
1613 prop_assert_eq!(converted.base_delay, Duration::from_millis(base_ms));
1614 prop_assert_eq!(converted.max_delay, Duration::from_millis(max_ms));
1615 prop_assert_eq!(converted.verify_timeout, Duration::from_millis(verify_ms));
1616 prop_assert_eq!(converted.verify_poll_interval, Duration::from_millis(poll_ms));
1617 prop_assert_eq!(converted.lock_timeout, Duration::from_millis(lock_ms));
1618 prop_assert_eq!(
1619 converted.parallel.per_package_timeout,
1620 Duration::from_millis(pkg_timeout_ms)
1621 );
1622 }
1623
1624 #[test]
1625 fn arbitrary_string_fields_survive_conversion(
1626 webhook_url in "\\PC{0,64}",
1627 secret in proptest::option::of("\\PC{0,32}"),
1628 passphrase in proptest::option::of("\\PC{0,32}"),
1629 env_var in proptest::option::of("[A-Z_]{1,32}"),
1630 resume in proptest::option::of("[a-z0-9_-]{1,32}"),
1631 reg_count in 0usize..5,
1632 ) {
1633 let mut opts = sample_runtime_options();
1634 opts.webhook.url = webhook_url.clone();
1635 opts.webhook.secret = secret.clone();
1636 opts.encryption.passphrase = passphrase.clone();
1637 opts.encryption.env_var = env_var.clone();
1638 opts.resume_from = resume.clone();
1639 opts.registries = (0..reg_count)
1640 .map(|i| Registry {
1641 name: format!("r-{i}"),
1642 api_base: format!("https://r{i}.example"),
1643 index_base: None,
1644 })
1645 .collect();
1646
1647 let converted = into_runtime_options(opts);
1648
1649 prop_assert_eq!(&converted.webhook.url, &webhook_url);
1650 prop_assert_eq!(&converted.webhook.secret, &secret);
1651 prop_assert_eq!(&converted.encryption.passphrase, &passphrase);
1652 prop_assert_eq!(&converted.encryption.env_var, &env_var);
1653 prop_assert_eq!(&converted.resume_from, &resume);
1654 prop_assert_eq!(converted.registries.len(), reg_count);
1655 }
1656 }
1657 }
1658
1659 mod composite_tests {
1661 use super::*;
1662
1663 #[test]
1664 fn full_config_all_fields_populated_roundtrips() {
1665 let original = sample_runtime_options();
1666 let converted = into_runtime_options(original.clone());
1667
1668 assert_eq!(converted.allow_dirty, original.allow_dirty);
1669 assert_eq!(
1670 converted.skip_ownership_check,
1671 original.skip_ownership_check
1672 );
1673 assert_eq!(converted.strict_ownership, original.strict_ownership);
1674 assert_eq!(converted.no_verify, original.no_verify);
1675 assert_eq!(converted.max_attempts, original.max_attempts);
1676 assert_eq!(converted.base_delay, original.base_delay);
1677 assert_eq!(converted.max_delay, original.max_delay);
1678 assert_eq!(converted.retry_strategy, original.retry_strategy);
1679 assert!((converted.retry_jitter - original.retry_jitter).abs() < f64::EPSILON);
1680 assert_eq!(converted.verify_timeout, original.verify_timeout);
1681 assert_eq!(
1682 converted.verify_poll_interval,
1683 original.verify_poll_interval
1684 );
1685 assert_eq!(converted.state_dir, original.state_dir);
1686 assert_eq!(converted.force_resume, original.force_resume);
1687 assert_eq!(converted.policy, original.policy);
1688 assert_eq!(converted.verify_mode, original.verify_mode);
1689 assert_eq!(converted.readiness.enabled, original.readiness.enabled);
1690 assert_eq!(converted.readiness.method, original.readiness.method);
1691 assert_eq!(
1692 converted.readiness.initial_delay,
1693 original.readiness.initial_delay
1694 );
1695 assert_eq!(converted.readiness.max_delay, original.readiness.max_delay);
1696 assert_eq!(
1697 converted.readiness.max_total_wait,
1698 original.readiness.max_total_wait
1699 );
1700 assert_eq!(
1701 converted.readiness.poll_interval,
1702 original.readiness.poll_interval
1703 );
1704 assert!(
1705 (converted.readiness.jitter_factor - original.readiness.jitter_factor).abs()
1706 < f64::EPSILON
1707 );
1708 assert_eq!(
1709 converted.readiness.index_path,
1710 original.readiness.index_path
1711 );
1712 assert_eq!(
1713 converted.readiness.prefer_index,
1714 original.readiness.prefer_index
1715 );
1716 assert_eq!(converted.output_lines, original.output_lines);
1717 assert_eq!(converted.force, original.force);
1718 assert_eq!(converted.lock_timeout, original.lock_timeout);
1719 assert_eq!(converted.parallel.enabled, original.parallel.enabled);
1720 assert_eq!(
1721 converted.parallel.max_concurrent,
1722 original.parallel.max_concurrent
1723 );
1724 assert_eq!(
1725 converted.parallel.per_package_timeout,
1726 original.parallel.per_package_timeout
1727 );
1728 assert_eq!(converted.webhook.url, original.webhook.url);
1729 assert_eq!(converted.webhook.secret, original.webhook.secret);
1730 assert_eq!(
1731 converted.webhook.timeout_secs,
1732 original.webhook.timeout_secs
1733 );
1734 assert_eq!(converted.encryption.enabled, original.encryption.enabled);
1735 assert_eq!(
1736 converted.encryption.passphrase,
1737 original.encryption.passphrase
1738 );
1739 assert_eq!(converted.encryption.env_var, original.encryption.env_var);
1740 assert_eq!(converted.registries.len(), original.registries.len());
1741 for (c, o) in converted.registries.iter().zip(original.registries.iter()) {
1742 assert_eq!(c.name, o.name);
1743 assert_eq!(c.api_base, o.api_base);
1744 assert_eq!(c.index_base, o.index_base);
1745 }
1746 assert_eq!(converted.resume_from, original.resume_from);
1747 }
1748
1749 #[test]
1750 fn extreme_values_combined() {
1751 let opts = RuntimeOptions {
1752 allow_dirty: true,
1753 skip_ownership_check: true,
1754 strict_ownership: true,
1755 no_verify: true,
1756 max_attempts: u32::MAX,
1757 base_delay: Duration::ZERO,
1758 max_delay: Duration::from_secs(u64::MAX / 2),
1759 retry_strategy: shipper_retry::RetryStrategyType::Immediate,
1760 retry_jitter: 0.0,
1761 retry_per_error: shipper_retry::PerErrorConfig::default(),
1762 verify_timeout: Duration::ZERO,
1763 verify_poll_interval: Duration::from_nanos(1),
1764 state_dir: PathBuf::from(""),
1765 force_resume: true,
1766 policy: PublishPolicy::Fast,
1767 verify_mode: VerifyMode::None,
1768 readiness: ReadinessConfig {
1769 enabled: false,
1770 method: ReadinessMethod::Api,
1771 initial_delay: Duration::ZERO,
1772 max_delay: Duration::ZERO,
1773 max_total_wait: Duration::ZERO,
1774 poll_interval: Duration::ZERO,
1775 jitter_factor: 0.0,
1776 index_path: None,
1777 prefer_index: false,
1778 },
1779 output_lines: 0,
1780 force: true,
1781 lock_timeout: Duration::ZERO,
1782 parallel: ParallelConfig {
1783 enabled: false,
1784 max_concurrent: 0,
1785 per_package_timeout: Duration::ZERO,
1786 },
1787 webhook: WebhookConfig::default(),
1788 encryption: EncryptionConfig::default(),
1789 registries: vec![],
1790 resume_from: Some(String::new()),
1791 rehearsal_registry: None,
1792 rehearsal_skip: false,
1793 rehearsal_smoke_install: None,
1794 };
1795
1796 let converted = into_runtime_options(opts);
1797 assert_eq!(converted.max_attempts, u32::MAX);
1798 assert_eq!(converted.base_delay, Duration::ZERO);
1799 assert_eq!(converted.output_lines, 0);
1800 assert_eq!(converted.state_dir, PathBuf::from(""));
1801 assert_eq!(converted.resume_from.as_deref(), Some(""));
1802 }
1803 }
1804}