1use std::collections::HashMap;
93use std::fmt;
94use std::net::IpAddr;
95use std::str::FromStr;
96
97use chrono::{DateTime, NaiveDateTime, Utc};
98use derive_builder::Builder;
99
100use crate::version::Version;
101use crate::Error;
102
103use super::authority::DirectoryAuthority;
104use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
105
106fn is_valid_fingerprint(fingerprint: &str) -> bool {
110 fingerprint.len() == 40 && fingerprint.chars().all(|c| c.is_ascii_hexdigit())
111}
112
113#[derive(Debug, Clone, PartialEq)]
124pub struct SharedRandomness {
125 pub num_reveals: u32,
127 pub value: String,
129}
130
131#[derive(Debug, Clone, PartialEq)]
144pub struct DocumentSignature {
145 pub identity: String,
147 pub signing_key_digest: String,
149 pub signature: String,
151 pub algorithm: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Builder)]
209#[builder(setter(into, strip_option))]
210pub struct NetworkStatusDocument {
211 pub version: u32,
213 pub version_flavor: String,
215 pub is_consensus: bool,
217 pub is_vote: bool,
219 pub is_microdescriptor: bool,
221 #[builder(default)]
223 pub consensus_method: Option<u32>,
224 #[builder(default)]
226 pub consensus_methods: Option<Vec<u32>>,
227 #[builder(default)]
229 pub published: Option<DateTime<Utc>>,
230 pub valid_after: DateTime<Utc>,
232 pub fresh_until: DateTime<Utc>,
234 pub valid_until: DateTime<Utc>,
236 #[builder(default)]
238 pub vote_delay: Option<u32>,
239 #[builder(default)]
241 pub dist_delay: Option<u32>,
242 #[builder(default)]
244 pub client_versions: Vec<Version>,
245 #[builder(default)]
247 pub server_versions: Vec<Version>,
248 #[builder(default)]
250 pub known_flags: Vec<String>,
251 #[builder(default)]
253 pub recommended_client_protocols: HashMap<String, Vec<u32>>,
254 #[builder(default)]
256 pub recommended_relay_protocols: HashMap<String, Vec<u32>>,
257 #[builder(default)]
259 pub required_client_protocols: HashMap<String, Vec<u32>>,
260 #[builder(default)]
262 pub required_relay_protocols: HashMap<String, Vec<u32>>,
263 #[builder(default)]
265 pub params: HashMap<String, i32>,
266 #[builder(default)]
268 pub shared_randomness_previous: Option<SharedRandomness>,
269 #[builder(default)]
271 pub shared_randomness_current: Option<SharedRandomness>,
272 #[builder(default)]
274 pub bandwidth_weights: HashMap<String, i32>,
275 #[builder(default)]
277 pub authorities: Vec<DirectoryAuthority>,
278 #[builder(default)]
280 pub signatures: Vec<DocumentSignature>,
281 #[builder(default)]
283 raw_content: Vec<u8>,
284 #[builder(default)]
286 unrecognized_lines: Vec<String>,
287}
288
289impl NetworkStatusDocument {
290 pub fn validate(&self) -> Result<(), Error> {
316 use crate::descriptor::ConsensusError;
317
318 if self.valid_after >= self.fresh_until {
319 return Err(Error::Descriptor(
320 crate::descriptor::DescriptorError::Consensus(
321 ConsensusError::TimestampOrderingViolation(format!(
322 "valid-after ({}) must be before fresh-until ({})",
323 self.valid_after.to_rfc3339(),
324 self.fresh_until.to_rfc3339()
325 )),
326 ),
327 ));
328 }
329
330 if self.fresh_until >= self.valid_until {
331 return Err(Error::Descriptor(
332 crate::descriptor::DescriptorError::Consensus(
333 ConsensusError::TimestampOrderingViolation(format!(
334 "fresh-until ({}) must be before valid-until ({})",
335 self.fresh_until.to_rfc3339(),
336 self.valid_until.to_rfc3339()
337 )),
338 ),
339 ));
340 }
341
342 if self.version != 3 {
343 return Err(Error::Descriptor(
344 crate::descriptor::DescriptorError::Consensus(
345 ConsensusError::InvalidNetworkStatusVersion(self.version.to_string()),
346 ),
347 ));
348 }
349
350 for authority in &self.authorities {
351 if !is_valid_fingerprint(&authority.v3ident) {
352 return Err(Error::Descriptor(
353 crate::descriptor::DescriptorError::Consensus(
354 ConsensusError::InvalidFingerprint(authority.v3ident.clone()),
355 ),
356 ));
357 }
358 }
359
360 if self.signatures.is_empty() {
361 return Err(Error::Descriptor(
362 crate::descriptor::DescriptorError::Consensus(
363 ConsensusError::MissingRequiredField("signatures".to_string()),
364 ),
365 ));
366 }
367
368 for signature in &self.signatures {
369 if !is_valid_fingerprint(&signature.identity) {
370 return Err(Error::Descriptor(
371 crate::descriptor::DescriptorError::Consensus(
372 ConsensusError::InvalidFingerprint(signature.identity.clone()),
373 ),
374 ));
375 }
376
377 if !is_valid_fingerprint(&signature.signing_key_digest) {
378 return Err(Error::Descriptor(
379 crate::descriptor::DescriptorError::Consensus(
380 ConsensusError::InvalidFingerprint(signature.signing_key_digest.clone()),
381 ),
382 ));
383 }
384
385 if signature.signature.is_empty() {
386 return Err(Error::Descriptor(
387 crate::descriptor::DescriptorError::Consensus(
388 ConsensusError::InvalidSignature("signature is empty".to_string()),
389 ),
390 ));
391 }
392 }
393
394 Ok(())
395 }
396
397 fn parse_timestamp(value: &str) -> Result<DateTime<Utc>, Error> {
398 let datetime =
399 NaiveDateTime::parse_from_str(value.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
400 Error::Parse {
401 location: "timestamp".to_string(),
402 reason: format!("invalid datetime: {} - {}", value, e),
403 }
404 })?;
405 Ok(datetime.and_utc())
406 }
407
408 fn parse_network_status_version(
409 value: &str,
410 builder: &mut NetworkStatusDocumentBuilder,
411 ) -> Result<(), Error> {
412 let parts: Vec<&str> = value.split_whitespace().collect();
413 let version: u32 = parts.first().and_then(|v| v.parse().ok()).unwrap_or(3);
414 builder.version(version);
415
416 if let Some(flavor) = parts.get(1) {
417 builder.version_flavor(flavor.to_string());
418 builder.is_microdescriptor(*flavor == "microdesc");
419 } else {
420 builder.version_flavor("ns".to_string());
421 builder.is_microdescriptor(false);
422 }
423 Ok(())
424 }
425
426 fn parse_vote_status(
427 value: &str,
428 builder: &mut NetworkStatusDocumentBuilder,
429 ) -> Result<(), Error> {
430 builder.is_consensus(value == "consensus");
431 builder.is_vote(value == "vote");
432 Ok(())
433 }
434
435 fn parse_voting_delay(
436 value: &str,
437 builder: &mut NetworkStatusDocumentBuilder,
438 ) -> Result<(), Error> {
439 let parts: Vec<&str> = value.split_whitespace().collect();
440 if parts.len() >= 2 {
441 if let Ok(vote) = parts[0].parse::<u32>() {
442 builder.vote_delay(vote);
443 }
444 if let Ok(dist) = parts[1].parse::<u32>() {
445 builder.dist_delay(dist);
446 }
447 }
448 Ok(())
449 }
450
451 fn parse_directory_signature(
452 value: &str,
453 lines: &[&str],
454 idx: usize,
455 _builder: &mut NetworkStatusDocumentBuilder,
456 ) -> Result<(DocumentSignature, usize), Error> {
457 let parts: Vec<&str> = value.split_whitespace().collect();
458 let (algorithm, identity, signing_key_digest) = if parts.len() >= 3 {
459 (
460 Some(parts[0].to_string()),
461 parts[1].to_string(),
462 parts[2].to_string(),
463 )
464 } else if parts.len() >= 2 {
465 (None, parts[0].to_string(), parts[1].to_string())
466 } else {
467 (None, String::new(), String::new())
468 };
469 let (signature, end_idx) = Self::extract_pem_block(lines, idx + 1);
470 Ok((
471 DocumentSignature {
472 identity,
473 signing_key_digest,
474 signature,
475 algorithm,
476 },
477 end_idx,
478 ))
479 }
480
481 fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
482 let mut protocols = HashMap::new();
483 for entry in value.split_whitespace() {
484 if let Some(eq_pos) = entry.find('=') {
485 let proto_name = &entry[..eq_pos];
486 let versions_str = &entry[eq_pos + 1..];
487 let versions: Vec<u32> = versions_str
488 .split(',')
489 .filter_map(|v| {
490 if let Some(dash) = v.find('-') {
491 let start: u32 = v[..dash].parse().ok()?;
492 let end: u32 = v[dash + 1..].parse().ok()?;
493 Some((start..=end).collect::<Vec<_>>())
494 } else {
495 v.parse().ok().map(|n| vec![n])
496 }
497 })
498 .flatten()
499 .collect();
500 protocols.insert(proto_name.to_string(), versions);
501 }
502 }
503 protocols
504 }
505
506 fn parse_params(value: &str) -> HashMap<String, i32> {
507 let mut params = HashMap::new();
508 for entry in value.split_whitespace() {
509 if let Some(eq_pos) = entry.find('=') {
510 let key = &entry[..eq_pos];
511 let val_str = &entry[eq_pos + 1..];
512 if let Ok(val) = val_str.parse::<i32>() {
513 params.insert(key.to_string(), val);
514 }
515 }
516 }
517 params
518 }
519
520 fn parse_shared_randomness(value: &str) -> Option<SharedRandomness> {
521 let parts: Vec<&str> = value.split_whitespace().collect();
522 if parts.len() >= 2 {
523 let num_reveals = parts[0].parse().ok()?;
524 let value = parts[1].to_string();
525 Some(SharedRandomness { num_reveals, value })
526 } else {
527 None
528 }
529 }
530
531 fn parse_versions(value: &str) -> Vec<Version> {
532 value
533 .split(',')
534 .filter_map(|v| {
535 let v = v.trim();
536 if v.is_empty() {
537 None
538 } else {
539 Version::parse(v).ok()
540 }
541 })
542 .collect()
543 }
544
545 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
546 let mut block = String::new();
547 let mut idx = start_idx;
548 while idx < lines.len() {
549 let line = lines[idx];
550 block.push_str(line);
551 block.push('\n');
552 if line.starts_with("-----END ") {
553 break;
554 }
555 idx += 1;
556 }
557 (block.trim_end().to_string(), idx)
558 }
559
560 fn parse_dir_source(value: &str) -> Result<DirectoryAuthority, Error> {
561 let parts: Vec<&str> = value.split_whitespace().collect();
562 if parts.len() < 6 {
563 return Err(Error::Parse {
564 location: "dir-source".to_string(),
565 reason: "dir-source requires 6 fields".to_string(),
566 });
567 }
568 let nickname = parts[0].to_string();
569 let v3ident = parts[1].to_string();
570 let hostname = parts[2].to_string();
571 let address: IpAddr = parts[3].parse().map_err(|_| Error::Parse {
572 location: "dir-source".to_string(),
573 reason: format!("invalid address: {}", parts[3]),
574 })?;
575 let dir_port: Option<u16> = {
576 let port: u16 = parts[4].parse().map_err(|_| Error::Parse {
577 location: "dir-source".to_string(),
578 reason: format!("invalid dir_port: {}", parts[4]),
579 })?;
580 if port == 0 {
581 None
582 } else {
583 Some(port)
584 }
585 };
586 let or_port: u16 = parts[5].parse().map_err(|_| Error::Parse {
587 location: "dir-source".to_string(),
588 reason: format!("invalid or_port: {}", parts[5]),
589 })?;
590 let is_legacy = nickname.ends_with("-legacy");
591 Ok(DirectoryAuthority {
592 nickname,
593 v3ident,
594 hostname,
595 address,
596 dir_port,
597 or_port,
598 is_legacy,
599 contact: None,
600 vote_digest: None,
601 legacy_dir_key: None,
602 key_certificate: None,
603 is_shared_randomness_participate: false,
604 shared_randomness_commitments: Vec::new(),
605 shared_randomness_previous_reveal_count: None,
606 shared_randomness_previous_value: None,
607 shared_randomness_current_reveal_count: None,
608 shared_randomness_current_value: None,
609 raw_content: Vec::new(),
610 unrecognized_lines: Vec::new(),
611 })
612 }
613}
614
615impl Descriptor for NetworkStatusDocument {
616 fn parse(content: &str) -> Result<Self, Error> {
617 let raw_content = content.as_bytes().to_vec();
618 let lines: Vec<&str> = content.lines().collect();
619
620 let mut builder = NetworkStatusDocumentBuilder::default();
621 let mut unrecognized_lines: Vec<String> = Vec::new();
622 let mut current_authority: Option<DirectoryAuthority> = None;
623
624 let mut idx = 0;
625 while idx < lines.len() {
626 let line = lines[idx];
627
628 if line.starts_with("@type ") {
629 idx += 1;
630 continue;
631 }
632
633 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
634 (&line[..space_pos], line[space_pos + 1..].trim())
635 } else {
636 (line, "")
637 };
638
639 match keyword {
640 "network-status-version" => {
641 Self::parse_network_status_version(value, &mut builder)?;
642 }
643 "vote-status" => {
644 Self::parse_vote_status(value, &mut builder)?;
645 }
646 "consensus-method" => {
647 if let Ok(method) = value.parse::<u32>() {
648 builder.consensus_method(method);
649 }
650 }
651 "consensus-methods" => {
652 let methods: Vec<u32> = value
653 .split_whitespace()
654 .filter_map(|v| v.parse().ok())
655 .collect();
656 builder.consensus_methods(methods);
657 }
658 "published" => {
659 builder.published(Self::parse_timestamp(value)?);
660 }
661 "valid-after" => {
662 builder.valid_after(Self::parse_timestamp(value)?);
663 }
664 "fresh-until" => {
665 builder.fresh_until(Self::parse_timestamp(value)?);
666 }
667 "valid-until" => {
668 builder.valid_until(Self::parse_timestamp(value)?);
669 }
670 "voting-delay" => {
671 Self::parse_voting_delay(value, &mut builder)?;
672 }
673 "client-versions" => {
674 builder.client_versions(Self::parse_versions(value));
675 }
676 "server-versions" => {
677 builder.server_versions(Self::parse_versions(value));
678 }
679 "known-flags" => {
680 let flags: Vec<String> =
681 value.split_whitespace().map(|s| s.to_string()).collect();
682 builder.known_flags(flags);
683 }
684 "recommended-client-protocols" => {
685 builder.recommended_client_protocols(Self::parse_protocols(value));
686 }
687 "recommended-relay-protocols" => {
688 builder.recommended_relay_protocols(Self::parse_protocols(value));
689 }
690 "required-client-protocols" => {
691 builder.required_client_protocols(Self::parse_protocols(value));
692 }
693 "required-relay-protocols" => {
694 builder.required_relay_protocols(Self::parse_protocols(value));
695 }
696 "params" => {
697 builder.params(Self::parse_params(value));
698 }
699 "shared-rand-previous-value" => {
700 if let Some(sr) = Self::parse_shared_randomness(value) {
701 builder.shared_randomness_previous(sr);
702 }
703 }
704 "shared-rand-current-value" => {
705 if let Some(sr) = Self::parse_shared_randomness(value) {
706 builder.shared_randomness_current(sr);
707 }
708 }
709 "bandwidth-weights" => {
710 builder.bandwidth_weights(Self::parse_params(value));
711 }
712 "dir-source" => {
713 if let Some(auth) = current_authority.take() {
714 let mut auths = builder.authorities.take().unwrap_or_default();
715 auths.push(auth);
716 builder.authorities(auths);
717 }
718 current_authority = Some(Self::parse_dir_source(value)?);
719 }
720 "contact" => {
721 if let Some(ref mut auth) = current_authority {
722 auth.contact = Some(value.to_string());
723 }
724 }
725 "vote-digest" => {
726 if let Some(ref mut auth) = current_authority {
727 auth.vote_digest = Some(value.to_string());
728 }
729 }
730 "legacy-dir-key" => {
731 if let Some(ref mut auth) = current_authority {
732 auth.legacy_dir_key = Some(value.to_string());
733 }
734 }
735 "directory-signature" => {
736 if let Some(auth) = current_authority.take() {
737 let mut auths = builder.authorities.take().unwrap_or_default();
738 auths.push(auth);
739 builder.authorities(auths);
740 }
741 let (signature, end_idx) =
742 Self::parse_directory_signature(value, &lines, idx, &mut builder)?;
743 let mut sigs = builder.signatures.take().unwrap_or_default();
744 sigs.push(signature);
745 builder.signatures(sigs);
746 idx = end_idx;
747 }
748 "r" | "s" | "v" | "pr" | "w" | "p" | "m" | "a" => {
749 if let Some(auth) = current_authority.take() {
750 let mut auths = builder.authorities.take().unwrap_or_default();
751 auths.push(auth);
752 builder.authorities(auths);
753 }
754 }
755 "directory-footer" => {}
756 _ => {
757 if !line.is_empty() && !line.starts_with("-----") {
758 unrecognized_lines.push(line.to_string());
759 }
760 }
761 }
762 idx += 1;
763 }
764
765 if let Some(auth) = current_authority.take() {
766 let mut auths = builder.authorities.take().unwrap_or_default();
767 auths.push(auth);
768 builder.authorities(auths);
769 }
770
771 builder.raw_content(raw_content);
772 builder.unrecognized_lines(unrecognized_lines);
773
774 builder.build().map_err(|e| Error::Parse {
775 location: "NetworkStatusDocument".to_string(),
776 reason: format!("builder error: {}", e),
777 })
778 }
779
780 fn to_descriptor_string(&self) -> String {
781 let mut result = String::new();
782
783 if self.is_microdescriptor {
784 result.push_str(&format!(
785 "network-status-version {} microdesc\n",
786 self.version
787 ));
788 } else {
789 result.push_str(&format!("network-status-version {}\n", self.version));
790 }
791
792 if self.is_consensus {
793 result.push_str("vote-status consensus\n");
794 } else {
795 result.push_str("vote-status vote\n");
796 }
797
798 if let Some(method) = self.consensus_method {
799 result.push_str(&format!("consensus-method {}\n", method));
800 }
801
802 if let Some(ref methods) = self.consensus_methods {
803 let methods_str: Vec<String> = methods.iter().map(|m| m.to_string()).collect();
804 result.push_str(&format!("consensus-methods {}\n", methods_str.join(" ")));
805 }
806
807 if let Some(published) = self.published {
808 result.push_str(&format!(
809 "published {}\n",
810 published.format("%Y-%m-%d %H:%M:%S")
811 ));
812 }
813
814 result.push_str(&format!(
815 "valid-after {}\n",
816 self.valid_after.format("%Y-%m-%d %H:%M:%S")
817 ));
818 result.push_str(&format!(
819 "fresh-until {}\n",
820 self.fresh_until.format("%Y-%m-%d %H:%M:%S")
821 ));
822 result.push_str(&format!(
823 "valid-until {}\n",
824 self.valid_until.format("%Y-%m-%d %H:%M:%S")
825 ));
826
827 if let (Some(vote), Some(dist)) = (self.vote_delay, self.dist_delay) {
828 result.push_str(&format!("voting-delay {} {}\n", vote, dist));
829 }
830
831 if !self.client_versions.is_empty() {
832 let versions: Vec<String> =
833 self.client_versions.iter().map(|v| v.to_string()).collect();
834 result.push_str(&format!("client-versions {}\n", versions.join(",")));
835 }
836
837 if !self.server_versions.is_empty() {
838 let versions: Vec<String> =
839 self.server_versions.iter().map(|v| v.to_string()).collect();
840 result.push_str(&format!("server-versions {}\n", versions.join(",")));
841 }
842
843 if !self.known_flags.is_empty() {
844 result.push_str(&format!("known-flags {}\n", self.known_flags.join(" ")));
845 }
846
847 result
848 }
849
850 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
851 Ok(compute_digest(&self.raw_content, hash, encoding))
852 }
853
854 fn raw_content(&self) -> &[u8] {
855 &self.raw_content
856 }
857
858 fn unrecognized_lines(&self) -> &[String] {
859 &self.unrecognized_lines
860 }
861}
862
863impl FromStr for NetworkStatusDocument {
864 type Err = Error;
865
866 fn from_str(s: &str) -> Result<Self, Self::Err> {
867 Self::parse(s)
868 }
869}
870
871impl fmt::Display for NetworkStatusDocument {
872 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
873 write!(f, "{}", self.to_descriptor_string())
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 const EXAMPLE_CONSENSUS: &str = r#"network-status-version 3
882vote-status consensus
883consensus-method 26
884valid-after 2017-05-25 04:46:30
885fresh-until 2017-05-25 04:46:40
886valid-until 2017-05-25 04:46:50
887voting-delay 2 2
888client-versions
889server-versions
890known-flags Authority Exit Fast Guard HSDir NoEdConsensus Running Stable V2Dir Valid
891recommended-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
892recommended-relay-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
893required-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
894required-relay-protocols Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=3-4 LinkAuth=1 Microdesc=1 Relay=1-2
895dir-source test001a 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
896contact auth1@test.test
897vote-digest 2E7177224BBA39B505F7608FF376C07884CF926F
898dir-source test000a BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7000 5000
899contact auth0@test.test
900vote-digest 5DD41617166FFB82882A117EEFDA0353A2794DC5
901directory-footer
902bandwidth-weights Wbd=3333 Wbe=0 Wbg=0 Wbm=10000
903directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
904-----BEGIN SIGNATURE-----
905Ho0rLojfLHs9cSPFxe6znuGuFU8BvRr6gnH1gULTjUZO0NSQvo5N628KFeAsq+pT
906-----END SIGNATURE-----
907"#;
908
909 #[test]
910 fn test_parse_consensus() {
911 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
912 assert_eq!(doc.version, 3);
913 assert!(doc.is_consensus);
914 assert!(!doc.is_vote);
915 assert!(!doc.is_microdescriptor);
916 assert_eq!(doc.consensus_method, Some(26));
917 assert_eq!(doc.vote_delay, Some(2));
918 assert_eq!(doc.dist_delay, Some(2));
919 }
920
921 #[test]
922 fn test_parse_known_flags() {
923 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
924 assert!(doc.known_flags.contains(&"Authority".to_string()));
925 assert!(doc.known_flags.contains(&"Exit".to_string()));
926 assert!(doc.known_flags.contains(&"Fast".to_string()));
927 assert!(doc.known_flags.contains(&"Guard".to_string()));
928 assert!(doc.known_flags.contains(&"HSDir".to_string()));
929 assert!(doc.known_flags.contains(&"Running".to_string()));
930 assert!(doc.known_flags.contains(&"Stable".to_string()));
931 assert!(doc.known_flags.contains(&"Valid".to_string()));
932 }
933
934 #[test]
935 fn test_parse_protocols() {
936 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
937 assert_eq!(
938 doc.recommended_client_protocols.get("Cons"),
939 Some(&vec![1, 2])
940 );
941 assert_eq!(doc.recommended_client_protocols.get("Link"), Some(&vec![4]));
942 assert_eq!(doc.required_relay_protocols.get("Link"), Some(&vec![3, 4]));
943 }
944
945 #[test]
946 fn test_parse_authorities() {
947 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
948 assert_eq!(doc.authorities.len(), 2);
949 let auth1 = &doc.authorities[0];
950 assert_eq!(auth1.nickname, "test001a");
951 assert_eq!(auth1.v3ident, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
952 assert_eq!(auth1.contact, Some("auth1@test.test".to_string()));
953 assert_eq!(
954 auth1.vote_digest,
955 Some("2E7177224BBA39B505F7608FF376C07884CF926F".to_string())
956 );
957 }
958
959 #[test]
960 fn test_parse_bandwidth_weights() {
961 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
962 assert_eq!(doc.bandwidth_weights.get("Wbd"), Some(&3333));
963 assert_eq!(doc.bandwidth_weights.get("Wbe"), Some(&0));
964 assert_eq!(doc.bandwidth_weights.get("Wbm"), Some(&10000));
965 }
966
967 #[test]
968 fn test_parse_signatures() {
969 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
970 assert_eq!(doc.signatures.len(), 1);
971 let sig = &doc.signatures[0];
972 assert_eq!(sig.identity, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
973 assert_eq!(
974 sig.signing_key_digest,
975 "9FBF54D6A62364320308A615BF4CF6B27B254FAD"
976 );
977 assert!(sig.signature.contains("BEGIN SIGNATURE"));
978 }
979
980 #[test]
981 fn test_parse_timestamps() {
982 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
983 assert_eq!(
984 doc.valid_after.format("%Y-%m-%d %H:%M:%S").to_string(),
985 "2017-05-25 04:46:30"
986 );
987 assert_eq!(
988 doc.fresh_until.format("%Y-%m-%d %H:%M:%S").to_string(),
989 "2017-05-25 04:46:40"
990 );
991 assert_eq!(
992 doc.valid_until.format("%Y-%m-%d %H:%M:%S").to_string(),
993 "2017-05-25 04:46:50"
994 );
995 }
996
997 #[test]
998 fn test_microdescriptor_consensus() {
999 let content = "network-status-version 3 microdesc
1000vote-status consensus
1001valid-after 2017-05-25 04:46:30
1002fresh-until 2017-05-25 04:46:40
1003valid-until 2017-05-25 04:46:50
1004";
1005 let doc = NetworkStatusDocument::parse(content).unwrap();
1006 assert!(doc.is_microdescriptor);
1007 assert_eq!(doc.version_flavor, "microdesc");
1008 }
1009
1010 use proptest::prelude::*;
1011
1012 fn valid_timestamp() -> impl Strategy<Value = DateTime<Utc>> {
1013 (
1014 2015u32..2025,
1015 1u32..13,
1016 1u32..29,
1017 0u32..24,
1018 0u32..60,
1019 0u32..60,
1020 )
1021 .prop_map(|(year, month, day, hour, min, sec)| {
1022 let naive = chrono::NaiveDate::from_ymd_opt(year as i32, month, day)
1023 .unwrap()
1024 .and_hms_opt(hour, min, sec)
1025 .unwrap();
1026 naive.and_utc()
1027 })
1028 }
1029
1030 fn valid_flag() -> impl Strategy<Value = String> {
1031 prop_oneof![
1032 Just("Authority".to_string()),
1033 Just("Exit".to_string()),
1034 Just("Fast".to_string()),
1035 Just("Guard".to_string()),
1036 Just("HSDir".to_string()),
1037 Just("Running".to_string()),
1038 Just("Stable".to_string()),
1039 Just("Valid".to_string()),
1040 ]
1041 }
1042
1043 fn simple_consensus() -> impl Strategy<Value = NetworkStatusDocument> {
1044 (
1045 valid_timestamp(),
1046 valid_timestamp(),
1047 valid_timestamp(),
1048 proptest::collection::vec(valid_flag(), 1..5),
1049 1u32..30,
1050 )
1051 .prop_map(|(valid_after, fresh_until, valid_until, flags, method)| {
1052 let mut doc = NetworkStatusDocument {
1053 version: 3,
1054 version_flavor: String::new(),
1055 is_consensus: true,
1056 is_vote: false,
1057 is_microdescriptor: false,
1058 consensus_method: Some(method),
1059 consensus_methods: None,
1060 published: None,
1061 valid_after,
1062 fresh_until,
1063 valid_until,
1064 vote_delay: Some(2),
1065 dist_delay: Some(2),
1066 client_versions: Vec::new(),
1067 server_versions: Vec::new(),
1068 known_flags: flags,
1069 recommended_client_protocols: HashMap::new(),
1070 recommended_relay_protocols: HashMap::new(),
1071 required_client_protocols: HashMap::new(),
1072 required_relay_protocols: HashMap::new(),
1073 params: HashMap::new(),
1074 shared_randomness_previous: None,
1075 shared_randomness_current: None,
1076 bandwidth_weights: HashMap::new(),
1077 authorities: Vec::new(),
1078 signatures: Vec::new(),
1079 raw_content: Vec::new(),
1080 unrecognized_lines: Vec::new(),
1081 };
1082 doc.known_flags.sort();
1083 doc.known_flags.dedup();
1084 doc
1085 })
1086 }
1087
1088 proptest! {
1089 #![proptest_config(ProptestConfig::with_cases(100))]
1090
1091 #[test]
1092 fn prop_consensus_roundtrip(doc in simple_consensus()) {
1093 let serialized = doc.to_descriptor_string();
1094 let parsed = NetworkStatusDocument::parse(&serialized);
1095
1096 prop_assert!(parsed.is_ok(), "Failed to parse serialized consensus: {:?}", parsed.err());
1097
1098 let parsed = parsed.unwrap();
1099
1100 prop_assert_eq!(doc.version, parsed.version, "version mismatch");
1101 prop_assert_eq!(doc.is_consensus, parsed.is_consensus, "is_consensus mismatch");
1102 }
1103 }
1104}
1105
1106#[cfg(test)]
1107mod comprehensive_tests {
1108 use super::*;
1109
1110 #[test]
1111 fn test_edge_case_empty_client_versions() {
1112 let content = r#"network-status-version 3
1113vote-status consensus
1114valid-after 2017-05-25 04:46:30
1115fresh-until 2017-05-25 04:46:40
1116valid-until 2017-05-25 04:46:50
1117client-versions
1118"#;
1119 let doc = NetworkStatusDocument::parse(content).unwrap();
1120 assert_eq!(doc.client_versions.len(), 0);
1121 }
1122
1123 #[test]
1124 fn test_edge_case_empty_server_versions() {
1125 let content = r#"network-status-version 3
1126vote-status consensus
1127valid-after 2017-05-25 04:46:30
1128fresh-until 2017-05-25 04:46:40
1129valid-until 2017-05-25 04:46:50
1130server-versions
1131"#;
1132 let doc = NetworkStatusDocument::parse(content).unwrap();
1133 assert_eq!(doc.server_versions.len(), 0);
1134 }
1135
1136 #[test]
1137 fn test_edge_case_empty_known_flags() {
1138 let content = r#"network-status-version 3
1139vote-status consensus
1140valid-after 2017-05-25 04:46:30
1141fresh-until 2017-05-25 04:46:40
1142valid-until 2017-05-25 04:46:50
1143known-flags
1144"#;
1145 let doc = NetworkStatusDocument::parse(content).unwrap();
1146 assert_eq!(doc.known_flags.len(), 0);
1147 }
1148
1149 #[test]
1150 fn test_edge_case_empty_params() {
1151 let content = r#"network-status-version 3
1152vote-status consensus
1153valid-after 2017-05-25 04:46:30
1154fresh-until 2017-05-25 04:46:40
1155valid-until 2017-05-25 04:46:50
1156params
1157"#;
1158 let doc = NetworkStatusDocument::parse(content).unwrap();
1159 assert_eq!(doc.params.len(), 0);
1160 }
1161
1162 #[test]
1163 fn test_edge_case_empty_bandwidth_weights() {
1164 let content = r#"network-status-version 3
1165vote-status consensus
1166valid-after 2017-05-25 04:46:30
1167fresh-until 2017-05-25 04:46:40
1168valid-until 2017-05-25 04:46:50
1169directory-footer
1170bandwidth-weights
1171"#;
1172 let doc = NetworkStatusDocument::parse(content).unwrap();
1173 assert_eq!(doc.bandwidth_weights.len(), 0);
1174 }
1175
1176 #[test]
1177 fn test_edge_case_multiple_authorities() {
1178 let content = r#"network-status-version 3
1179vote-status consensus
1180valid-after 2017-05-25 04:46:30
1181fresh-until 2017-05-25 04:46:40
1182valid-until 2017-05-25 04:46:50
1183dir-source auth1 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
1184dir-source auth2 BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7002 5002
1185dir-source auth3 ABC380A633592C218757BEE11E630511A485658B 127.0.0.1 127.0.0.1 7003 5003
1186"#;
1187 let doc = NetworkStatusDocument::parse(content).unwrap();
1188 assert_eq!(doc.authorities.len(), 3);
1189 assert_eq!(doc.authorities[0].nickname, "auth1");
1190 assert_eq!(doc.authorities[1].nickname, "auth2");
1191 assert_eq!(doc.authorities[2].nickname, "auth3");
1192 }
1193
1194 #[test]
1195 fn test_edge_case_multiple_signatures() {
1196 let content = r#"network-status-version 3
1197vote-status consensus
1198valid-after 2017-05-25 04:46:30
1199fresh-until 2017-05-25 04:46:40
1200valid-until 2017-05-25 04:46:50
1201directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
1202-----BEGIN SIGNATURE-----
1203sig1
1204-----END SIGNATURE-----
1205directory-signature BCB380A633592C218757BEE11E630511A485658A 8FBF54D6A62364320308A615BF4CF6B27B254FAE
1206-----BEGIN SIGNATURE-----
1207sig2
1208-----END SIGNATURE-----
1209"#;
1210 let doc = NetworkStatusDocument::parse(content).unwrap();
1211 assert_eq!(doc.signatures.len(), 2);
1212 assert_eq!(
1213 doc.signatures[0].identity,
1214 "596CD48D61FDA4E868F4AA10FF559917BE3B1A35"
1215 );
1216 assert_eq!(
1217 doc.signatures[1].identity,
1218 "BCB380A633592C218757BEE11E630511A485658A"
1219 );
1220 }
1221
1222 #[test]
1223 fn test_edge_case_shared_randomness_both() {
1224 let content = r#"network-status-version 3
1225vote-status consensus
1226valid-after 2017-05-25 04:46:30
1227fresh-until 2017-05-25 04:46:40
1228valid-until 2017-05-25 04:46:50
1229shared-rand-previous-value 9 abcdef1234567890
1230shared-rand-current-value 10 1234567890abcdef
1231"#;
1232 let doc = NetworkStatusDocument::parse(content).unwrap();
1233 assert!(doc.shared_randomness_previous.is_some());
1234 assert!(doc.shared_randomness_current.is_some());
1235 let prev = doc.shared_randomness_previous.unwrap();
1236 assert_eq!(prev.num_reveals, 9);
1237 assert_eq!(prev.value, "abcdef1234567890");
1238 let curr = doc.shared_randomness_current.unwrap();
1239 assert_eq!(curr.num_reveals, 10);
1240 assert_eq!(curr.value, "1234567890abcdef");
1241 }
1242
1243 #[test]
1244 fn test_edge_case_vote_document() {
1245 let content = r#"network-status-version 3
1246vote-status vote
1247consensus-methods 25 26 27
1248published 2017-05-25 04:46:20
1249valid-after 2017-05-25 04:46:30
1250fresh-until 2017-05-25 04:46:40
1251valid-until 2017-05-25 04:46:50
1252"#;
1253 let doc = NetworkStatusDocument::parse(content).unwrap();
1254 assert!(!doc.is_consensus);
1255 assert!(doc.is_vote);
1256 assert!(doc.consensus_methods.is_some());
1257 assert_eq!(doc.consensus_methods.unwrap(), vec![25, 26, 27]);
1258 assert!(doc.published.is_some());
1259 }
1260
1261 #[test]
1262 fn test_edge_case_protocol_ranges() {
1263 let content = r#"network-status-version 3
1264vote-status consensus
1265valid-after 2017-05-25 04:46:30
1266fresh-until 2017-05-25 04:46:40
1267valid-until 2017-05-25 04:46:50
1268recommended-client-protocols Cons=1-2 Link=1-5 Relay=1-3
1269"#;
1270 let doc = NetworkStatusDocument::parse(content).unwrap();
1271 assert_eq!(
1272 doc.recommended_client_protocols.get("Cons"),
1273 Some(&vec![1, 2])
1274 );
1275 assert_eq!(
1276 doc.recommended_client_protocols.get("Link"),
1277 Some(&vec![1, 2, 3, 4, 5])
1278 );
1279 assert_eq!(
1280 doc.recommended_client_protocols.get("Relay"),
1281 Some(&vec![1, 2, 3])
1282 );
1283 }
1284
1285 #[test]
1286 fn test_edge_case_protocol_mixed_ranges() {
1287 let content = r#"network-status-version 3
1288vote-status consensus
1289valid-after 2017-05-25 04:46:30
1290fresh-until 2017-05-25 04:46:40
1291valid-until 2017-05-25 04:46:50
1292required-relay-protocols Link=1-3,5,7-9
1293"#;
1294 let doc = NetworkStatusDocument::parse(content).unwrap();
1295 let link_protos = doc.required_relay_protocols.get("Link").unwrap();
1296 assert!(link_protos.contains(&1));
1297 assert!(link_protos.contains(&2));
1298 assert!(link_protos.contains(&3));
1299 assert!(link_protos.contains(&5));
1300 assert!(link_protos.contains(&7));
1301 assert!(link_protos.contains(&8));
1302 assert!(link_protos.contains(&9));
1303 assert!(!link_protos.contains(&4));
1304 assert!(!link_protos.contains(&6));
1305 }
1306
1307 #[test]
1308 fn test_edge_case_params_negative_values() {
1309 let content = r#"network-status-version 3
1310vote-status consensus
1311valid-after 2017-05-25 04:46:30
1312fresh-until 2017-05-25 04:46:40
1313valid-until 2017-05-25 04:46:50
1314params param1=100 param2=-50 param3=0
1315"#;
1316 let doc = NetworkStatusDocument::parse(content).unwrap();
1317 assert_eq!(doc.params.get("param1"), Some(&100));
1318 assert_eq!(doc.params.get("param2"), Some(&-50));
1319 assert_eq!(doc.params.get("param3"), Some(&0));
1320 }
1321
1322 #[test]
1323 fn test_edge_case_bandwidth_weights_negative() {
1324 let content = r#"network-status-version 3
1325vote-status consensus
1326valid-after 2017-05-25 04:46:30
1327fresh-until 2017-05-25 04:46:40
1328valid-until 2017-05-25 04:46:50
1329directory-footer
1330bandwidth-weights Wbd=3333 Wbe=-100 Wbg=0
1331"#;
1332 let doc = NetworkStatusDocument::parse(content).unwrap();
1333 assert_eq!(doc.bandwidth_weights.get("Wbd"), Some(&3333));
1334 assert_eq!(doc.bandwidth_weights.get("Wbe"), Some(&-100));
1335 assert_eq!(doc.bandwidth_weights.get("Wbg"), Some(&0));
1336 }
1337
1338 #[test]
1339 fn test_edge_case_unrecognized_lines() {
1340 let content = r#"network-status-version 3
1341vote-status consensus
1342valid-after 2017-05-25 04:46:30
1343fresh-until 2017-05-25 04:46:40
1344valid-until 2017-05-25 04:46:50
1345unknown-field some value
1346another-unknown another value
1347"#;
1348 let doc = NetworkStatusDocument::parse(content).unwrap();
1349 assert_eq!(doc.unrecognized_lines.len(), 2);
1350 assert!(doc
1351 .unrecognized_lines
1352 .contains(&"unknown-field some value".to_string()));
1353 assert!(doc
1354 .unrecognized_lines
1355 .contains(&"another-unknown another value".to_string()));
1356 }
1357
1358 #[test]
1359 fn test_validation_invalid_timestamp_ordering() {
1360 let doc = NetworkStatusDocument {
1361 version: 3,
1362 version_flavor: String::new(),
1363 is_consensus: true,
1364 is_vote: false,
1365 is_microdescriptor: false,
1366 consensus_method: Some(26),
1367 consensus_methods: None,
1368 published: None,
1369 valid_after: Utc::now(),
1370 fresh_until: Utc::now() - chrono::Duration::hours(1),
1371 valid_until: Utc::now() + chrono::Duration::hours(1),
1372 vote_delay: Some(2),
1373 dist_delay: Some(2),
1374 client_versions: Vec::new(),
1375 server_versions: Vec::new(),
1376 known_flags: Vec::new(),
1377 recommended_client_protocols: HashMap::new(),
1378 recommended_relay_protocols: HashMap::new(),
1379 required_client_protocols: HashMap::new(),
1380 required_relay_protocols: HashMap::new(),
1381 params: HashMap::new(),
1382 shared_randomness_previous: None,
1383 shared_randomness_current: None,
1384 bandwidth_weights: HashMap::new(),
1385 authorities: Vec::new(),
1386 signatures: Vec::new(),
1387 raw_content: Vec::new(),
1388 unrecognized_lines: Vec::new(),
1389 };
1390 let result = doc.validate();
1391 assert!(result.is_err());
1392 match result.unwrap_err() {
1393 Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1394 crate::descriptor::ConsensusError::TimestampOrderingViolation(_),
1395 )) => {}
1396 _ => panic!("Expected TimestampOrderingViolation error"),
1397 }
1398 }
1399
1400 #[test]
1401 fn test_validation_invalid_version() {
1402 let doc = NetworkStatusDocument {
1403 version: 2,
1404 version_flavor: String::new(),
1405 is_consensus: true,
1406 is_vote: false,
1407 is_microdescriptor: false,
1408 consensus_method: Some(26),
1409 consensus_methods: None,
1410 published: None,
1411 valid_after: Utc::now(),
1412 fresh_until: Utc::now() + chrono::Duration::hours(1),
1413 valid_until: Utc::now() + chrono::Duration::hours(2),
1414 vote_delay: Some(2),
1415 dist_delay: Some(2),
1416 client_versions: Vec::new(),
1417 server_versions: Vec::new(),
1418 known_flags: Vec::new(),
1419 recommended_client_protocols: HashMap::new(),
1420 recommended_relay_protocols: HashMap::new(),
1421 required_client_protocols: HashMap::new(),
1422 required_relay_protocols: HashMap::new(),
1423 params: HashMap::new(),
1424 shared_randomness_previous: None,
1425 shared_randomness_current: None,
1426 bandwidth_weights: HashMap::new(),
1427 authorities: Vec::new(),
1428 signatures: Vec::new(),
1429 raw_content: Vec::new(),
1430 unrecognized_lines: Vec::new(),
1431 };
1432 let result = doc.validate();
1433 assert!(result.is_err());
1434 match result.unwrap_err() {
1435 Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1436 crate::descriptor::ConsensusError::InvalidNetworkStatusVersion(_),
1437 )) => {}
1438 _ => panic!("Expected InvalidNetworkStatusVersion error"),
1439 }
1440 }
1441
1442 #[test]
1443 fn test_validation_invalid_authority_fingerprint() {
1444 let doc = NetworkStatusDocument {
1445 version: 3,
1446 version_flavor: String::new(),
1447 is_consensus: true,
1448 is_vote: false,
1449 is_microdescriptor: false,
1450 consensus_method: Some(26),
1451 consensus_methods: None,
1452 published: None,
1453 valid_after: Utc::now(),
1454 fresh_until: Utc::now() + chrono::Duration::hours(1),
1455 valid_until: Utc::now() + chrono::Duration::hours(2),
1456 vote_delay: Some(2),
1457 dist_delay: Some(2),
1458 client_versions: Vec::new(),
1459 server_versions: Vec::new(),
1460 known_flags: Vec::new(),
1461 recommended_client_protocols: HashMap::new(),
1462 recommended_relay_protocols: HashMap::new(),
1463 required_client_protocols: HashMap::new(),
1464 required_relay_protocols: HashMap::new(),
1465 params: HashMap::new(),
1466 shared_randomness_previous: None,
1467 shared_randomness_current: None,
1468 bandwidth_weights: HashMap::new(),
1469 authorities: vec![DirectoryAuthority {
1470 nickname: "test".to_string(),
1471 v3ident: "INVALID".to_string(),
1472 hostname: "test.test".to_string(),
1473 address: "127.0.0.1".parse().unwrap(),
1474 dir_port: Some(7000),
1475 or_port: 5000,
1476 is_legacy: false,
1477 contact: None,
1478 vote_digest: None,
1479 legacy_dir_key: None,
1480 key_certificate: None,
1481 is_shared_randomness_participate: false,
1482 shared_randomness_commitments: Vec::new(),
1483 shared_randomness_previous_reveal_count: None,
1484 shared_randomness_previous_value: None,
1485 shared_randomness_current_reveal_count: None,
1486 shared_randomness_current_value: None,
1487 raw_content: Vec::new(),
1488 unrecognized_lines: Vec::new(),
1489 }],
1490 signatures: Vec::new(),
1491 raw_content: Vec::new(),
1492 unrecognized_lines: Vec::new(),
1493 };
1494 let result = doc.validate();
1495 assert!(result.is_err());
1496 match result.unwrap_err() {
1497 Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1498 crate::descriptor::ConsensusError::InvalidFingerprint(_),
1499 )) => {}
1500 _ => panic!("Expected InvalidFingerprint error"),
1501 }
1502 }
1503
1504 #[test]
1505 fn test_validation_missing_signatures() {
1506 let doc = NetworkStatusDocument {
1507 version: 3,
1508 version_flavor: String::new(),
1509 is_consensus: true,
1510 is_vote: false,
1511 is_microdescriptor: false,
1512 consensus_method: Some(26),
1513 consensus_methods: None,
1514 published: None,
1515 valid_after: Utc::now(),
1516 fresh_until: Utc::now() + chrono::Duration::hours(1),
1517 valid_until: Utc::now() + chrono::Duration::hours(2),
1518 vote_delay: Some(2),
1519 dist_delay: Some(2),
1520 client_versions: Vec::new(),
1521 server_versions: Vec::new(),
1522 known_flags: Vec::new(),
1523 recommended_client_protocols: HashMap::new(),
1524 recommended_relay_protocols: HashMap::new(),
1525 required_client_protocols: HashMap::new(),
1526 required_relay_protocols: HashMap::new(),
1527 params: HashMap::new(),
1528 shared_randomness_previous: None,
1529 shared_randomness_current: None,
1530 bandwidth_weights: HashMap::new(),
1531 authorities: Vec::new(),
1532 signatures: Vec::new(),
1533 raw_content: Vec::new(),
1534 unrecognized_lines: Vec::new(),
1535 };
1536 let result = doc.validate();
1537 assert!(result.is_err());
1538 match result.unwrap_err() {
1539 Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1540 crate::descriptor::ConsensusError::MissingRequiredField(_),
1541 )) => {}
1542 _ => panic!("Expected MissingRequiredField error"),
1543 }
1544 }
1545
1546 #[test]
1547 fn test_validation_valid_consensus() {
1548 let doc = NetworkStatusDocument {
1549 version: 3,
1550 version_flavor: String::new(),
1551 is_consensus: true,
1552 is_vote: false,
1553 is_microdescriptor: false,
1554 consensus_method: Some(26),
1555 consensus_methods: None,
1556 published: None,
1557 valid_after: Utc::now(),
1558 fresh_until: Utc::now() + chrono::Duration::hours(1),
1559 valid_until: Utc::now() + chrono::Duration::hours(2),
1560 vote_delay: Some(2),
1561 dist_delay: Some(2),
1562 client_versions: Vec::new(),
1563 server_versions: Vec::new(),
1564 known_flags: Vec::new(),
1565 recommended_client_protocols: HashMap::new(),
1566 recommended_relay_protocols: HashMap::new(),
1567 required_client_protocols: HashMap::new(),
1568 required_relay_protocols: HashMap::new(),
1569 params: HashMap::new(),
1570 shared_randomness_previous: None,
1571 shared_randomness_current: None,
1572 bandwidth_weights: HashMap::new(),
1573 authorities: vec![DirectoryAuthority {
1574 nickname: "test".to_string(),
1575 v3ident: "596CD48D61FDA4E868F4AA10FF559917BE3B1A35".to_string(),
1576 hostname: "test.test".to_string(),
1577 address: "127.0.0.1".parse().unwrap(),
1578 dir_port: Some(7000),
1579 or_port: 5000,
1580 is_legacy: false,
1581 contact: None,
1582 vote_digest: None,
1583 legacy_dir_key: None,
1584 key_certificate: None,
1585 is_shared_randomness_participate: false,
1586 shared_randomness_commitments: Vec::new(),
1587 shared_randomness_previous_reveal_count: None,
1588 shared_randomness_previous_value: None,
1589 shared_randomness_current_reveal_count: None,
1590 shared_randomness_current_value: None,
1591 raw_content: Vec::new(),
1592 unrecognized_lines: Vec::new(),
1593 }],
1594 signatures: vec![DocumentSignature {
1595 identity: "596CD48D61FDA4E868F4AA10FF559917BE3B1A35".to_string(),
1596 signing_key_digest: "9FBF54D6A62364320308A615BF4CF6B27B254FAD".to_string(),
1597 signature: "test".to_string(),
1598 algorithm: None,
1599 }],
1600 raw_content: Vec::new(),
1601 unrecognized_lines: Vec::new(),
1602 };
1603 let result = doc.validate();
1604 assert!(result.is_ok());
1605 }
1606
1607 #[test]
1608 fn test_builder_basic() {
1609 let now = Utc::now();
1610 let doc = NetworkStatusDocumentBuilder::default()
1611 .version(3_u32)
1612 .version_flavor("")
1613 .is_consensus(true)
1614 .is_vote(false)
1615 .is_microdescriptor(false)
1616 .valid_after(now)
1617 .fresh_until(now + chrono::Duration::hours(1))
1618 .valid_until(now + chrono::Duration::hours(2))
1619 .build()
1620 .expect("Failed to build");
1621
1622 assert_eq!(doc.version, 3);
1623 assert!(doc.is_consensus);
1624 assert!(!doc.is_vote);
1625 }
1626
1627 #[test]
1628 fn test_builder_with_optional_fields() {
1629 let now = Utc::now();
1630 let doc = NetworkStatusDocumentBuilder::default()
1631 .version(3_u32)
1632 .version_flavor("microdesc")
1633 .is_consensus(true)
1634 .is_vote(false)
1635 .is_microdescriptor(true)
1636 .consensus_method(26_u32)
1637 .valid_after(now)
1638 .fresh_until(now + chrono::Duration::hours(1))
1639 .valid_until(now + chrono::Duration::hours(2))
1640 .vote_delay(2_u32)
1641 .dist_delay(2_u32)
1642 .build()
1643 .expect("Failed to build");
1644
1645 assert_eq!(doc.version_flavor, "microdesc");
1646 assert!(doc.is_microdescriptor);
1647 assert_eq!(doc.consensus_method, Some(26));
1648 assert_eq!(doc.vote_delay, Some(2));
1649 assert_eq!(doc.dist_delay, Some(2));
1650 }
1651
1652 #[test]
1653 fn test_round_trip_serialization() {
1654 let content = r#"network-status-version 3
1655vote-status consensus
1656consensus-method 26
1657valid-after 2017-05-25 04:46:30
1658fresh-until 2017-05-25 04:46:40
1659valid-until 2017-05-25 04:46:50
1660voting-delay 2 2
1661known-flags Authority Exit Fast Guard
1662"#;
1663 let doc1 = NetworkStatusDocument::parse(content).unwrap();
1664 let serialized = doc1.to_descriptor_string();
1665 let doc2 = NetworkStatusDocument::parse(&serialized).unwrap();
1666
1667 assert_eq!(doc1.version, doc2.version);
1668 assert_eq!(doc1.is_consensus, doc2.is_consensus);
1669 assert_eq!(doc1.consensus_method, doc2.consensus_method);
1670 assert_eq!(doc1.vote_delay, doc2.vote_delay);
1671 assert_eq!(doc1.dist_delay, doc2.dist_delay);
1672 assert_eq!(doc1.known_flags, doc2.known_flags);
1673 }
1674
1675 #[test]
1676 fn test_display_implementation() {
1677 let now = Utc::now();
1678 let doc = NetworkStatusDocument {
1679 version: 3,
1680 version_flavor: String::new(),
1681 is_consensus: true,
1682 is_vote: false,
1683 is_microdescriptor: false,
1684 consensus_method: Some(26),
1685 consensus_methods: None,
1686 published: None,
1687 valid_after: now,
1688 fresh_until: now + chrono::Duration::hours(1),
1689 valid_until: now + chrono::Duration::hours(2),
1690 vote_delay: Some(2),
1691 dist_delay: Some(2),
1692 client_versions: Vec::new(),
1693 server_versions: Vec::new(),
1694 known_flags: Vec::new(),
1695 recommended_client_protocols: HashMap::new(),
1696 recommended_relay_protocols: HashMap::new(),
1697 required_client_protocols: HashMap::new(),
1698 required_relay_protocols: HashMap::new(),
1699 params: HashMap::new(),
1700 shared_randomness_previous: None,
1701 shared_randomness_current: None,
1702 bandwidth_weights: HashMap::new(),
1703 authorities: Vec::new(),
1704 signatures: Vec::new(),
1705 raw_content: Vec::new(),
1706 unrecognized_lines: Vec::new(),
1707 };
1708 let display_str = format!("{}", doc);
1709 assert!(display_str.contains("network-status-version"));
1710 assert!(display_str.contains("vote-status"));
1711 }
1712
1713 #[test]
1714 fn test_from_str_implementation() {
1715 let content = r#"network-status-version 3
1716vote-status consensus
1717valid-after 2017-05-25 04:46:30
1718fresh-until 2017-05-25 04:46:40
1719valid-until 2017-05-25 04:46:50
1720"#;
1721 let doc: NetworkStatusDocument = content.parse().unwrap();
1722 assert_eq!(doc.version, 3);
1723 assert!(doc.is_consensus);
1724 }
1725}