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