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_resolver::net::runtime::TokioRuntimeProvider;
18use hickory_resolver::net::{DnsError, NetError, NoRecords};
19use hickory_resolver::proto::rr::domain::Name;
20use hickory_resolver::Resolver;
21use lazy_static::lazy_static;
22use log::{error, warn};
23use thiserror::Error;
24use tokio::runtime::Runtime;
25use ttl_cache::TtlCache;
26
27#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
29pub enum ValidNameFullNamePolicy {
30 #[default]
32 Required,
33 Preferred,
35 Optional,
37}
38
39impl ValidNameFullNamePolicy {
40 fn apply<F>(self, result: &mut CheckResult, msg: F)
42 where
43 F: Fn(&str) -> String,
44 {
45 match self {
46 ValidNameFullNamePolicy::Required => {
47 result.add_error(msg("required"));
48 },
49 ValidNameFullNamePolicy::Preferred => {
50 result.add_warning(msg("preferred"));
51 },
52 ValidNameFullNamePolicy::Optional => {},
53 }
54 }
55}
56
57const LOCK_POISONED: &str = "DNS cache lock poisoned";
58const DEFAULT_TTL_CACHE_SIZE: usize = 100;
59const DEFAULT_TTL_CACHE_HIT_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
62const DEFAULT_TTL_CACHE_MISS_DURATION: Duration = Duration::from_secs(5 * 60);
65
66lazy_static! {
67 static ref DNS_RESOLVER: Result<Resolver<TokioRuntimeProvider>, NetError> = Resolver::builder_tokio()
69 .and_then(|builder| builder.build())
70 .map_err(|err| {
71 error!(
72 target: "git-checks/valid_name",
73 "failed to construct DNS resolver: {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_resolver::proto::ProtoError,
203 },
204}
205
206impl ValidNameError {
207 fn no_resolver(source: &NetError) -> 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(NetError::Timeout) => None,
245 Err(NetError::Dns(DnsError::NoRecordsFound(NoRecords {
246 negative_ttl, ..
247 }))) => {
248 let valid_until = negative_ttl.and_then(|ttl| {
249 let ttl_duration = Duration::from_secs(ttl.into());
250 let now = Instant::now();
251
252 now.checked_add(ttl_duration)
253 });
254 Some(HostLookup::Miss {
255 valid_until,
256 })
257 },
258 Err(err) => {
259 warn!(
260 target: "git-checks/valid_name",
261 "failed to look up MX record for domain {domain}: {err:?}",
262 );
263
264 None
265 },
266 })
267 }
268
269 fn check_email(&self, email: &str) -> bool {
271 let domain_part = email.split_once('@').map(|t| t.1);
272
273 if let Some(domain) = domain_part {
274 if self.trust_domains.contains(domain) {
275 return true;
276 }
277
278 let mut cache = self.dns_cache.lock().expect(LOCK_POISONED);
279 if let Some(cached_res) = cache.get_mut(domain) {
280 return *cached_res;
281 }
282
283 if let Ok(lookup) = Self::check_host(domain) {
284 lookup.is_some_and(|lookup| {
285 let hit = lookup.is_hit();
286 cache.insert(domain.into(), hit, lookup.cache_duration());
287 hit
288 })
289 } else {
290 false
291 }
292 } else {
293 false
294 }
295 }
296
297 fn check_identity(&self, what: &str, who: &str, identity: &Identity) -> CheckResult {
299 let mut result = CheckResult::new();
300
301 if !Self::check_name(&identity.name) {
302 self.full_name_policy.apply(&mut result, |policy| {
303 format!(
304 "The {who} name (`{}`) for {what} has no space in it. A full name is {policy} \
305 for contribution. Please set the `user.name` Git configuration value.",
306 identity.name,
307 )
308 });
309 }
310
311 if !self.check_email(&identity.email) {
312 result.add_error(format!(
313 "The {who} email (`{}`) for {what} has an unknown domain. Please set the \
314 `user.email` Git configuration value.",
315 identity.email,
316 ));
317 }
318
319 result
320 }
321}
322
323impl Check for ValidName {
324 fn name(&self) -> &str {
325 "valid-name"
326 }
327
328 fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
329 let what = format!("commit {}", commit.sha1);
330
331 Ok(if commit.author == commit.committer {
332 self.check_identity(&what, "given", &commit.author)
333 } else {
334 let author_res = self.check_identity(&what, "author", &commit.author);
335 let commiter_res = self.check_identity(&what, "committer", &commit.committer);
336
337 author_res.combine(commiter_res)
338 })
339 }
340}
341
342impl BranchCheck for ValidName {
343 fn name(&self) -> &str {
344 "valid-name"
345 }
346
347 fn check(&self, ctx: &CheckGitContext, _: &CommitId) -> Result<CheckResult, Box<dyn Error>> {
348 Ok(self.check_identity("the topic", "owner", ctx.topic_owner()))
349 }
350}
351
352#[cfg(feature = "config")]
353pub(crate) mod config {
354 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
355 use log::warn;
356 use serde::Deserialize;
357 #[cfg(test)]
358 use serde_json::json;
359
360 use crate::ValidName;
361 use crate::ValidNameFullNamePolicy;
362
363 #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
365 pub enum ValidNameFullNamePolicyIo {
366 #[serde(rename = "required")]
368 Required,
369 #[serde(rename = "preferred")]
371 Preferred,
372 #[serde(rename = "optional")]
374 Optional,
375 }
376
377 impl From<ValidNameFullNamePolicyIo> for ValidNameFullNamePolicy {
378 fn from(policy: ValidNameFullNamePolicyIo) -> Self {
379 match policy {
380 ValidNameFullNamePolicyIo::Required => ValidNameFullNamePolicy::Required,
381 ValidNameFullNamePolicyIo::Preferred => ValidNameFullNamePolicy::Preferred,
382 ValidNameFullNamePolicyIo::Optional => ValidNameFullNamePolicy::Optional,
383 }
384 }
385 }
386
387 #[derive(Deserialize, Debug)]
408 pub struct ValidNameConfig {
409 #[serde(default)]
410 full_name_policy: Option<ValidNameFullNamePolicyIo>,
411 #[serde(default)]
412 trust_domains: Option<Vec<String>>,
413 #[serde(default)]
414 whitelisted_domains: Option<Vec<String>>,
415 }
416
417 impl IntoCheck for ValidNameConfig {
418 type Check = ValidName;
419
420 fn into_check(self) -> Self::Check {
421 let mut builder = ValidName::builder();
422
423 if let Some(full_name_policy) = self.full_name_policy {
424 builder.full_name_policy(full_name_policy.into());
425 }
426
427 if let Some(trust_domains) = self.trust_domains {
428 builder.trust_domains(trust_domains);
429 } else if let Some(trust_domains) = self.whitelisted_domains {
430 warn!(
431 target: "git-checks/valid_name",
432 "the `whitelisted_domains` configuration key is deprecated; use \
433 `trust_domains` instead.",
434 );
435 builder.trust_domains(trust_domains);
436 }
437
438 builder
439 .build()
440 .expect("configuration mismatch for `ValidName`")
441 }
442 }
443
444 register_checks! {
445 ValidNameConfig {
446 "valid_name" => CommitCheckConfig,
447 },
448 }
449
450 #[test]
451 fn test_valid_name_full_name_policy_deserialize() {
452 let value = json!("required");
453 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
454 assert_eq!(policy, ValidNameFullNamePolicyIo::Required);
455 assert_eq!(
456 ValidNameFullNamePolicy::from(policy),
457 ValidNameFullNamePolicy::Required,
458 );
459
460 let value = json!("optional");
461 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
462 assert_eq!(policy, ValidNameFullNamePolicyIo::Optional);
463 assert_eq!(
464 ValidNameFullNamePolicy::from(policy),
465 ValidNameFullNamePolicy::Optional,
466 );
467
468 let value = json!("preferred");
469 let policy = ValidNameFullNamePolicyIo::deserialize(value).unwrap();
470 assert_eq!(policy, ValidNameFullNamePolicyIo::Preferred);
471 assert_eq!(
472 ValidNameFullNamePolicy::from(policy),
473 ValidNameFullNamePolicy::Preferred,
474 );
475
476 let value = json!("invalid");
477 let err = ValidNameFullNamePolicyIo::deserialize(value).unwrap_err();
478
479 assert!(!err.is_io());
480 assert!(!err.is_syntax());
481 assert!(err.is_data());
482 assert!(!err.is_eof());
483
484 assert_eq!(
485 err.to_string(),
486 "unknown variant `invalid`, expected one of `required`, `preferred`, `optional`",
487 );
488 }
489
490 #[test]
491 fn test_valid_name_config_empty() {
492 let json = json!({});
493 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
494
495 assert_eq!(check.full_name_policy, None);
496 assert_eq!(check.trust_domains, None);
497 assert_eq!(check.whitelisted_domains, None);
498
499 let check = check.into_check();
500
501 if let ValidNameFullNamePolicy::Required = check.full_name_policy {
502 } else {
504 panic!("unexpected full name policy: {:?}", check.full_name_policy);
505 }
506 itertools::assert_equal(&check.trust_domains, &[] as &[&str]);
507 }
508
509 #[test]
510 fn test_valid_name_config_all_fields() {
511 let exp_domain: String = "mycompany.invalid".into();
512 let json = json!({
513 "full_name_policy": "optional",
514 "trust_domains": [exp_domain],
515 });
516 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
517
518 assert_eq!(
519 check.full_name_policy,
520 Some(ValidNameFullNamePolicyIo::Optional),
521 );
522 itertools::assert_equal(&check.trust_domains, &Some([exp_domain.clone()]));
523 assert_eq!(check.whitelisted_domains, None);
524
525 let check = check.into_check();
526
527 if let ValidNameFullNamePolicy::Optional = check.full_name_policy {
528 } else {
530 panic!("unexpected full name policy: {:?}", check.full_name_policy);
531 }
532 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
533 }
534
535 #[test]
536 fn test_valid_name_config_all_fields_deprecated() {
537 let exp_domain: String = "mycompany.invalid".into();
538 let json = json!({
539 "whitelisted_domains": [exp_domain],
540 });
541 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
542
543 assert_eq!(check.full_name_policy, None);
544 assert_eq!(check.trust_domains, None);
545 itertools::assert_equal(&check.whitelisted_domains, &Some([exp_domain.clone()]));
546
547 let check = check.into_check();
548
549 if let ValidNameFullNamePolicy::Required = check.full_name_policy {
550 } else {
552 panic!("unexpected full name policy: {:?}", check.full_name_policy);
553 }
554 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
555 }
556
557 #[test]
558 fn test_valid_name_config_all_fields_with_deprecated() {
559 let exp_domain: String = "mycompany.invalid".into();
560 let exp_deprecated_domain: String = "myothercompany.invalid".into();
561 let json = json!({
562 "full_name_policy": "optional",
563 "trust_domains": [exp_domain],
564 "whitelisted_domains": [exp_deprecated_domain],
565 });
566 let check: ValidNameConfig = serde_json::from_value(json).unwrap();
567
568 assert_eq!(
569 check.full_name_policy,
570 Some(ValidNameFullNamePolicyIo::Optional),
571 );
572 itertools::assert_equal(&check.trust_domains, &Some([exp_domain.clone()]));
573 itertools::assert_equal(&check.whitelisted_domains, &Some([exp_deprecated_domain]));
574
575 let check = check.into_check();
576
577 if let ValidNameFullNamePolicy::Optional = check.full_name_policy {
578 } else {
580 panic!("unexpected full name policy: {:?}", check.full_name_policy);
581 }
582 itertools::assert_equal(&check.trust_domains, &[exp_domain]);
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use std::time::{Duration, Instant};
589
590 use git_checks_core::{BranchCheck, Check};
591 use git_workarea::Identity;
592
593 use crate::test::*;
594 use crate::ValidName;
595 use crate::ValidNameFullNamePolicy;
596
597 const BAD_TOPIC: &str = "91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8";
598 const BAD_AUTHOR_NAME: &str = "edac4e5b3a00eac60280a78ee84b5ef8d4cce97a";
599
600 #[test]
601 fn test_valid_name_builder_default() {
602 assert!(ValidName::builder().build().is_ok());
603 }
604
605 #[test]
606 fn test_valid_name_name_commit() {
607 let check = ValidName::default();
608 assert_eq!(Check::name(&check), "valid-name");
609 }
610
611 #[test]
612 fn test_valid_name_name_branch() {
613 let check = ValidName::default();
614 assert_eq!(BranchCheck::name(&check), "valid-name");
615 }
616
617 #[test]
618 fn test_valid_name_required() {
619 let check = ValidName::default();
620 let result = run_check("test_valid_name_required", BAD_TOPIC, check);
621 test_result_errors(result, &[
622 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
623 no space in it. A full name is required for contribution. Please set the `user.name` \
624 Git configuration value.",
625 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
626 has an unknown domain. Please set the `user.email` Git configuration value.",
627 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
628 has no space in it. A full name is required for contribution. Please set the \
629 `user.name` Git configuration value.",
630 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
631 has an unknown domain. Please set the `user.email` Git configuration value.",
632 "The author email (`bademail@baddomain.invalid`) for commit \
633 9002239437a06e81a58fed07150b215a917028d6 has an unknown domain. Please set the \
634 `user.email` Git configuration value.",
635 "The committer email (`bademail@baddomain.invalid`) for commit \
636 dcd8895d299031d607481b4936478f8de4cc28ae has an unknown domain. Please set the \
637 `user.email` Git configuration value.",
638 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
639 no space in it. A full name is required for contribution. Please set the `user.name` \
640 Git configuration value.",
641 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
642 an unknown domain. Please set the `user.email` Git configuration value.",
643 ]);
644 }
645
646 #[test]
647 #[allow(deprecated)]
648 fn test_valid_name_whitelist() {
649 let check = ValidName::builder()
650 .whitelisted_domains(["baddomain.invalid"].iter().cloned())
651 .build()
652 .unwrap();
653 let result = run_check("test_valid_name_whitelist", BAD_TOPIC, check);
654 test_result_errors(result, &[
655 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
656 no space in it. A full name is required for contribution. Please set the `user.name` \
657 Git configuration value.",
658 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
659 has an unknown domain. Please set the `user.email` Git configuration value.",
660 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
661 has no space in it. A full name is required for contribution. Please set the \
662 `user.name` Git configuration value.",
663 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
664 has an unknown domain. Please set the `user.email` Git configuration value.",
665 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
666 no space in it. A full name is required for contribution. Please set the `user.name` \
667 Git configuration value.",
668 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
669 an unknown domain. Please set the `user.email` Git configuration value.",
670 ]);
671 }
672
673 #[test]
674 fn test_valid_name_trust_domains() {
675 let check = ValidName::builder()
676 .trust_domains(["baddomain.invalid"].iter().cloned())
677 .build()
678 .unwrap();
679 let result = run_check("test_valid_name_trust_domains", BAD_TOPIC, check);
680 test_result_errors(result, &[
681 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
682 no space in it. A full name is required for contribution. Please set the `user.name` \
683 Git configuration value.",
684 "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
685 has an unknown domain. Please set the `user.email` Git configuration value.",
686 "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
687 has no space in it. A full name is required for contribution. Please set the \
688 `user.name` Git configuration value.",
689 "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
690 has an unknown domain. Please set the `user.email` Git configuration value.",
691 "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
692 no space in it. A full name is required for contribution. Please set the `user.name` \
693 Git configuration value.",
694 "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
695 an unknown domain. Please set the `user.email` Git configuration value.",
696 ]);
697 }
698
699 #[test]
700 fn test_valid_name_preferred() {
701 let check = ValidName::builder()
702 .full_name_policy(ValidNameFullNamePolicy::Preferred)
703 .build()
704 .unwrap();
705 let result = run_check("test_valid_name_preferred", BAD_AUTHOR_NAME, check);
706 test_result_warnings(result, &[
707 "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
708 no space in it. A full name is preferred for contribution. Please set the \
709 `user.name` Git configuration value.",
710 ]);
711 }
712
713 #[test]
714 fn test_valid_name_optional() {
715 let check = ValidName::builder()
716 .full_name_policy(ValidNameFullNamePolicy::Optional)
717 .build()
718 .unwrap();
719 run_check_ok("test_valid_name_optional", BAD_AUTHOR_NAME, check);
720 }
721
722 fn mononym_ident() -> Identity {
723 Identity::new("Mononym", "email@example.com")
724 }
725
726 fn bademail_ident() -> Identity {
727 Identity::new("Anon E. Mouse", "bademail")
728 }
729
730 fn bademail_mx_ident() -> Identity {
731 Identity::new("Anon E. Mouse", "bademail@baddomain.invalid")
732 }
733
734 #[test]
735 fn test_valid_name_branch_required() {
736 let check = ValidName::default();
737 run_branch_check_ok(
738 "test_valid_name_branch_required/ok",
739 BAD_TOPIC,
740 check.clone(),
741 );
742 let result = run_branch_check_ident(
743 "test_valid_name_branch_required/mononym",
744 BAD_TOPIC,
745 check.clone(),
746 mononym_ident(),
747 );
748 test_result_errors(result, &[
749 "The owner name (`Mononym`) for the topic has no space in it. A full name is required \
750 for contribution. Please set the `user.name` Git configuration value.",
751 ]);
752 let result = run_branch_check_ident(
753 "test_valid_name_branch_required/bademail",
754 BAD_TOPIC,
755 check.clone(),
756 bademail_ident(),
757 );
758 test_result_errors(
759 result,
760 &[
761 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
762 `user.email` Git configuration value.",
763 ],
764 );
765 let result = run_branch_check_ident(
766 "test_valid_name_branch_required/bademail_mx",
767 BAD_TOPIC,
768 check,
769 bademail_mx_ident(),
770 );
771 test_result_errors(result, &[
772 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
773 Please set the `user.email` Git configuration value.",
774 ]);
775 }
776
777 #[test]
778 fn test_valid_name_branch_trust_domains() {
779 let check = ValidName::builder()
780 .trust_domains(["baddomain.invalid"].iter().cloned())
781 .build()
782 .unwrap();
783 run_branch_check_ok(
784 "test_valid_name_branch_trust_domains/ok",
785 BAD_TOPIC,
786 check.clone(),
787 );
788 let result = run_branch_check_ident(
789 "test_valid_name_branch_required/mononym",
790 BAD_TOPIC,
791 check.clone(),
792 mononym_ident(),
793 );
794 test_result_errors(result, &[
795 "The owner name (`Mononym`) for the topic has no space in it. A full name is required \
796 for contribution. Please set the `user.name` Git configuration value.",
797 ]);
798 let result = run_branch_check_ident(
799 "test_valid_name_branch_trust_domains/bademail",
800 BAD_TOPIC,
801 check.clone(),
802 bademail_ident(),
803 );
804 test_result_errors(
805 result,
806 &[
807 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
808 `user.email` Git configuration value.",
809 ],
810 );
811 run_branch_check_ident_ok(
812 "test_valid_name_branch_trust_domains/bademail_mx",
813 BAD_TOPIC,
814 check,
815 bademail_mx_ident(),
816 );
817 }
818
819 #[test]
820 fn test_valid_name_branch_preferred() {
821 let check = ValidName::builder()
822 .full_name_policy(ValidNameFullNamePolicy::Preferred)
823 .build()
824 .unwrap();
825 run_branch_check_ok(
826 "test_valid_name_branch_preferred/ok",
827 BAD_TOPIC,
828 check.clone(),
829 );
830 let result = run_branch_check_ident(
831 "test_valid_name_branch_preferred/mononym",
832 BAD_TOPIC,
833 check.clone(),
834 mononym_ident(),
835 );
836 test_result_warnings(result, &[
837 "The owner name (`Mononym`) for the topic has \
838 no space in it. A full name is preferred for contribution. Please set the `user.name` \
839 Git configuration value.",
840 ]);
841 let result = run_branch_check_ident(
842 "test_valid_name_branch_preferred/bademail",
843 BAD_TOPIC,
844 check.clone(),
845 bademail_ident(),
846 );
847 test_result_errors(
848 result,
849 &[
850 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
851 `user.email` Git configuration value.",
852 ],
853 );
854 let result = run_branch_check_ident(
855 "test_valid_name_branch_preferred/bademail_mx",
856 BAD_TOPIC,
857 check,
858 bademail_mx_ident(),
859 );
860 test_result_errors(result, &[
861 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
862 Please set the `user.email` Git configuration value.",
863 ]);
864 }
865
866 #[test]
867 fn test_valid_name_branch_optional() {
868 let check = ValidName::builder()
869 .full_name_policy(ValidNameFullNamePolicy::Optional)
870 .build()
871 .unwrap();
872 run_branch_check_ok(
873 "test_valid_name_branch_optional/ok",
874 BAD_TOPIC,
875 check.clone(),
876 );
877 run_branch_check_ident_ok(
878 "test_valid_name_branch_optional/mononym",
879 BAD_TOPIC,
880 check.clone(),
881 mononym_ident(),
882 );
883 let result = run_branch_check_ident(
884 "test_valid_name_branch_optional/bademail",
885 BAD_TOPIC,
886 check.clone(),
887 bademail_ident(),
888 );
889 test_result_errors(
890 result,
891 &[
892 "The owner email (`bademail`) for the topic has an unknown domain. Please set the \
893 `user.email` Git configuration value.",
894 ],
895 );
896 let result = run_branch_check_ident(
897 "test_valid_name_branch_optional/bademail_mx",
898 BAD_TOPIC,
899 check,
900 bademail_mx_ident(),
901 );
902 test_result_errors(result, &[
903 "The owner email (`bademail@baddomain.invalid`) for the topic has an unknown domain. \
904 Please set the `user.email` Git configuration value.",
905 ]);
906 }
907
908 #[test]
909 fn test_valid_name_impl_debug() {
910 let check = ValidName::builder().build().unwrap();
911 let out = format!("{check:?}");
912 assert_eq!(
913 out,
914 "ValidName { full_name_policy: Required, trust_domains: {} }",
915 );
916 }
917
918 #[test]
919 fn test_valid_name_impl_clone() {
920 let check = ValidName::builder().build().unwrap();
921 {
922 let mut cache = check.dns_cache.lock().unwrap();
923 cache.insert(
924 "example.com".into(),
925 true,
926 super::DEFAULT_TTL_CACHE_HIT_DURATION,
927 );
928 }
929 #[allow(clippy::redundant_clone)]
931 let cloned = check.clone();
932 assert_eq!(cloned.full_name_policy, check.full_name_policy);
933 assert_eq!(cloned.dns_cache.lock().unwrap().iter().count(), 0);
934 assert_eq!(cloned.trust_domains, check.trust_domains);
935 }
936
937 #[test]
938 fn test_host_lookup_cache_duration() {
939 use super::HostLookup;
940
941 let items = [
942 (HostLookup::Hit, super::DEFAULT_TTL_CACHE_HIT_DURATION),
943 (
944 HostLookup::Miss {
945 valid_until: None,
946 },
947 super::DEFAULT_TTL_CACHE_MISS_DURATION,
948 ),
949 ];
950
951 for (l, d) in items {
952 assert_eq!(l.cache_duration(), d);
953 }
954
955 let short_timeout = Duration::from_secs(60);
956 let soon = Instant::now() + short_timeout;
957 let long_timeout = Duration::from_secs(10000);
958 let later = Instant::now() + long_timeout;
959
960 let range_items = [
961 (
962 HostLookup::Miss {
963 valid_until: Some(soon),
964 },
965 short_timeout,
966 ),
967 (
968 HostLookup::Miss {
969 valid_until: Some(later),
970 },
971 long_timeout,
972 ),
973 ];
974
975 for (l, d) in range_items {
976 let duration = l.cache_duration();
977 assert!(duration <= d);
979 assert!(d - duration <= Duration::from_secs(1));
982 }
983 }
984}