1use std::env;
8use std::path::{Path, PathBuf};
9
10use super::credentials::{CREDENTIALS_FILE, token_from_credentials_file};
11
12pub const CRATES_IO_REGISTRY: &str = "crates-io";
14
15pub const CARGO_REGISTRY_TOKEN_ENV: &str = "CARGO_REGISTRY_TOKEN";
17
18pub const CARGO_REGISTRIES_TOKEN_PREFIX: &str = "CARGO_REGISTRIES_";
20
21pub const CARGO_HOME_ENV: &str = "CARGO_HOME";
23
24#[derive(Debug, Clone)]
29pub struct AuthInfo {
30 pub token: Option<String>,
32 pub source: TokenSource,
34 pub detected: bool,
36}
37
38impl Default for AuthInfo {
39 fn default() -> Self {
40 Self {
41 token: None,
42 source: TokenSource::None,
43 detected: false,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum TokenSource {
54 None,
56 EnvDefault,
58 EnvRegistry,
60 CredentialsFile,
62}
63
64impl std::fmt::Display for TokenSource {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 TokenSource::None => write!(f, "none"),
68 TokenSource::EnvDefault => write!(f, "CARGO_REGISTRY_TOKEN"),
69 TokenSource::EnvRegistry => write!(f, "CARGO_REGISTRIES_<NAME>_TOKEN"),
70 TokenSource::CredentialsFile => write!(f, "credentials.toml"),
71 }
72 }
73}
74
75pub fn resolve_token(registry: &str, cargo_home: Option<&Path>) -> AuthInfo {
88 if (registry == CRATES_IO_REGISTRY || registry.is_empty())
90 && let Ok(token) = env::var(CARGO_REGISTRY_TOKEN_ENV)
91 && !token.is_empty()
92 {
93 return AuthInfo {
94 token: Some(token),
95 source: TokenSource::EnvDefault,
96 detected: true,
97 };
98 }
99
100 let env_var = format!(
102 "{}{}_TOKEN",
103 CARGO_REGISTRIES_TOKEN_PREFIX,
104 registry.to_uppercase().replace('-', "_")
105 );
106 if let Ok(token) = env::var(&env_var)
107 && !token.is_empty()
108 {
109 return AuthInfo {
110 token: Some(token),
111 source: TokenSource::EnvRegistry,
112 detected: true,
113 };
114 }
115
116 let home = cargo_home_path(cargo_home);
118 let credentials_path = home.join(CREDENTIALS_FILE);
119
120 if let Ok(token) = token_from_credentials_file(&credentials_path, registry) {
121 return AuthInfo {
122 token: Some(token),
123 source: TokenSource::CredentialsFile,
124 detected: true,
125 };
126 }
127
128 AuthInfo::default()
129}
130
131pub fn has_token(registry: &str, cargo_home: Option<&Path>) -> bool {
136 resolve_token(registry, cargo_home).detected
137}
138
139pub fn cargo_home_path(cargo_home: Option<&Path>) -> PathBuf {
147 if let Some(path) = cargo_home {
148 return path.to_path_buf();
149 }
150
151 if let Ok(path) = env::var(CARGO_HOME_ENV) {
152 return PathBuf::from(path);
153 }
154
155 if let Some(home) = dirs::home_dir() {
156 return home.join(".cargo");
157 }
158
159 PathBuf::from(".cargo")
160}
161
162pub fn mask_token(token: &str) -> String {
167 if token.len() <= 8 {
168 return "*".repeat(token.len());
169 }
170 format!("{}****{}", &token[..4], &token[token.len() - 4..])
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use tempfile::tempdir;
177
178 #[test]
179 fn mask_token_short() {
180 assert_eq!(mask_token("abc"), "***");
181 assert_eq!(mask_token("abcdefgh"), "********");
182 }
183
184 #[test]
185 fn mask_token_long() {
186 assert_eq!(mask_token("abcdefghijklmnop"), "abcd****mnop");
187 }
188
189 #[test]
190 fn mask_token_empty() {
191 assert_eq!(mask_token(""), "");
192 }
193
194 #[test]
195 fn mask_token_boundary_nine_chars() {
196 assert_eq!(mask_token("123456789"), "1234****6789");
197 }
198
199 #[test]
200 fn mask_token_exactly_eight_chars() {
201 assert_eq!(mask_token("12345678"), "********");
202 }
203
204 #[test]
205 fn mask_token_single_char() {
206 assert_eq!(mask_token("x"), "*");
207 }
208
209 #[test]
210 fn cargo_home_path_uses_env() {
211 let td = tempdir().expect("tempdir");
212 let path = cargo_home_path(Some(td.path()));
213 assert_eq!(path, td.path());
214 }
215
216 #[test]
217 fn cargo_home_path_explicit_overrides_env() {
218 let explicit = tempdir().expect("tempdir");
219 temp_env::with_var(CARGO_HOME_ENV, Some("/some/other/path"), || {
220 let path = cargo_home_path(Some(explicit.path()));
221 assert_eq!(path, explicit.path());
222 });
223 }
224
225 #[test]
226 fn cargo_home_path_falls_back_to_env_var() {
227 let td = tempdir().expect("tempdir");
228 temp_env::with_var(CARGO_HOME_ENV, Some(td.path().to_str().unwrap()), || {
229 let path = cargo_home_path(None);
230 assert_eq!(path, td.path());
231 });
232 }
233
234 #[test]
235 fn cargo_home_path_no_env_falls_to_home_dir() {
236 temp_env::with_var(CARGO_HOME_ENV, None::<&str>, || {
237 let path = cargo_home_path(None);
238 assert!(path.to_str().unwrap().contains(".cargo"));
239 });
240 }
241
242 #[test]
243 fn resolve_token_from_env_default() {
244 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("test-token"), || {
245 let auth = resolve_token(CRATES_IO_REGISTRY, None);
246 assert!(auth.detected);
247 assert_eq!(auth.token, Some("test-token".to_string()));
248 assert_eq!(auth.source, TokenSource::EnvDefault);
249 });
250 }
251
252 #[test]
253 fn resolve_token_from_env_registry() {
254 temp_env::with_var(
255 "CARGO_REGISTRIES_MY_REGISTRY_TOKEN",
256 Some("custom-token"),
257 || {
258 let auth = resolve_token("my-registry", None);
259 assert!(auth.detected);
260 assert_eq!(auth.token, Some("custom-token".to_string()));
261 assert_eq!(auth.source, TokenSource::EnvRegistry);
262 },
263 );
264 }
265
266 #[test]
267 fn resolve_token_none_found() {
268 temp_env::with_vars(
269 [
270 (CARGO_REGISTRY_TOKEN_ENV, None::<String>),
271 ("CARGO_REGISTRIES_TEST_TOKEN", None::<String>),
272 ],
273 || {
274 let auth = resolve_token("test", None);
275 assert!(!auth.detected);
276 assert!(auth.token.is_none());
277 },
278 );
279 }
280
281 #[test]
282 fn token_source_display() {
283 assert_eq!(TokenSource::None.to_string(), "none");
284 assert_eq!(TokenSource::EnvDefault.to_string(), "CARGO_REGISTRY_TOKEN");
285 assert_eq!(
286 TokenSource::EnvRegistry.to_string(),
287 "CARGO_REGISTRIES_<NAME>_TOKEN"
288 );
289 assert_eq!(TokenSource::CredentialsFile.to_string(), "credentials.toml");
290 }
291
292 #[test]
293 fn auth_info_default_values() {
294 let info = AuthInfo::default();
295 assert!(info.token.is_none());
296 assert_eq!(info.source, TokenSource::None);
297 assert!(!info.detected);
298 }
299
300 #[test]
301 fn resolve_token_empty_env_is_skipped() {
302 temp_env::with_vars(
303 [
304 (CARGO_REGISTRY_TOKEN_ENV, Some("")),
305 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
306 ],
307 || {
308 let td = tempdir().expect("tempdir");
309 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
310 assert!(!auth.detected);
311 assert!(auth.token.is_none());
312 assert_eq!(auth.source, TokenSource::None);
313 },
314 );
315 }
316
317 #[test]
318 fn resolve_token_empty_registry_specific_env_is_skipped() {
319 temp_env::with_var("CARGO_REGISTRIES_MY_REG_TOKEN", Some(""), || {
320 let td = tempdir().expect("tempdir");
321 let auth = resolve_token("my-reg", Some(td.path()));
322 assert!(!auth.detected);
323 assert!(auth.token.is_none());
324 });
325 }
326
327 #[test]
328 fn resolve_token_empty_registry_name_uses_default_env() {
329 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("default-tok"), || {
330 let auth = resolve_token("", None);
331 assert!(auth.detected);
332 assert_eq!(auth.token, Some("default-tok".to_string()));
333 assert_eq!(auth.source, TokenSource::EnvDefault);
334 });
335 }
336
337 #[test]
338 fn resolve_token_custom_registry_ignores_default_env() {
339 temp_env::with_vars(
340 [
341 (CARGO_REGISTRY_TOKEN_ENV, Some("default-tok")),
342 ("CARGO_REGISTRIES_CUSTOM_REG_TOKEN", None::<&str>),
343 ],
344 || {
345 let td = tempdir().expect("tempdir");
346 let auth = resolve_token("custom-reg", Some(td.path()));
347 assert!(!auth.detected);
348 },
349 );
350 }
351
352 #[test]
353 fn resolve_token_env_default_takes_priority_over_credentials() {
354 let td = tempdir().expect("tempdir");
355 let creds = td.path().join(CREDENTIALS_FILE);
356 std::fs::write(&creds, "[registry]\ntoken = \"creds-token\"\n").expect("write");
357
358 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("env-token"), || {
359 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
360 assert!(auth.detected);
361 assert_eq!(auth.token, Some("env-token".to_string()));
362 assert_eq!(auth.source, TokenSource::EnvDefault);
363 });
364 }
365
366 #[test]
367 fn resolve_token_env_registry_takes_priority_over_credentials() {
368 let td = tempdir().expect("tempdir");
369 let creds = td.path().join(CREDENTIALS_FILE);
370 std::fs::write(
371 &creds,
372 "[registries.my-registry]\ntoken = \"creds-token\"\n",
373 )
374 .expect("write");
375
376 temp_env::with_var(
377 "CARGO_REGISTRIES_MY_REGISTRY_TOKEN",
378 Some("env-token"),
379 || {
380 let auth = resolve_token("my-registry", Some(td.path()));
381 assert!(auth.detected);
382 assert_eq!(auth.token, Some("env-token".to_string()));
383 assert_eq!(auth.source, TokenSource::EnvRegistry);
384 },
385 );
386 }
387
388 #[test]
389 fn resolve_token_falls_through_to_credentials_file() {
390 let td = tempdir().expect("tempdir");
391 let creds = td.path().join(CREDENTIALS_FILE);
392 std::fs::write(&creds, "[registry]\ntoken = \"file-token\"\n").expect("write");
393
394 temp_env::with_vars(
395 [
396 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
397 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
398 ],
399 || {
400 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
401 assert!(auth.detected);
402 assert_eq!(auth.token, Some("file-token".to_string()));
403 assert_eq!(auth.source, TokenSource::CredentialsFile);
404 },
405 );
406 }
407
408 #[test]
409 fn resolve_token_custom_registry_from_credentials_file() {
410 let td = tempdir().expect("tempdir");
411 let creds = td.path().join(CREDENTIALS_FILE);
412 std::fs::write(&creds, "[registries.private-reg]\ntoken = \"priv-token\"\n")
413 .expect("write");
414
415 temp_env::with_var("CARGO_REGISTRIES_PRIVATE_REG_TOKEN", None::<&str>, || {
416 let auth = resolve_token("private-reg", Some(td.path()));
417 assert!(auth.detected);
418 assert_eq!(auth.token, Some("priv-token".to_string()));
419 assert_eq!(auth.source, TokenSource::CredentialsFile);
420 });
421 }
422
423 #[test]
424 fn has_token_returns_true_when_found() {
425 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("tok"), || {
426 assert!(has_token(CRATES_IO_REGISTRY, None));
427 });
428 }
429
430 #[test]
431 fn has_token_returns_false_when_missing() {
432 temp_env::with_vars(
433 [
434 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
435 ("CARGO_REGISTRIES_NOEXIST_TOKEN", None::<&str>),
436 ],
437 || {
438 let td = tempdir().expect("tempdir");
439 assert!(!has_token("noexist", Some(td.path())));
440 },
441 );
442 }
443
444 #[test]
445 fn token_source_equality() {
446 assert_eq!(TokenSource::None, TokenSource::None);
447 assert_eq!(TokenSource::EnvDefault, TokenSource::EnvDefault);
448 assert_ne!(TokenSource::EnvDefault, TokenSource::EnvRegistry);
449 assert_ne!(TokenSource::CredentialsFile, TokenSource::None);
450 }
451
452 #[test]
453 fn resolve_token_registry_name_with_hyphens_maps_to_underscores() {
454 temp_env::with_var(
455 "CARGO_REGISTRIES_MY_CUSTOM_REG_TOKEN",
456 Some("hyphen-tok"),
457 || {
458 let auth = resolve_token("my-custom-reg", None);
459 assert!(auth.detected);
460 assert_eq!(auth.token, Some("hyphen-tok".to_string()));
461 assert_eq!(auth.source, TokenSource::EnvRegistry);
462 },
463 );
464 }
465
466 #[test]
467 fn resolve_token_registry_name_uppercased() {
468 temp_env::with_var("CARGO_REGISTRIES_MYREG_TOKEN", Some("upper-tok"), || {
469 let auth = resolve_token("myReg", None);
470 assert!(auth.detected);
471 assert_eq!(auth.token, Some("upper-tok".to_string()));
472 assert_eq!(auth.source, TokenSource::EnvRegistry);
473 });
474 }
475
476 #[test]
477 fn env_default_over_env_registry_for_crates_io() {
478 temp_env::with_vars(
479 [
480 (CARGO_REGISTRY_TOKEN_ENV, Some("env-default")),
481 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", Some("env-registry")),
482 ],
483 || {
484 let auth = resolve_token(CRATES_IO_REGISTRY, None);
485 assert!(auth.detected);
486 assert_eq!(auth.token, Some("env-default".to_string()));
487 assert_eq!(auth.source, TokenSource::EnvDefault);
488 },
489 );
490 }
491
492 #[test]
493 fn env_registry_used_when_default_unset_for_crates_io() {
494 let td = tempdir().expect("tempdir");
495 temp_env::with_vars(
496 [
497 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
498 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", Some("env-registry")),
499 ],
500 || {
501 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
502 assert!(auth.detected);
503 assert_eq!(auth.token, Some("env-registry".to_string()));
504 assert_eq!(auth.source, TokenSource::EnvRegistry);
505 },
506 );
507 }
508
509 #[test]
510 fn full_precedence_chain_env_default_wins() {
511 let td = tempdir().expect("tempdir");
512 let creds = td.path().join(CREDENTIALS_FILE);
513 std::fs::write(&creds, "[registry]\ntoken = \"file-token\"\n").expect("write");
514
515 temp_env::with_vars(
516 [
517 (CARGO_REGISTRY_TOKEN_ENV, Some("env-default")),
518 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", Some("env-registry")),
519 ],
520 || {
521 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
522 assert_eq!(auth.source, TokenSource::EnvDefault);
523 assert_eq!(auth.token, Some("env-default".to_string()));
524 },
525 );
526 }
527
528 #[test]
529 fn full_precedence_chain_env_registry_second() {
530 let td = tempdir().expect("tempdir");
531 let creds = td.path().join(CREDENTIALS_FILE);
532 std::fs::write(&creds, "[registry]\ntoken = \"file-token\"\n").expect("write");
533
534 temp_env::with_vars(
535 [
536 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
537 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", Some("env-registry")),
538 ],
539 || {
540 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
541 assert_eq!(auth.source, TokenSource::EnvRegistry);
542 assert_eq!(auth.token, Some("env-registry".to_string()));
543 },
544 );
545 }
546
547 #[test]
548 fn full_precedence_chain_file_last() {
549 let td = tempdir().expect("tempdir");
550 let creds = td.path().join(CREDENTIALS_FILE);
551 std::fs::write(&creds, "[registry]\ntoken = \"file-token\"\n").expect("write");
552
553 temp_env::with_vars(
554 [
555 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
556 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
557 ],
558 || {
559 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
560 assert_eq!(auth.source, TokenSource::CredentialsFile);
561 assert_eq!(auth.token, Some("file-token".to_string()));
562 },
563 );
564 }
565
566 #[test]
567 fn resolve_token_whitespace_only_env_is_not_skipped() {
568 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some(" "), || {
569 let auth = resolve_token(CRATES_IO_REGISTRY, None);
570 assert!(auth.detected);
571 assert_eq!(auth.token, Some(" ".to_string()));
572 });
573 }
574
575 #[test]
576 fn resolve_token_very_long_env_token() {
577 let long_token = "x".repeat(10_000);
578 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some(long_token.as_str()), || {
579 let auth = resolve_token(CRATES_IO_REGISTRY, None);
580 assert!(auth.detected);
581 assert_eq!(auth.token.as_deref(), Some(long_token.as_str()));
582 });
583 }
584
585 #[test]
586 fn resolve_token_env_with_unicode() {
587 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("tök€n_πλ∞"), || {
588 let auth = resolve_token(CRATES_IO_REGISTRY, None);
589 assert!(auth.detected);
590 assert_eq!(auth.token, Some("tök€n_πλ∞".to_string()));
591 });
592 }
593
594 #[test]
595 fn resolve_token_env_with_tabs() {
596 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("token\twith\ttabs"), || {
597 let auth = resolve_token(CRATES_IO_REGISTRY, None);
598 assert!(auth.detected);
599 assert_eq!(auth.token, Some("token\twith\ttabs".to_string()));
600 });
601 }
602
603 #[test]
604 fn resolve_token_env_with_newlines() {
605 temp_env::with_var(
606 CARGO_REGISTRY_TOKEN_ENV,
607 Some("token\nwith\nnewlines"),
608 || {
609 let auth = resolve_token(CRATES_IO_REGISTRY, None);
610 assert!(auth.detected);
611 assert_eq!(auth.token, Some("token\nwith\nnewlines".to_string()));
612 },
613 );
614 }
615
616 #[test]
617 fn resolve_token_no_cargo_home_no_env_no_creds() {
618 temp_env::with_vars(
619 [
620 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
621 (CARGO_HOME_ENV, None::<&str>),
622 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
623 ],
624 || {
625 let td = tempdir().expect("tempdir");
626 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
627 assert!(!auth.detected);
628 assert!(auth.token.is_none());
629 assert_eq!(auth.source, TokenSource::None);
630 },
631 );
632 }
633
634 #[test]
635 fn resolve_token_multiple_registries_independent() {
636 temp_env::with_vars(
637 [
638 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
639 ("CARGO_REGISTRIES_ALPHA_TOKEN", Some("alpha-token")),
640 ("CARGO_REGISTRIES_BETA_TOKEN", Some("beta-token")),
641 ],
642 || {
643 let auth_a = resolve_token("alpha", None);
644 let auth_b = resolve_token("beta", None);
645 assert_eq!(auth_a.token, Some("alpha-token".to_string()));
646 assert_eq!(auth_b.token, Some("beta-token".to_string()));
647 assert_eq!(auth_a.source, TokenSource::EnvRegistry);
648 assert_eq!(auth_b.source, TokenSource::EnvRegistry);
649 },
650 );
651 }
652
653 #[test]
654 fn resolve_token_registry_with_numbers_in_name() {
655 temp_env::with_var("CARGO_REGISTRIES_REG123_TOKEN", Some("num-tok"), || {
656 let auth = resolve_token("reg123", None);
657 assert!(auth.detected);
658 assert_eq!(auth.token, Some("num-tok".to_string()));
659 });
660 }
661
662 #[test]
663 fn resolve_token_registry_single_char_name() {
664 temp_env::with_var("CARGO_REGISTRIES_X_TOKEN", Some("x-tok"), || {
665 let auth = resolve_token("x", None);
666 assert!(auth.detected);
667 assert_eq!(auth.token, Some("x-tok".to_string()));
668 assert_eq!(auth.source, TokenSource::EnvRegistry);
669 });
670 }
671
672 mod snapshots {
675 use super::*;
676 use insta::assert_debug_snapshot;
677 use tempfile::tempdir;
678
679 #[test]
680 fn snapshot_resolve_token_from_env_default() {
681 temp_env::with_vars(
682 [
683 (CARGO_REGISTRY_TOKEN_ENV, Some("cio-secret-token-value")),
684 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
685 ],
686 || {
687 let auth = resolve_token(CRATES_IO_REGISTRY, None);
688 assert_debug_snapshot!(auth);
689 },
690 );
691 }
692
693 #[test]
694 fn snapshot_resolve_token_from_env_registry() {
695 temp_env::with_vars(
696 [
697 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
698 ("CARGO_REGISTRIES_MY_REGISTRY_TOKEN", Some("my-reg-token")),
699 ],
700 || {
701 let td = tempdir().expect("tempdir");
702 let auth = resolve_token("my-registry", Some(td.path()));
703 assert_debug_snapshot!(auth);
704 },
705 );
706 }
707
708 #[test]
709 fn snapshot_resolve_token_none_found() {
710 temp_env::with_vars(
711 [
712 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
713 ("CARGO_REGISTRIES_MISSING_TOKEN", None::<&str>),
714 ],
715 || {
716 let td = tempdir().expect("tempdir");
717 let auth = resolve_token("missing", Some(td.path()));
718 assert_debug_snapshot!(auth);
719 },
720 );
721 }
722
723 #[test]
724 fn snapshot_resolve_token_from_credentials_file() {
725 let td = tempdir().expect("tempdir");
726 let creds = td.path().join(CREDENTIALS_FILE);
727 std::fs::write(&creds, "[registry]\ntoken = \"file-secret-token\"\n").expect("write");
728
729 temp_env::with_vars(
730 [
731 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
732 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
733 ],
734 || {
735 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
736 assert_debug_snapshot!(auth);
737 },
738 );
739 }
740
741 #[test]
742 fn snapshot_resolve_token_custom_registry_from_credentials() {
743 let td = tempdir().expect("tempdir");
744 let creds = td.path().join(CREDENTIALS_FILE);
745 std::fs::write(
746 &creds,
747 "[registries.private-reg]\ntoken = \"priv-token-abc\"\n",
748 )
749 .expect("write");
750
751 temp_env::with_var("CARGO_REGISTRIES_PRIVATE_REG_TOKEN", None::<&str>, || {
752 let auth = resolve_token("private-reg", Some(td.path()));
753 assert_debug_snapshot!(auth);
754 });
755 }
756
757 #[test]
758 fn snapshot_auth_info_default() {
759 let info = AuthInfo::default();
760 assert_debug_snapshot!(info);
761 }
762 }
763
764 mod edge_snapshots {
767 use super::*;
768 use insta::assert_debug_snapshot;
769
770 #[test]
771 fn snapshot_token_source_none() {
772 assert_debug_snapshot!(TokenSource::None);
773 }
774
775 #[test]
776 fn snapshot_token_source_env_default() {
777 assert_debug_snapshot!(TokenSource::EnvDefault);
778 }
779
780 #[test]
781 fn snapshot_token_source_env_registry() {
782 assert_debug_snapshot!(TokenSource::EnvRegistry);
783 }
784
785 #[test]
786 fn snapshot_token_source_credentials_file() {
787 assert_debug_snapshot!(TokenSource::CredentialsFile);
788 }
789
790 #[test]
791 fn snapshot_auth_info_with_env_default() {
792 let info = AuthInfo {
793 token: Some("tok-from-env".to_string()),
794 source: TokenSource::EnvDefault,
795 detected: true,
796 };
797 assert_debug_snapshot!(info);
798 }
799
800 #[test]
801 fn snapshot_auth_info_with_env_registry() {
802 let info = AuthInfo {
803 token: Some("tok-from-registry-env".to_string()),
804 source: TokenSource::EnvRegistry,
805 detected: true,
806 };
807 assert_debug_snapshot!(info);
808 }
809
810 #[test]
811 fn snapshot_auth_info_with_credentials_file() {
812 let info = AuthInfo {
813 token: Some("tok-from-file".to_string()),
814 source: TokenSource::CredentialsFile,
815 detected: true,
816 };
817 assert_debug_snapshot!(info);
818 }
819
820 #[test]
821 fn snapshot_auth_info_not_detected() {
822 let info = AuthInfo {
823 token: None,
824 source: TokenSource::None,
825 detected: false,
826 };
827 assert_debug_snapshot!(info);
828 }
829
830 #[test]
831 fn snapshot_resolve_whitespace_token() {
832 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some(" "), || {
833 let auth = resolve_token(CRATES_IO_REGISTRY, None);
834 assert_debug_snapshot!(auth);
835 });
836 }
837
838 #[test]
839 fn snapshot_resolve_unicode_token() {
840 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some("tök€n_πλ∞"), || {
841 let auth = resolve_token(CRATES_IO_REGISTRY, None);
842 assert_debug_snapshot!(auth);
843 });
844 }
845
846 #[test]
847 fn snapshot_mask_token_short_values() {
848 let results: Vec<String> = ["", "a", "ab", "abcd", "abcdefgh"]
849 .iter()
850 .map(|t| mask_token(t))
851 .collect();
852 assert_debug_snapshot!(results);
853 }
854
855 #[test]
856 fn snapshot_mask_token_long_value() {
857 assert_debug_snapshot!(mask_token("abcdefghijklmnopqrstuvwxyz"));
858 }
859 }
860
861 mod error_message_snapshots {
864 use super::*;
865 use tempfile::tempdir;
866
867 #[test]
868 fn snapshot_error_missing_token_message() {
869 temp_env::with_vars(
870 [
871 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
872 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
873 ],
874 || {
875 let td = tempdir().expect("tempdir");
876 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
877 insta::assert_snapshot!(
878 "error_msg_missing_token",
879 format!(
880 "detected={}, source={}, has_token={}",
881 auth.detected,
882 auth.source,
883 auth.token.is_some()
884 )
885 );
886 },
887 );
888 }
889
890 #[test]
891 fn snapshot_error_token_source_display_none() {
892 insta::assert_snapshot!(
893 "error_msg_token_source_none",
894 format!("Token source: {}", TokenSource::None)
895 );
896 }
897 }
898
899 mod proptests {
902 use super::*;
903 use proptest::prelude::*;
904
905 fn token_strategy() -> impl Strategy<Value = String> {
906 "[a-zA-Z0-9_\\-\\.]{1,128}"
907 }
908
909 fn registry_name_strategy() -> impl Strategy<Value = String> {
910 "[a-z][a-z0-9\\-]{0,20}"
911 }
912
913 proptest! {
914 #[test]
915 fn env_default_token_resolution(token in token_strategy()) {
916 temp_env::with_vars(
917 [
918 (CARGO_REGISTRY_TOKEN_ENV, Some(token.as_str())),
919 ("CARGO_REGISTRIES_CRATES_IO_TOKEN", None::<&str>),
920 ],
921 || {
922 let auth = resolve_token(CRATES_IO_REGISTRY, None);
923 prop_assert_eq!(auth.token.as_deref(), Some(token.as_str()));
924 prop_assert_eq!(auth.source, TokenSource::EnvDefault);
925 prop_assert!(auth.detected);
926 Ok(())
927 },
928 )?;
929 }
930
931 #[test]
932 fn env_registry_token_resolution(
933 name in registry_name_strategy(),
934 token in token_strategy(),
935 ) {
936 let env_var = format!(
937 "{}{}_TOKEN",
938 CARGO_REGISTRIES_TOKEN_PREFIX,
939 name.to_uppercase().replace('-', "_")
940 );
941 temp_env::with_vars(
942 [
943 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
944 (env_var.as_str(), Some(token.as_str())),
945 ],
946 || {
947 let auth = resolve_token(&name, None);
948 prop_assert_eq!(auth.token.as_deref(), Some(token.as_str()));
949 prop_assert_eq!(auth.source, TokenSource::EnvRegistry);
950 prop_assert!(auth.detected);
951 Ok(())
952 },
953 )?;
954 }
955
956 #[test]
957 fn env_token_takes_precedence_over_credentials(
958 env_token in token_strategy(),
959 file_token in token_strategy(),
960 ) {
961 let td = tempfile::tempdir().expect("tempdir");
962 let creds = td.path().join(CREDENTIALS_FILE);
963 let content = format!("[registry]\ntoken = \"{file_token}\"\n");
964 std::fs::write(&creds, &content).expect("write");
965
966 temp_env::with_var(CARGO_REGISTRY_TOKEN_ENV, Some(env_token.as_str()), || {
967 let auth = resolve_token(CRATES_IO_REGISTRY, Some(td.path()));
968 prop_assert_eq!(auth.token.as_deref(), Some(env_token.as_str()));
969 prop_assert_eq!(auth.source, TokenSource::EnvDefault);
970 Ok(())
971 })?;
972 }
973
974 #[test]
975 fn env_registry_token_takes_precedence_over_credentials(
976 name in registry_name_strategy(),
977 env_token in token_strategy(),
978 file_token in token_strategy(),
979 ) {
980 let td = tempfile::tempdir().expect("tempdir");
981 let creds = td.path().join(CREDENTIALS_FILE);
982 let content = format!("[registries.{name}]\ntoken = \"{file_token}\"\n");
983 std::fs::write(&creds, &content).expect("write");
984
985 let env_var = format!(
986 "{}{}_TOKEN",
987 CARGO_REGISTRIES_TOKEN_PREFIX,
988 name.to_uppercase().replace('-', "_")
989 );
990 temp_env::with_vars(
991 [
992 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
993 (env_var.as_str(), Some(env_token.as_str())),
994 ],
995 || {
996 let auth = resolve_token(&name, Some(td.path()));
997 prop_assert_eq!(auth.token.as_deref(), Some(env_token.as_str()));
998 prop_assert_eq!(auth.source, TokenSource::EnvRegistry);
999 Ok(())
1000 },
1001 )?;
1002 }
1003
1004 #[test]
1005 fn mask_token_never_exposes_middle(token in "[[:ascii:]]{1,200}") {
1006 let masked = mask_token(&token);
1007 if token.len() <= 8 {
1008 prop_assert!(masked.chars().all(|c| c == '*'));
1009 prop_assert_eq!(masked.len(), token.len());
1010 } else {
1011 prop_assert!(masked.starts_with(&token[..4]));
1012 prop_assert!(masked.ends_with(&token[token.len() - 4..]));
1013 prop_assert!(masked.contains("****"));
1014 }
1015 }
1016
1017 #[test]
1018 fn resolve_token_never_panics(registry in "[a-zA-Z0-9_\\-]{0,50}") {
1019 let td = tempfile::tempdir().expect("tempdir");
1020 temp_env::with_vars(
1021 [
1022 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
1023 (CARGO_HOME_ENV, None::<&str>),
1024 ],
1025 || -> Result<(), proptest::test_runner::TestCaseError> {
1026 let _ = resolve_token(®istry, Some(td.path()));
1027 Ok(())
1028 },
1029 )?;
1030 }
1031
1032 #[test]
1033 fn cargo_home_path_never_panics(home in "[^\x00]{1,100}") {
1034 temp_env::with_var(CARGO_HOME_ENV, Some(home.as_str()), || {
1035 let _ = cargo_home_path(None);
1036 });
1037 }
1038
1039 #[test]
1040 fn mask_token_never_panics(token in "[[:ascii:]]{1,500}") {
1041 let _ = mask_token(&token);
1042 }
1043
1044 #[test]
1045 fn has_token_never_panics(registry in "[a-zA-Z0-9_\\-]{0,50}") {
1046 let td = tempfile::tempdir().expect("tempdir");
1047 temp_env::with_vars(
1048 [
1049 (CARGO_REGISTRY_TOKEN_ENV, None::<&str>),
1050 (CARGO_HOME_ENV, None::<&str>),
1051 ],
1052 || -> Result<(), proptest::test_runner::TestCaseError> {
1053 let _ = has_token(®istry, Some(td.path()));
1054 Ok(())
1055 },
1056 )?;
1057 }
1058 }
1059 }
1060}