1#[cfg(feature = "network")]
11use crate::url::URLType;
12use crate::{
13 DIDWebVHError, DIDWebVHState,
14 log_entry::{LogEntry, LogEntryMethods, MetaData},
15 log_entry_state::{LogEntryState, LogEntryValidationStatus},
16 parameters::Parameters,
17 url::WebVHURL,
18 witness::proofs::WitnessProofCollection,
19};
20use chrono::DateTime;
21#[cfg(feature = "network")]
22use chrono::Utc;
23#[cfg(feature = "network")]
24use reqwest::{Client, StatusCode};
25#[cfg(feature = "network")]
26use std::time::Duration;
27#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
28use tracing::trace;
29#[cfg(feature = "network")]
30use tracing::warn;
31use tracing::{Instrument, Level, span};
32#[cfg(feature = "network")]
33use url::Url;
34
35#[cfg(feature = "ssi")]
37pub mod ssi_resolve;
38
39pub mod implicit; #[cfg(feature = "network")]
43pub const DEFAULT_MAX_RESPONSE_BYTES: u64 = 200 * 1024;
44
45#[cfg(feature = "network")]
47#[derive(Debug, Clone)]
48pub struct ResolveOptions {
49 pub timeout: Option<Duration>,
51 pub eager_witness_download: bool,
53 pub max_response_bytes: u64,
56}
57
58#[cfg(feature = "network")]
59impl Default for ResolveOptions {
60 fn default() -> Self {
61 Self {
62 timeout: None,
63 eager_witness_download: false,
64 max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
65 }
66 }
67}
68
69#[cfg(feature = "network")]
71pub struct DIDWebVH;
72
73#[cfg(feature = "network")]
74impl DIDWebVH {
75 async fn download_file(
84 client: Client,
85 url: Url,
86 max_bytes: u64,
87 ) -> Result<String, DIDWebVHError> {
88 let url_str = url.to_string();
89 let mut response =
90 client
91 .get(url.clone())
92 .send()
93 .await
94 .map_err(|e| DIDWebVHError::NetworkError {
95 url: url_str.clone(),
96 status_code: None,
97 message: format!("Request failed: {e}"),
98 })?;
99
100 if response.status() != StatusCode::OK {
101 let status = response.status().as_u16();
102 warn!("url ({url_str}): HTTP Status code = {status}");
103 return Err(DIDWebVHError::NetworkError {
104 url: url_str,
105 status_code: Some(status),
106 message: format!("HTTP {status}"),
107 });
108 }
109
110 if let Some(content_length) = response.content_length()
112 && content_length > max_bytes
113 {
114 return Err(DIDWebVHError::ResponseTooLarge {
115 url: url_str,
116 max_bytes,
117 });
118 }
119
120 let mut body = Vec::new();
122 let mut total_bytes: u64 = 0;
123 while let Some(chunk) = response
124 .chunk()
125 .await
126 .map_err(|e| DIDWebVHError::NetworkError {
127 url: url_str.clone(),
128 status_code: Some(200),
129 message: format!("Failed to read response body: {e}"),
130 })?
131 {
132 total_bytes += chunk.len() as u64;
133 if total_bytes > max_bytes {
134 return Err(DIDWebVHError::ResponseTooLarge {
135 url: url_str,
136 max_bytes,
137 });
138 }
139 body.extend_from_slice(&chunk);
140 }
141
142 String::from_utf8(body).map_err(|e| DIDWebVHError::NetworkError {
143 url: url_str,
144 status_code: Some(200),
145 message: format!("Response body is not valid UTF-8: {e}"),
146 })
147 }
148
149 async fn get_log_entries(
151 url: WebVHURL,
152 client: Client,
153 max_bytes: u64,
154 ) -> Result<String, DIDWebVHError> {
155 let log_entries_url = match url.get_http_url(Some("did.jsonl")) {
156 Ok(url) => url,
157 Err(e) => {
158 warn!("Invalid URL for DID: {e}");
159 return Err(DIDWebVHError::InvalidMethodIdentifier(format!(
160 "Couldn't generate a valid URL from the DID: {e}"
161 )));
162 }
163 };
164
165 Self::download_file(client, log_entries_url, max_bytes).await
166 }
167
168 async fn get_witness_proofs(
170 url: WebVHURL,
171 client: Client,
172 max_bytes: u64,
173 ) -> Result<String, DIDWebVHError> {
174 let witness_url = match url.get_http_url(Some("did-witness.json")) {
175 Ok(url) => url,
176 Err(e) => {
177 warn!("Invalid URL for DID: {e}");
178 return Err(DIDWebVHError::InvalidMethodIdentifier(format!(
179 "Couldn't generate a valid URL from the DID: {e}"
180 )));
181 }
182 };
183
184 Self::download_file(client, witness_url, max_bytes).await
185 }
186}
187
188impl DIDWebVHState {
189 pub async fn resolve_file(
194 &mut self,
195 did: &str,
196 log_entries_path: &str,
197 witness_proofs_file: Option<&str>,
198 ) -> Result<(&LogEntry, MetaData), DIDWebVHError> {
199 let _span = span!(Level::DEBUG, "resolve_file", PATH = log_entries_path);
200 async move {
201 let parsed_did_url = WebVHURL::parse_did_url(did)?;
202
203 self.load_log_entries_from_file(log_entries_path)?;
205
206 if let Some(witness_path) = witness_proofs_file {
208 self.load_witness_proofs_from_file(witness_path);
209 } else {
210 self.witness_proofs = WitnessProofCollection::default();
211 }
212
213 self.validated = false;
215 self.expires = DateTime::default();
216
217 self.resolve_state(&parsed_did_url)
218 }
219 .instrument(_span)
220 .await
221 }
222
223 pub async fn resolve_file_owned(
226 &mut self,
227 did: &str,
228 log_entries_path: &str,
229 witness_proofs_file: Option<&str>,
230 ) -> Result<(LogEntry, MetaData), DIDWebVHError> {
231 let (entry, metadata) = self
232 .resolve_file(did, log_entries_path, witness_proofs_file)
233 .await?;
234 Ok((entry.clone(), metadata))
235 }
236
237 pub fn parse_log_entries(raw: &str) -> Result<Vec<LogEntryState>, DIDWebVHError> {
242 let mut log_entries = Vec::new();
243 let mut version = None;
244 for line in raw.lines() {
245 let log_entry = LogEntry::deserialize_string(line, version)?;
246 version = Some(log_entry.get_webvh_version());
247 log_entries.push(LogEntryState {
248 log_entry: log_entry.clone(),
249 version_number: log_entry.get_version_id_fields()?.0,
250 validation_status: LogEntryValidationStatus::NotValidated,
251 validated_parameters: Parameters::default(),
252 });
253 }
254 Ok(log_entries)
255 }
256
257 pub fn needs_witness_proofs(log_entries: &[LogEntryState]) -> bool {
259 log_entries.iter().any(|e| {
260 e.log_entry
261 .get_parameters()
262 .witness
263 .as_ref()
264 .is_some_and(|w| !w.is_empty())
265 })
266 }
267
268 pub fn parse_witness_proofs(raw: &str) -> Result<WitnessProofCollection, DIDWebVHError> {
270 Ok(WitnessProofCollection {
271 proofs: serde_json::from_str(raw).map_err(|e| {
272 DIDWebVHError::WitnessProofError(format!(
273 "Couldn't deserialize Witness Proofs Data: {e}",
274 ))
275 })?,
276 ..Default::default()
277 })
278 }
279
280 fn validate_log_entries(log_entries: &[LogEntryState], did: &str) -> Result<(), DIDWebVHError> {
282 if log_entries.is_empty() {
283 return Err(DIDWebVHError::NotFound(format!(
284 "No LogEntries found for DID: {did}",
285 )));
286 }
287 Ok(())
288 }
289
290 pub async fn resolve_log(
319 &mut self,
320 did: &str,
321 log_entries: &str,
322 witness_proofs: Option<&str>,
323 ) -> Result<(&LogEntry, MetaData), DIDWebVHError> {
324 let _span = span!(Level::DEBUG, "resolve_log", DID = did);
325 async move {
326 let parsed_did_url = WebVHURL::parse_did_url(did)?;
327
328 let parsed_entries = Self::parse_log_entries(log_entries)?;
329 Self::validate_log_entries(&parsed_entries, did)?;
330
331 let witness_collection = if let Some(raw_witnesses) = witness_proofs {
332 Self::parse_witness_proofs(raw_witnesses)?
333 } else {
334 WitnessProofCollection::default()
335 };
336
337 self.log_entries = parsed_entries;
338 self.witness_proofs = witness_collection;
339 self.validated = false;
340 self.expires = DateTime::default();
341
342 self.resolve_state(&parsed_did_url)
343 }
344 .instrument(_span)
345 .await
346 }
347
348 pub async fn resolve_log_owned(
351 &mut self,
352 did: &str,
353 log_entries: &str,
354 witness_proofs: Option<&str>,
355 ) -> Result<(LogEntry, MetaData), DIDWebVHError> {
356 let (entry, metadata) = self.resolve_log(did, log_entries, witness_proofs).await?;
357 Ok((entry.clone(), metadata))
358 }
359}
360
361#[cfg(feature = "network")]
362impl DIDWebVHState {
363 fn resolve_witness_proofs(
366 raw_result: Result<String, DIDWebVHError>,
367 needs_witnesses: bool,
368 ) -> Result<WitnessProofCollection, DIDWebVHError> {
369 match raw_result {
370 Ok(raw) => Self::parse_witness_proofs(&raw),
371 Err(e) => {
372 if needs_witnesses {
373 Err(DIDWebVHError::WitnessProofError(format!(
374 "Witnesses are configured but witness proofs could not be downloaded: {e}"
375 )))
376 } else {
377 Ok(WitnessProofCollection::default())
378 }
379 }
380 }
381 }
382
383 pub async fn resolve(
412 &mut self,
413 did: &str,
414 options: ResolveOptions,
415 ) -> Result<(&LogEntry, MetaData), DIDWebVHError> {
416 let _span = span!(Level::DEBUG, "resolve", DID = did);
417 async move {
418 let parsed_did_url = WebVHURL::parse_did_url(did)?;
419
420 if parsed_did_url.type_ == URLType::WhoIs {
421 return Err(DIDWebVHError::NotImplemented(
422 "Resolving /whois URLs is not yet supported. Use the DID's #whois service endpoint directly.".to_string(),
423 ));
424 }
425
426 if !self.validated || self.expires < Utc::now() {
427 let max_bytes = options.max_response_bytes;
428
429 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
432 let (log_entries, witness_proofs) = {
433 trace!("timeout is not available in WASM builds! {:#?}", options.timeout);
434 let client = reqwest::Client::new();
435
436 let raw_entries =
437 DIDWebVH::get_log_entries(parsed_did_url.clone(), client.clone(), max_bytes).await?;
438 let log_entries = Self::parse_log_entries(&raw_entries)?;
439 Self::validate_log_entries(&log_entries, did)?;
440
441 let needs_witnesses = Self::needs_witness_proofs(&log_entries);
442 let witness_proofs = if options.eager_witness_download || needs_witnesses {
443 let raw_result =
444 DIDWebVH::get_witness_proofs(parsed_did_url.clone(), client.clone(), max_bytes)
445 .await;
446 Self::resolve_witness_proofs(raw_result, needs_witnesses)?
447 } else {
448 WitnessProofCollection::default()
449 };
450
451 (log_entries, witness_proofs)
452 };
453
454 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
456 let (log_entries, witness_proofs) = {
457 let network_timeout = options.timeout.unwrap_or(Duration::from_secs(10));
459
460 let client = reqwest::ClientBuilder::new()
461 .timeout(network_timeout)
462 .redirect(reqwest::redirect::Policy::none())
463 .build()
464 .map_err(|e| DIDWebVHError::NetworkError {
465 url: String::new(),
466 status_code: None,
467 message: format!("Failed to build HTTP client: {e}"),
468 })?;
469
470 if options.eager_witness_download {
471 let r1 = tokio::spawn(DIDWebVH::get_log_entries(
473 parsed_did_url.clone(),
474 client.clone(),
475 max_bytes,
476 ));
477 let r2 = tokio::spawn(DIDWebVH::get_witness_proofs(
478 parsed_did_url.clone(),
479 client.clone(),
480 max_bytes,
481 ));
482
483 let raw_entries = r1.await.map_err(|e| {
484 DIDWebVHError::NetworkError {
485 url: did.to_string(),
486 status_code: None,
487 message: format!("Error downloading LogEntries for DID: {e}"),
488 }
489 })??;
490 let witness_result = match r2.await {
491 Ok(result) => result,
492 Err(_) => Ok("{}".to_string()),
493 };
494
495 let log_entries = Self::parse_log_entries(&raw_entries)?;
496 Self::validate_log_entries(&log_entries, did)?;
497
498 let needs_witnesses = Self::needs_witness_proofs(&log_entries);
499 let witness_proofs =
500 Self::resolve_witness_proofs(witness_result, needs_witnesses)?;
501
502 (log_entries, witness_proofs)
503 } else {
504 let raw_entries = tokio::spawn(DIDWebVH::get_log_entries(
506 parsed_did_url.clone(),
507 client.clone(),
508 max_bytes,
509 ))
510 .await
511 .map_err(|e| DIDWebVHError::NetworkError {
512 url: did.to_string(),
513 status_code: None,
514 message: format!("Error downloading LogEntries for DID: {e}"),
515 })??;
516
517 let log_entries = Self::parse_log_entries(&raw_entries)?;
518 Self::validate_log_entries(&log_entries, did)?;
519
520 let witness_proofs = if Self::needs_witness_proofs(&log_entries) {
521 let raw_result = DIDWebVH::get_witness_proofs(
522 parsed_did_url.clone(),
523 client.clone(),
524 max_bytes,
525 )
526 .await;
527 Self::resolve_witness_proofs(raw_result, true)?
528 } else {
529 WitnessProofCollection::default()
530 };
531
532 (log_entries, witness_proofs)
533 }
534 };
535
536 self.log_entries = log_entries;
538 self.witness_proofs = witness_proofs;
539 self.validated = false;
540 self.expires = DateTime::default();
541 }
542
543 self.resolve_state(&parsed_did_url)
544 }
545 .instrument(_span)
546 .await
547 }
548
549 pub async fn resolve_owned(
552 &mut self,
553 did: &str,
554 options: ResolveOptions,
555 ) -> Result<(LogEntry, MetaData), DIDWebVHError> {
556 let (entry, metadata) = self.resolve(did, options).await?;
557 Ok((entry.clone(), metadata))
558 }
559}
560
561impl DIDWebVHState {
562 fn resolve_state(
563 &mut self,
564 parsed_did_url: &WebVHURL,
565 ) -> Result<(&LogEntry, MetaData), DIDWebVHError> {
566 let _span = span!(Level::DEBUG, "resolve_state").entered();
567 self.validate()?.assert_complete()?;
572
573 let resolved_did = parsed_did_url.to_did_base();
576 let did_matches_any = self.log_entries.iter().any(|entry| {
577 entry
578 .get_state()
579 .get("id")
580 .and_then(|v| v.as_str())
581 .is_some_and(|id| id == resolved_did)
582 });
583 if !did_matches_any {
584 return Err(DIDWebVHError::ValidationError(format!(
585 "DID being resolved ({resolved_did}) does not match the top-level 'id' in any DIDDoc version",
586 )));
587 }
588
589 if let Some(first) = self.log_entries.first() {
591 self.scid = first
592 .get_scid()
593 .ok_or_else(|| {
594 DIDWebVHError::ValidationError("First log entry is missing SCID".to_string())
595 })?
596 .to_string();
597 self.meta_first_ts = first.get_version_time_string();
598 }
599 if let Some(last) = self.log_entries.last() {
600 self.meta_last_ts = last.get_version_time_string();
601 }
602
603 if parsed_did_url.query_version_id.is_some()
605 || parsed_did_url.query_version_time.is_some()
606 || parsed_did_url.query_version_number.is_some()
607 {
608 match self.get_specific_log_entry(
609 parsed_did_url.query_version_id.as_deref(),
610 parsed_did_url.query_version_time,
611 parsed_did_url.query_version_number,
612 ) {
613 Ok(entry) => {
614 let metadata = self.generate_meta_data(entry);
615 Ok((&entry.log_entry, metadata))
616 }
617 Err(e) => Err(DIDWebVHError::NotFound(format!(
618 "Query matched no log entry: {e}"
619 ))),
620 }
621 } else if let Some(last) = self.log_entries.last() {
622 let metadata = self.generate_meta_data(last);
623 Ok((&last.log_entry, metadata))
624 } else {
625 Err(DIDWebVHError::NotFound(
626 "No LogEntries found after validation".to_string(),
627 ))
628 }
629 }
630}
631
632#[cfg(all(test, feature = "network"))]
633mod tests {
634 use super::ResolveOptions;
635 use crate::{DIDWebVHError, DIDWebVHState};
636
637 use wiremock::{
643 Mock, MockServer, ResponseTemplate,
644 matchers::{any, path},
645 };
646
647 async fn setup_mock_resolve() -> (MockServer, String) {
650 use crate::test_utils::{did_doc_with_key, key_and_params};
651
652 let server = MockServer::start().await;
653 let port = server.address().port();
654
655 let (key, params) = key_and_params();
656 let did_template = format!("did:webvh:{{SCID}}:localhost%3A{port}");
657 let doc = did_doc_with_key(&did_template, &key);
658
659 let mut state = DIDWebVHState::default();
660 state
661 .create_log_entry(None, &doc, ¶ms, &key)
662 .await
663 .expect("Failed to create log entry");
664
665 let log_entry = &state.log_entries[0].log_entry;
666 let jsonl = serde_json::to_string(log_entry).unwrap();
667 let scid = state.scid();
668 let did = format!("did:webvh:{scid}:localhost%3A{port}");
669
670 Mock::given(path("/.well-known/did.jsonl"))
671 .respond_with(ResponseTemplate::new(200).set_body_string(&jsonl))
672 .mount(&server)
673 .await;
674
675 (server, did)
676 }
677
678 #[tokio::test]
680 async fn resolve_mock() {
681 let (_server, did) = setup_mock_resolve().await;
682
683 let mut webvh = DIDWebVHState::default();
684 let result = webvh.resolve(&did, ResolveOptions::default()).await;
685 assert!(result.is_ok(), "resolve failed: {result:?}");
686 }
687
688 #[tokio::test]
690 async fn resolve_mock_eager() {
691 let (server, did) = setup_mock_resolve().await;
692
693 Mock::given(path("/.well-known/did-witness.json"))
695 .respond_with(ResponseTemplate::new(404))
696 .mount(&server)
697 .await;
698
699 let mut webvh = DIDWebVHState::default();
700 let result = webvh
701 .resolve(
702 &did,
703 ResolveOptions {
704 eager_witness_download: true,
705 ..ResolveOptions::default()
706 },
707 )
708 .await;
709 assert!(result.is_ok(), "eager resolve failed: {result:?}");
710 }
711
712 #[tokio::test]
714 async fn resolve_mock_specific_version() {
715 use crate::log_entry::LogEntryMethods;
716
717 let (_server, did) = setup_mock_resolve().await;
718
719 let mut webvh = DIDWebVHState::default();
721 let (entry, _) = webvh
722 .resolve(&did, ResolveOptions::default())
723 .await
724 .unwrap();
725 let version_id = entry.get_version_id().to_string();
726
727 let mut webvh2 = DIDWebVHState::default();
729 let did_with_version = format!("{did}?versionId={version_id}");
730 let result = webvh2
731 .resolve(&did_with_version, ResolveOptions::default())
732 .await;
733 assert!(result.is_ok(), "versionId resolve failed: {result:?}");
734 }
735
736 #[tokio::test]
738 async fn resolve_mock_specific_time() {
739 use crate::log_entry::LogEntryMethods;
740
741 let (_server, did) = setup_mock_resolve().await;
742
743 let mut webvh = DIDWebVHState::default();
745 let (entry, _) = webvh
746 .resolve(&did, ResolveOptions::default())
747 .await
748 .unwrap();
749 let version_time = entry.get_version_time_string();
750
751 let mut webvh2 = DIDWebVHState::default();
753 let did_with_time = format!("{did}?versionTime={version_time}");
754 let result = webvh2
755 .resolve(&did_with_time, ResolveOptions::default())
756 .await;
757 assert!(result.is_ok(), "versionTime resolve failed: {result:?}");
758 }
759
760 fn mock_did(server: &wiremock::MockServer, scid: &str) -> String {
768 let port = server.address().port();
769 format!("did:webvh:{scid}:localhost%3A{port}")
770 }
771
772 #[tokio::test]
775 async fn resolve_http_404() {
776 let server = MockServer::start().await;
777 Mock::given(any())
778 .respond_with(ResponseTemplate::new(404))
779 .mount(&server)
780 .await;
781
782 let did = mock_did(&server, "testscid404");
783 let mut webvh = DIDWebVHState::default();
784 let result = webvh.resolve(&did, ResolveOptions::default()).await;
785
786 match result {
787 Err(DIDWebVHError::NetworkError {
788 status_code: Some(404),
789 ..
790 }) => {} other => panic!("Expected NetworkError with status 404, got: {other:?}"),
792 }
793 }
794
795 #[tokio::test]
798 async fn resolve_http_500() {
799 let server = MockServer::start().await;
800 Mock::given(any())
801 .respond_with(ResponseTemplate::new(500))
802 .mount(&server)
803 .await;
804
805 let did = mock_did(&server, "testscid500");
806 let mut webvh = DIDWebVHState::default();
807 let result = webvh.resolve(&did, ResolveOptions::default()).await;
808
809 match result {
810 Err(DIDWebVHError::NetworkError {
811 status_code: Some(500),
812 ..
813 }) => {}
814 other => panic!("Expected NetworkError with status 500, got: {other:?}"),
815 }
816 }
817
818 #[tokio::test]
821 async fn resolve_malformed_response() {
822 let server = MockServer::start().await;
823 Mock::given(any())
824 .respond_with(ResponseTemplate::new(200).set_body_string("this is not jsonl"))
825 .mount(&server)
826 .await;
827
828 let did = mock_did(&server, "testscidbad");
829 let mut webvh = DIDWebVHState::default();
830 let result = webvh.resolve(&did, ResolveOptions::default()).await;
831
832 match result {
833 Err(DIDWebVHError::LogEntryError(_)) => {} other => panic!("Expected LogEntryError for malformed response body, got: {other:?}"),
835 }
836 }
837
838 #[tokio::test]
841 async fn resolve_empty_response() {
842 let server = MockServer::start().await;
843 Mock::given(any())
844 .respond_with(ResponseTemplate::new(200).set_body_string(""))
845 .mount(&server)
846 .await;
847
848 let did = mock_did(&server, "testscidempty");
849 let mut webvh = DIDWebVHState::default();
850 let result = webvh.resolve(&did, ResolveOptions::default()).await;
851
852 match result {
853 Err(DIDWebVHError::NotFound(msg)) => {
854 assert!(
855 msg.contains("No LogEntries"),
856 "Expected 'No LogEntries' message, got: {msg}"
857 );
858 }
859 other => panic!("Expected NotFound error, got: {other:?}"),
860 }
861 }
862
863 #[tokio::test]
865 async fn resolve_timeout() {
866 use std::time::Duration;
867 let server = MockServer::start().await;
868 Mock::given(any())
870 .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(5)))
871 .mount(&server)
872 .await;
873
874 let did = mock_did(&server, "testscidtimeout");
875 let mut webvh = DIDWebVHState::default();
876 let result = webvh
877 .resolve(
878 &did,
879 ResolveOptions {
880 timeout: Some(Duration::from_secs(1)),
881 ..ResolveOptions::default()
882 },
883 )
884 .await;
885
886 match result {
887 Err(DIDWebVHError::NetworkError {
888 status_code: None, ..
889 }) => {} other => panic!("Expected NetworkError with no status_code (timeout), got: {other:?}"),
891 }
892 }
893
894 #[tokio::test]
897 async fn resolve_connection_refused() {
898 let did = "did:webvh:testscidrefused:localhost%3A1";
900 let mut webvh = DIDWebVHState::default();
901 let result = webvh
902 .resolve(
903 did,
904 ResolveOptions {
905 timeout: Some(std::time::Duration::from_secs(2)),
906 ..ResolveOptions::default()
907 },
908 )
909 .await;
910
911 match result {
912 Err(DIDWebVHError::NetworkError {
913 status_code: None, ..
914 }) => {}
915 other => panic!(
916 "Expected NetworkError with no status_code (connection refused), got: {other:?}"
917 ),
918 }
919 }
920
921 #[tokio::test]
924 async fn resolve_network_error_fields() {
925 let server = MockServer::start().await;
926 Mock::given(any())
927 .respond_with(ResponseTemplate::new(503))
928 .mount(&server)
929 .await;
930
931 let did = mock_did(&server, "testscidfields");
932 let mut webvh = DIDWebVHState::default();
933 let result = webvh.resolve(&did, ResolveOptions::default()).await;
934
935 match result {
936 Err(DIDWebVHError::NetworkError {
937 ref url,
938 status_code,
939 ref message,
940 }) => {
941 assert!(
942 url.contains("localhost"),
943 "url should contain localhost: {url}"
944 );
945 assert_eq!(status_code, Some(503));
946 assert!(
947 message.contains("503"),
948 "message should contain status: {message}"
949 );
950 }
951 other => panic!("Expected structured NetworkError, got: {other:?}"),
952 }
953 }
954
955 async fn setup_resolve_log_data() -> (String, String) {
959 use crate::test_utils::{did_doc_with_key, key_and_params};
960
961 let server = MockServer::start().await;
962 let port = server.address().port();
963
964 let (key, params) = key_and_params();
965 let did_template = format!("did:webvh:{{SCID}}:localhost%3A{port}");
966 let doc = did_doc_with_key(&did_template, &key);
967
968 let mut state = DIDWebVHState::default();
969 state
970 .create_log_entry(None, &doc, ¶ms, &key)
971 .await
972 .expect("Failed to create log entry");
973
974 let log_entry = &state.log_entries[0].log_entry;
975 let jsonl = serde_json::to_string(log_entry).unwrap();
976 let scid = state.scid();
977 let did = format!("did:webvh:{scid}:localhost%3A{port}");
978
979 (did, jsonl)
980 }
981
982 #[tokio::test]
984 async fn resolve_log_from_str() {
985 let (did, jsonl) = setup_resolve_log_data().await;
986
987 let mut webvh = DIDWebVHState::default();
988 let result = webvh.resolve_log(&did, &jsonl, None).await;
989 assert!(result.is_ok(), "resolve_log failed: {result:?}");
990 }
991
992 #[tokio::test]
995 async fn resolve_log_matches_network_resolve() {
996 use crate::log_entry::LogEntryMethods;
997
998 let (server, did) = setup_mock_resolve().await;
999 Mock::given(path("/.well-known/did-witness.json"))
1001 .respond_with(ResponseTemplate::new(404))
1002 .mount(&server)
1003 .await;
1004
1005 let mut webvh_net = DIDWebVHState::default();
1007 let (net_entry, _) = webvh_net
1008 .resolve(&did, ResolveOptions::default())
1009 .await
1010 .unwrap();
1011 let net_doc = net_entry.get_did_document().unwrap();
1012
1013 let log_entry = &webvh_net.log_entries()[0].log_entry;
1015 let jsonl = serde_json::to_string(log_entry).unwrap();
1016
1017 let mut webvh_log = DIDWebVHState::default();
1019 let (log_entry, _) = webvh_log.resolve_log(&did, &jsonl, None).await.unwrap();
1020 let log_doc = log_entry.get_did_document().unwrap();
1021
1022 assert_eq!(
1023 net_doc, log_doc,
1024 "Documents from network and log resolution should match"
1025 );
1026 }
1027
1028 #[tokio::test]
1030 async fn resolve_log_owned_works() {
1031 let (did, jsonl) = setup_resolve_log_data().await;
1032
1033 let mut webvh = DIDWebVHState::default();
1034 let result = webvh.resolve_log_owned(&did, &jsonl, None).await;
1035 assert!(result.is_ok(), "resolve_log_owned failed: {result:?}");
1036 }
1037
1038 #[tokio::test]
1040 async fn resolve_log_empty_log() {
1041 let mut webvh = DIDWebVHState::default();
1042 let result = webvh
1043 .resolve_log("did:webvh:testscid:example.com", "", None)
1044 .await;
1045
1046 match result {
1047 Err(DIDWebVHError::NotFound(msg)) => {
1048 assert!(
1049 msg.contains("No LogEntries"),
1050 "Expected 'No LogEntries' message, got: {msg}"
1051 );
1052 }
1053 other => panic!("Expected NotFound error, got: {other:?}"),
1054 }
1055 }
1056
1057 #[tokio::test]
1059 async fn resolve_log_malformed_jsonl() {
1060 let mut webvh = DIDWebVHState::default();
1061 let result = webvh
1062 .resolve_log("did:webvh:testscid:example.com", "not valid json", None)
1063 .await;
1064
1065 match result {
1066 Err(DIDWebVHError::LogEntryError(_)) => {} other => panic!("Expected LogEntryError, got: {other:?}"),
1068 }
1069 }
1070
1071 #[tokio::test]
1073 async fn resolve_log_tampered_data_fails() {
1074 let (did, jsonl) = setup_resolve_log_data().await;
1075
1076 let tampered = jsonl.replacen("z", "y", 1);
1079 if tampered == jsonl {
1080 return;
1082 }
1083
1084 let mut webvh = DIDWebVHState::default();
1085 let result = webvh.resolve_log(&did, &tampered, None).await;
1086 assert!(
1087 result.is_err(),
1088 "resolve_log should fail with tampered data"
1089 );
1090 }
1091
1092 #[tokio::test]
1096 async fn resolve_rejects_large_response_body() {
1097 let server = MockServer::start().await;
1098 let large_body = "x".repeat(210 * 1024);
1100 Mock::given(any())
1101 .respond_with(ResponseTemplate::new(200).set_body_string(&large_body))
1102 .mount(&server)
1103 .await;
1104
1105 let did = mock_did(&server, "testscidlarge");
1106 let mut webvh = DIDWebVHState::default();
1107 let result = webvh.resolve(&did, ResolveOptions::default()).await;
1108
1109 match result {
1110 Err(DIDWebVHError::ResponseTooLarge { max_bytes, .. }) => {
1111 assert_eq!(max_bytes, super::DEFAULT_MAX_RESPONSE_BYTES);
1112 }
1113 other => panic!("Expected ResponseTooLarge, got: {other:?}"),
1114 }
1115 }
1116
1117 #[tokio::test]
1119 async fn resolve_custom_size_limit() {
1120 let (_server, did) = setup_mock_resolve().await;
1121
1122 let mut webvh = DIDWebVHState::default();
1124 let result = webvh
1125 .resolve(
1126 &did,
1127 ResolveOptions {
1128 max_response_bytes: 10, ..ResolveOptions::default()
1130 },
1131 )
1132 .await;
1133
1134 match result {
1135 Err(DIDWebVHError::ResponseTooLarge { max_bytes, .. }) => {
1136 assert_eq!(max_bytes, 10);
1137 }
1138 other => panic!("Expected ResponseTooLarge, got: {other:?}"),
1139 }
1140 }
1141
1142 #[tokio::test]
1144 async fn resolve_normal_response_passes_size_check() {
1145 let (_server, did) = setup_mock_resolve().await;
1146
1147 let mut webvh = DIDWebVHState::default();
1148 let result = webvh.resolve(&did, ResolveOptions::default()).await;
1149 assert!(
1150 result.is_ok(),
1151 "Normal response should pass size check: {result:?}"
1152 );
1153 }
1154}