1use std::collections::hash_set::HashSet;
10use std::fmt::{self, Debug};
11use std::io;
12use std::sync::Mutex;
13use std::time::{Duration, Instant};
14
15use derive_builder::Builder;
16use git_checks_core::impl_prelude::*;
17use hickory_proto::ProtoErrorKind;
18use hickory_resolver::name_server::TokioConnectionProvider;
19use hickory_resolver::{Name, ResolveError, ResolveErrorKind, Resolver};
20use lazy_static::lazy_static;
21use log::{error, warn};
22use thiserror::Error;
23use tokio::runtime::Runtime;
24use ttl_cache::TtlCache;
25
26#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
28pub enum ValidNameFullNamePolicy {
29 #[default]
31 Required,
32 Preferred,
34 Optional,
36}
37
38impl ValidNameFullNamePolicy {
39 fn apply<F>(self, result: &mut CheckResult, msg: F)
41 where
42 F: Fn(&str) -> String,
43 {
44 match self {
45 ValidNameFullNamePolicy::Required => {
46 result.add_error(msg("required"));
47 },
48 ValidNameFullNamePolicy::Preferred => {
49 result.add_warning(msg("preferred"));
50 },
51 ValidNameFullNamePolicy::Optional => {},
52 }
53 }
54}
55
56const LOCK_POISONED: &str = "DNS cache lock poisoned";
57const DEFAULT_TTL_CACHE_SIZE: usize = 100;
58const DEFAULT_TTL_CACHE_HIT_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
61const DEFAULT_TTL_CACHE_MISS_DURATION: Duration = Duration::from_secs(5 * 60);
64
65lazy_static! {
66 static ref DNS_RESOLVER: Result<Resolver<TokioConnectionProvider>, ResolveError> = Resolver::tokio_from_system_conf()
68 .map_err(|err| {
69 error!(
70 target: "git-checks/valid_name",
71 "failed to construct DNS resolver: {}",
72 err,
73 );
74
75 err
76 });
77}
78
79#[derive(Builder)]
86#[builder(field(private))]
87pub struct ValidName {
88 #[builder(default)]
93 full_name_policy: ValidNameFullNamePolicy,
94 #[builder(setter(skip))]
96 #[builder(default = "empty_dns_cache()")]
97 dns_cache: Mutex<TtlCache<String, bool>>,
98 #[builder(private)]
100 #[builder(setter(name = "_trust_domains"))]
101 #[builder(default = "HashSet::new()")]
102 trust_domains: HashSet<String>,
103}
104
105impl ValidNameBuilder {
106 pub fn trust_domains<I, D>(&mut self, domains: I) -> &mut Self
108 where
109 I: IntoIterator<Item = D>,
110 D: Into<String>,
111 {
112 self.trust_domains = Some(domains.into_iter().map(Into::into).collect());
113 self
114 }
115
116 #[deprecated(
118 since = "4.1.0",
119 note = "better terminology; use `trust_domains` instead"
120 )]
121 pub fn whitelisted_domains<I, D>(&mut self, domains: I) -> &mut Self
122 where
123 I: IntoIterator<Item = D>,
124 D: Into<String>,
125 {
126 self.trust_domains = Some(domains.into_iter().map(Into::into).collect());
127 self
128 }
129}
130
131fn empty_dns_cache() -> Mutex<TtlCache<String, bool>> {
132 Mutex::new(TtlCache::new(DEFAULT_TTL_CACHE_SIZE))
133}
134
135impl Debug for ValidName {
136 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137 f.debug_struct("ValidName")
138 .field("full_name_policy", &self.full_name_policy)
139 .field("trust_domains", &self.trust_domains)
140 .finish()
141 }
142}
143
144impl Default for ValidName {
145 fn default() -> Self {
146 Self {
147 full_name_policy: ValidNameFullNamePolicy::default(),
148 dns_cache: empty_dns_cache(),
149 trust_domains: HashSet::new(),
150 }
151 }
152}
153
154impl Clone for ValidName {
155 fn clone(&self) -> Self {
156 Self {
157 full_name_policy: self.full_name_policy,
158 dns_cache: empty_dns_cache(),
159 trust_domains: self.trust_domains.clone(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy)]
165enum HostLookup {
166 Hit,
167 Miss { valid_until: Option<Instant> },
168}
169
170impl HostLookup {
171 fn is_hit(self) -> bool {
172 matches!(self, HostLookup::Hit)
173 }
174
175 fn cache_duration(self) -> Duration {
176 match self {
177 HostLookup::Hit => DEFAULT_TTL_CACHE_HIT_DURATION,
178 HostLookup::Miss {
179 valid_until,
180 } => {
181 valid_until
182 .and_then(|inst| {
183 let now = Instant::now();
184 inst.checked_duration_since(now)
185 })
186 .unwrap_or(DEFAULT_TTL_CACHE_MISS_DURATION)
187 },
188 }
189 }
190}
191
192#[derive(Debug, Error)]
193enum ValidNameError {
194 #[error("failed to initialize a DNS resolver: {}", reason)]
195 NoResolver { reason: String },
196 #[error("failed to start a tokio runtime: {}", source)]
197 TokioRuntime { source: io::Error },
198 #[error("failed to parse the domain name: {}", source)]
199 ParseName {
200 #[from]
201 source: hickory_proto::ProtoError,
202 },
203}
204
205impl ValidNameError {
206 fn no_resolver(source: &ResolveError) -> Self {
207 Self::NoResolver {
208 reason: format!("{}", source),
209 }
210 }
211
212 fn tokio_runtime(source: io::Error) -> Self {
213 Self::TokioRuntime {
214 source,
215 }
216 }
217}
218
219impl ValidName {
220 pub fn builder() -> ValidNameBuilder {
222 Default::default()
223 }
224
225 fn check_name(name: &str) -> bool {
227 name.find(' ').is_some()
228 }
229
230 fn check_host(domain: &str) -> Result<Option<HostLookup>, ValidNameError> {
231 let resolver = DNS_RESOLVER.as_ref().map_err(ValidNameError::no_resolver)?;
232
233 let abs_domain = format!("{}.", domain);
235 let name = Name::from_str_relaxed(abs_domain)?;
236 let lookup_async = resolver.mx_lookup(name);
237
238 let rt = Runtime::new().map_err(ValidNameError::tokio_runtime)?;
239 let lookup = rt.block_on(lookup_async);
240
241 Ok(match lookup {
242 Ok(_) => Some(HostLookup::Hit),
243 Err(err) => {
244 if let ResolveErrorKind::Proto(err) = err.kind() {
245 match err.kind() {
246 ProtoErrorKind::NoRecordsFound {
247 negative_ttl, ..
248 } => {
249 let valid_until = negative_ttl.and_then(|ttl| {
250 let ttl_duration = Duration::from_secs(ttl.into());
251 let now = Instant::now();
252
253 now.checked_add(ttl_duration)
254 });
255 Some(HostLookup::Miss {
256 valid_until,
257 })
258 },
259 ProtoErrorKind::Timeout => None,
260 _ => {
261 warn!(
262 target: "git-checks/valid_name",
263 "failed to look up MX record for domain {} (timeout): {:?}",
264 domain,
265 err,
266 );
267
268 None
269 },
270 }
271 } else {
272 warn!(
273 target: "git-checks/valid_name",
274 "failed to look up MX record for domain {}: {:?}",
275 domain,
276 err,
277 );
278
279 None
280 }
281 },
282 })
283 }
284
285 fn check_email(&self, email: &str) -> bool {
287 let domain_part = email.split_once('@').map(|t| t.1);
288
289 if let Some(domain) = domain_part {
290 if self.trust_domains.contains(domain) {
291 return true;
292 }
293
294 let mut cache = self.dns_cache.lock().expect(LOCK_POISONED);
295 if let Some(cached_res) = cache.get_mut(domain) {
296 return *cached_res;
297 }
298
299 if let Ok(lookup) = Self::check_host(domain) {
300 lookup.is_some_and(|lookup| {
301 let hit = lookup.is_hit();
302 cache.insert(domain.into(), hit, lookup.cache_duration());
303 hit
304 })
305 } else {
306 false
307 }
308 } else {
309 false
310 }
311 }
312
313 fn check_identity(&self, what: &str, who: &str, identity: &Identity) -> CheckResult {
315 let mut result = CheckResult::new();
316
317 if !Self::check_name(&identity.name) {
318 self.full_name_policy.apply(&mut result, |policy| {
319 format!(
320 "The {} name (`{}`) for {} has no space in it. A full name is {} for \
321 contribution. Please set the `user.name` Git configuration value.",
322 who, identity.name, what, policy,
323 )
324 });
325 }
326
327 if !self.check_email(&identity.email) {
328 result.add_error(format!(
329 "The {} email (`{}`) for {} has an unknown domain. Please set the `user.email` \
330 Git configuration value.",
331 who, identity.email, what,
332 ));
333 }
334
335 result
336 }
337}
338
339impl Check for ValidName {
340 fn name(&self) -> &str {
341 "valid-name"
342 }
343
344 fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
345 let what = format!("commit {}", commit.sha1);
346
347 Ok(if commit.author == commit.committer {
348 self.check_identity(&what, "given", &commit.author)
349 } else {
350 let author_res = self.check_identity(&what, "author", &commit.author);
351 let commiter_res = self.check_identity(&what, "committer", &commit.committer);
352
353 author_res.combine(commiter_res)
354 })
355 }
356}
357
358impl BranchCheck for ValidName {
359 fn name(&self) -> &str {
360 "valid-name"
361 }
362
363 fn check(&self, ctx: &CheckGitContext, _: &CommitId) -> Result<CheckResult, Box<dyn Error>> {
364 Ok(self.check_identity("the topic", "owner", ctx.topic_owner()))
365 }
366}
367
368#[cfg(feature = "config")]
369pub(crate) mod config {
370 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
371 use log::warn;
372 use serde::Deserialize;
373 #[cfg(test)]
374 use serde_json::json;
375
376 use crate::ValidName;
377 use crate::ValidNameFullNamePolicy;
378
379 #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
381 pub enum ValidNameFullNamePolicyIo {
382 #[serde(rename = "required")]
384 Required,
385 #[serde(rename = "preferred")]
387 Preferred,
388 #[serde(rename = "optional")]
390 Optional,
391 }
392
393 impl From<ValidNameFullNamePolicyIo> for ValidNameFullNamePolicy {
394 fn from(policy: ValidNameFullNamePolicyIo) -> Self {
395 match policy {
396 ValidNameFullNamePolicyIo::Required => ValidNameFullNamePolicy::Required,
397 ValidNameFullNamePolicyIo::Preferred => ValidNameFullNamePolicy::Preferred,
398 ValidNameFullNamePolicyIo::Optional => ValidNameFullNamePolicy::Optional,
399 }
400 }
401 }
402
403 #[derive(Deserialize, Debug)]
424 pub struct ValidNameConfig {
425 #[serde(default)]
426 full_name_policy: Option<ValidNameFullNamePolicyIo>,
427 #[serde(default)]
428 trust_domains: Option<Vec<String>>,
429 #[serde(default)]
430 whitelisted_domains: Option<Vec<String>>,
431 }
432
433 impl IntoCheck for ValidNameConfig {
434 type Check = ValidName;
435
436 fn into_check(self) -> Self::Check {
437 let mut builder = ValidName::builder();
438
439 if let Some(full_name_policy) = self.full_name_policy {
440 builder.full_name_policy(full_name_policy.into());
441 }
442
443 if let Some(trust_domains) = self.trust_domains {
444 builder.trust_domains(trust_domains);
445 } else if let Some(trust_domains) = self.whitelisted_domains {
446 warn!(
447 target: "git-checks/valid_name",
448 "the `whitelisted_domains` configuration key is deprecated; use \
449 `trust_domains` instead.",
450 );
451 builder.trust_domains(trust_domains);
452 }
453
454 builder
455 .build()
456 .expect("configuration mismatch for `ValidName`")
457 }
458 }
459
460 register_checks! {
461 ValidNameConfig {
462 "valid_name" => CommitCheckConfig,
463 },
464 }
465
466 #[test]
467 fn test_valid_name_full_name_policy_deserialize() {
468 let value = json!("required");
469 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
470 assert_eq!(policy, ValidNameFullNamePolicyIo::Required);
471 assert_eq!(
472 ValidNameFullNamePolicy::from(policy),
473 ValidNameFullNamePolicy::Required,
474 );
475
476 let value = json!("optional");
477 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
478 assert_eq!(policy, ValidNameFullNamePolicyIo::Optional);
479 assert_eq!(
480 ValidNameFullNamePolicy::from(policy),
481 ValidNameFullNamePolicy::Optional,
482 );
483
484 let value = json!("preferred");
485 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
486 assert_eq!(policy, ValidNameFullNamePolicyIo::Preferred);
487 assert_eq!(
488 ValidNameFullNamePolicy::from(policy),
489 ValidNameFullNamePolicy::Preferred,
490 );
491
492 let value = json!("invalid");
493 let err = ValidNameFullNamePolicyIo::deserialize(value).unwrap_err();
494
495 assert!(!err.is_io());
496 assert!(!err.is_syntax());
497 assert!(err.is_data());
498 assert!(!err.is_eof());
499
500 assert_eq!(
501 err.to_string(),
502 "unknown variant `invalid`, expected one of `required`, `preferred`, `optional`",
503 );
504 }
505
506 #[test]
507 fn test_valid_name_config_empty() {
508 let json = json!({});
509 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
510
511 assert_eq!(check.full_name_policy, None);
512 assert_eq!(check.trust_domains, None);
513 assert_eq!(check.whitelisted_domains, None);
514
515 let check = check.into_check();
516
517 if let ValidNameFullNamePolicy::Required = check.full_name_policy {
518 } else {
520 panic!("unexpected full name policy: {:?}", check.full_name_policy);
521 }
522 itertools::assert_equal(&check.trust_domains, &[] as &[&str]);
523 }
524
525 #[test]
526 fn test_valid_name_config_all_fields() {
527 let exp_domain: String = "mycompany.invalid".into();
528 let json = json!({
529 "full_name_policy": "optional",
530 "trust_domains": [exp_domain],
531 });
532 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
533
534 assert_eq!(
535 check.full_name_policy,
536 Some(ValidNameFullNamePolicyIo::Optional),
537 );
538 itertools::assert_equal(&check.trust_domains, &Some([exp_domain.clone()]));
539 assert_eq!(check.whitelisted_domains, None);
540
541 let check = check.into_check();
542
543 if let ValidNameFullNamePolicy::Optional = check.full_name_policy {
544 } else {
546 panic!("unexpected full name policy: {:?}", check.full_name_policy);
547 }
548 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
549 }
550
551 #[test]
552 fn test_valid_name_config_all_fields_deprecated() {
553 let exp_domain: String = "mycompany.invalid".into();
554 let json = json!({
555 "whitelisted_domains": [exp_domain],
556 });
557 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
558
559 assert_eq!(check.full_name_policy, None);
560 assert_eq!(check.trust_domains, None);
561 itertools::assert_equal(&check.whitelisted_domains, &Some([exp_domain.clone()]));
562
563 let check = check.into_check();
564
565 if let ValidNameFullNamePolicy::Required = check.full_name_policy {
566 } else {
568 panic!("unexpected full name policy: {:?}", check.full_name_policy);
569 }
570 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
571 }
572
573 #[test]
574 fn test_valid_name_config_all_fields_with_deprecated() {
575 let exp_domain: String = "mycompany.invalid".into();
576 let exp_deprecated_domain: String = "myothercompany.invalid".into();
577 let json = json!({
578 "full_name_policy": "optional",
579 "trust_domains": [exp_domain],
580 "whitelisted_domains": [exp_deprecated_domain],
581 });
582 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
583
584 assert_eq!(
585 check.full_name_policy,
586 Some(ValidNameFullNamePolicyIo::Optional),
587 );
588 itertools::assert_equal(&check.trust_domains, &Some([exp_domain.clone()]));
589 itertools::assert_equal(&check.whitelisted_domains, &Some([exp_deprecated_domain]));
590
591 let check = check.into_check();
592
593 if let ValidNameFullNamePolicy::Optional = check.full_name_policy {
594 } else {
596 panic!("unexpected full name policy: {:?}", check.full_name_policy);
597 }
598 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use std::time::{Duration, Instant};
605
606 use git_checks_core::{BranchCheck, Check};
607 use git_workarea::Identity;
608
609 use crate::test::*;
610 use crate::ValidName;
611 use crate::ValidNameFullNamePolicy;
612
613 const BAD_TOPIC: &str = "91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8";
614 const BAD_AUTHOR_NAME: &str = "edac4e5b3a00eac60280a78ee84b5ef8d4cce97a";
615
616 #[test]
617 fn test_valid_name_builder_default() {
618 assert!(ValidName::builder().build().is_ok());
619 }
620
621 #[test]
622 fn test_valid_name_name_commit() {
623 let check = ValidName::default();
624 assert_eq!(Check::name(&check), "valid-name");
625 }
626
627 #[test]
628 fn test_valid_name_name_branch() {
629 let check = ValidName::default();
630 assert_eq!(BranchCheck::name(&check), "valid-name");
631 }
632
633 #[test]
634 fn test_valid_name_required() {
635 let check = ValidName::default();
636 let result = run_check("test_valid_name_required", BAD_TOPIC, check);
637 test_result_errors(result, &[
638 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
639 no space in it. A full name is required for contribution. Please set the `user.name` \
640 Git configuration value.",
641 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
642 has an unknown domain. Please set the `user.email` Git configuration value.",
643 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
644 has no space in it. A full name is required for contribution. Please set the \
645 `user.name` Git configuration value.",
646 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
647 has an unknown domain. Please set the `user.email` Git configuration value.",
648 "The author email (`bademail@baddomain.invalid`) for commit \
649 9002239437a06e81a58fed07150b215a917028d6 has an unknown domain. Please set the \
650 `user.email` Git configuration value.",
651 "The committer email (`bademail@baddomain.invalid`) for commit \
652 dcd8895d299031d607481b4936478f8de4cc28ae has an unknown domain. Please set the \
653 `user.email` Git configuration value.",
654 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
655 no space in it. A full name is required for contribution. Please set the `user.name` \
656 Git configuration value.",
657 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
658 an unknown domain. Please set the `user.email` Git configuration value.",
659 ]);
660 }
661
662 #[test]
663 #[allow(deprecated)]
664 fn test_valid_name_whitelist() {
665 let check = ValidName::builder()
666 .whitelisted_domains(["baddomain.invalid"].iter().cloned())
667 .build()
668 .unwrap();
669 let result = run_check("test_valid_name_whitelist", BAD_TOPIC, check);
670 test_result_errors(result, &[
671 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
672 no space in it. A full name is required for contribution. Please set the `user.name` \
673 Git configuration value.",
674 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
675 has an unknown domain. Please set the `user.email` Git configuration value.",
676 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
677 has no space in it. A full name is required for contribution. Please set the \
678 `user.name` Git configuration value.",
679 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
680 has an unknown domain. Please set the `user.email` Git configuration value.",
681 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
682 no space in it. A full name is required for contribution. Please set the `user.name` \
683 Git configuration value.",
684 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
685 an unknown domain. Please set the `user.email` Git configuration value.",
686 ]);
687 }
688
689 #[test]
690 fn test_valid_name_trust_domains() {
691 let check = ValidName::builder()
692 .trust_domains(["baddomain.invalid"].iter().cloned())
693 .build()
694 .unwrap();
695 let result = run_check("test_valid_name_trust_domains", BAD_TOPIC, check);
696 test_result_errors(result, &[
697 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
698 no space in it. A full name is required for contribution. Please set the `user.name` \
699 Git configuration value.",
700 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
701 has an unknown domain. Please set the `user.email` Git configuration value.",
702 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
703 has no space in it. A full name is required for contribution. Please set the \
704 `user.name` Git configuration value.",
705 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
706 has an unknown domain. Please set the `user.email` Git configuration value.",
707 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
708 no space in it. A full name is required for contribution. Please set the `user.name` \
709 Git configuration value.",
710 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
711 an unknown domain. Please set the `user.email` Git configuration value.",
712 ]);
713 }
714
715 #[test]
716 fn test_valid_name_preferred() {
717 let check = ValidName::builder()
718 .full_name_policy(ValidNameFullNamePolicy::Preferred)
719 .build()
720 .unwrap();
721 let result = run_check("test_valid_name_preferred", BAD_AUTHOR_NAME, check);
722 test_result_warnings(result, &[
723 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
724 no space in it. A full name is preferred for contribution. Please set the \
725 `user.name` Git configuration value.",
726 ]);
727 }
728
729 #[test]
730 fn test_valid_name_optional() {
731 let check = ValidName::builder()
732 .full_name_policy(ValidNameFullNamePolicy::Optional)
733 .build()
734 .unwrap();
735 run_check_ok("test_valid_name_optional", BAD_AUTHOR_NAME, check);
736 }
737
738 fn mononym_ident() -> Identity {
739 Identity::new("Mononym", "email@example.com")
740 }
741
742 fn bademail_ident() -> Identity {
743 Identity::new("Anon E. Mouse", "bademail")
744 }
745
746 fn bademail_mx_ident() -> Identity {
747 Identity::new("Anon E. Mouse", "bademail@baddomain.invalid")
748 }
749
750 #[test]
751 fn test_valid_name_branch_required() {
752 let check = ValidName::default();
753 run_branch_check_ok(
754 "test_valid_name_branch_required/ok",
755 BAD_TOPIC,
756 check.clone(),
757 );
758 let result = run_branch_check_ident(
759 "test_valid_name_branch_required/mononym",
760 BAD_TOPIC,
761 check.clone(),
762 mononym_ident(),
763 );
764 test_result_errors(result, &[
765 "The owner name (`Mononym`) for the topic has no space in it. A full name is required \
766 for contribution. Please set the `user.name` Git configuration value.",
767 ]);
768 let result = run_branch_check_ident(
769 "test_valid_name_branch_required/bademail",
770 BAD_TOPIC,
771 check.clone(),
772 bademail_ident(),
773 );
774 test_result_errors(
775 result,
776 &[
777 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
778 `user.email` Git configuration value.",
779 ],
780 );
781 let result = run_branch_check_ident(
782 "test_valid_name_branch_required/bademail_mx",
783 BAD_TOPIC,
784 check,
785 bademail_mx_ident(),
786 );
787 test_result_errors(result, &[
788 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
789 Please set the `user.email` Git configuration value.",
790 ]);
791 }
792
793 #[test]
794 fn test_valid_name_branch_trust_domains() {
795 let check = ValidName::builder()
796 .trust_domains(["baddomain.invalid"].iter().cloned())
797 .build()
798 .unwrap();
799 run_branch_check_ok(
800 "test_valid_name_branch_trust_domains/ok",
801 BAD_TOPIC,
802 check.clone(),
803 );
804 let result = run_branch_check_ident(
805 "test_valid_name_branch_required/mononym",
806 BAD_TOPIC,
807 check.clone(),
808 mononym_ident(),
809 );
810 test_result_errors(result, &[
811 "The owner name (`Mononym`) for the topic has no space in it. A full name is required \
812 for contribution. Please set the `user.name` Git configuration value.",
813 ]);
814 let result = run_branch_check_ident(
815 "test_valid_name_branch_trust_domains/bademail",
816 BAD_TOPIC,
817 check.clone(),
818 bademail_ident(),
819 );
820 test_result_errors(
821 result,
822 &[
823 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
824 `user.email` Git configuration value.",
825 ],
826 );
827 run_branch_check_ident_ok(
828 "test_valid_name_branch_trust_domains/bademail_mx",
829 BAD_TOPIC,
830 check,
831 bademail_mx_ident(),
832 );
833 }
834
835 #[test]
836 fn test_valid_name_branch_preferred() {
837 let check = ValidName::builder()
838 .full_name_policy(ValidNameFullNamePolicy::Preferred)
839 .build()
840 .unwrap();
841 run_branch_check_ok(
842 "test_valid_name_branch_preferred/ok",
843 BAD_TOPIC,
844 check.clone(),
845 );
846 let result = run_branch_check_ident(
847 "test_valid_name_branch_preferred/mononym",
848 BAD_TOPIC,
849 check.clone(),
850 mononym_ident(),
851 );
852 test_result_warnings(result, &[
853 "The owner name (`Mononym`) for the topic has \
854 no space in it. A full name is preferred for contribution. Please set the `user.name` \
855 Git configuration value.",
856 ]);
857 let result = run_branch_check_ident(
858 "test_valid_name_branch_preferred/bademail",
859 BAD_TOPIC,
860 check.clone(),
861 bademail_ident(),
862 );
863 test_result_errors(
864 result,
865 &[
866 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
867 `user.email` Git configuration value.",
868 ],
869 );
870 let result = run_branch_check_ident(
871 "test_valid_name_branch_preferred/bademail_mx",
872 BAD_TOPIC,
873 check,
874 bademail_mx_ident(),
875 );
876 test_result_errors(result, &[
877 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
878 Please set the `user.email` Git configuration value.",
879 ]);
880 }
881
882 #[test]
883 fn test_valid_name_branch_optional() {
884 let check = ValidName::builder()
885 .full_name_policy(ValidNameFullNamePolicy::Optional)
886 .build()
887 .unwrap();
888 run_branch_check_ok(
889 "test_valid_name_branch_optional/ok",
890 BAD_TOPIC,
891 check.clone(),
892 );
893 run_branch_check_ident_ok(
894 "test_valid_name_branch_optional/mononym",
895 BAD_TOPIC,
896 check.clone(),
897 mononym_ident(),
898 );
899 let result = run_branch_check_ident(
900 "test_valid_name_branch_optional/bademail",
901 BAD_TOPIC,
902 check.clone(),
903 bademail_ident(),
904 );
905 test_result_errors(
906 result,
907 &[
908 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
909 `user.email` Git configuration value.",
910 ],
911 );
912 let result = run_branch_check_ident(
913 "test_valid_name_branch_optional/bademail_mx",
914 BAD_TOPIC,
915 check,
916 bademail_mx_ident(),
917 );
918 test_result_errors(result, &[
919 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
920 Please set the `user.email` Git configuration value.",
921 ]);
922 }
923
924 #[test]
925 fn test_valid_name_impl_debug() {
926 let check = ValidName::builder().build().unwrap();
927 let out = format!("{:?}", check);
928 assert_eq!(
929 out,
930 "ValidName { full_name_policy: Required, trust_domains: {} }",
931 );
932 }
933
934 #[test]
935 fn test_valid_name_impl_clone() {
936 let check = ValidName::builder().build().unwrap();
937 {
938 let mut cache = check.dns_cache.lock().unwrap();
939 cache.insert(
940 "example.com".into(),
941 true,
942 super::DEFAULT_TTL_CACHE_HIT_DURATION,
943 );
944 }
945 #[allow(clippy::redundant_clone)]
947 let cloned = check.clone();
948 assert_eq!(cloned.full_name_policy, check.full_name_policy);
949 assert_eq!(cloned.dns_cache.lock().unwrap().iter().count(), 0);
950 assert_eq!(cloned.trust_domains, check.trust_domains);
951 }
952
953 #[test]
954 fn test_host_lookup_cache_duration() {
955 use super::HostLookup;
956
957 let items = [
958 (HostLookup::Hit, super::DEFAULT_TTL_CACHE_HIT_DURATION),
959 (
960 HostLookup::Miss {
961 valid_until: None,
962 },
963 super::DEFAULT_TTL_CACHE_MISS_DURATION,
964 ),
965 ];
966
967 for (l, d) in items {
968 assert_eq!(l.cache_duration(), d);
969 }
970
971 let short_timeout = Duration::from_secs(60);
972 let soon = Instant::now() + short_timeout;
973 let long_timeout = Duration::from_secs(10000);
974 let later = Instant::now() + long_timeout;
975
976 let range_items = [
977 (
978 HostLookup::Miss {
979 valid_until: Some(soon),
980 },
981 short_timeout,
982 ),
983 (
984 HostLookup::Miss {
985 valid_until: Some(later),
986 },
987 long_timeout,
988 ),
989 ];
990
991 for (l, d) in range_items {
992 let duration = l.cache_duration();
993 assert!(duration <= d);
995 assert!(d - duration <= Duration::from_secs(1));
998 }
999 }
1000}