1use anyhow::Result;
12use rustenium_bidi_definitions::network::events::{
13 BeforeRequestSent, FetchError, ResponseCompleted,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::{broadcast, RwLock};
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct CapturedHeader {
26 pub name: String,
27 pub value: String,
28}
29
30impl From<&rustenium_bidi_definitions::network::types::Header> for CapturedHeader {
31 fn from(h: &rustenium_bidi_definitions::network::types::Header) -> Self {
32 let value = match &h.value {
33 rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
34 s.value.clone()
35 }
36 rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
37 b.value.clone()
38 }
39 };
40 Self {
41 name: h.name.clone(),
42 value,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
48pub struct CapturedTiming {
49 pub dns_start_ms: Option<f64>,
50 pub dns_end_ms: Option<f64>,
51 pub connect_start_ms: Option<f64>,
52 pub connect_end_ms: Option<f64>,
53 pub tls_start_ms: Option<f64>,
54 pub response_start_ms: Option<f64>,
55 pub response_end_ms: Option<f64>,
56}
57
58impl From<&rustenium_bidi_definitions::network::types::FetchTimingInfo> for CapturedTiming {
59 fn from(t: &rustenium_bidi_definitions::network::types::FetchTimingInfo) -> Self {
60 let origin = t.request_time;
61 Self {
62 dns_start_ms: non_neg(t.dns_start - origin),
63 dns_end_ms: non_neg(t.dns_end - origin),
64 connect_start_ms: non_neg(t.connect_start - origin),
65 connect_end_ms: non_neg(t.connect_end - origin),
66 tls_start_ms: non_neg(t.tls_start - origin),
67 response_start_ms: non_neg(t.response_start - origin),
68 response_end_ms: non_neg(t.response_end - origin),
69 }
70 }
71}
72
73fn non_neg(v: f64) -> Option<f64> {
74 if v >= 0.0 {
75 Some(v)
76 } else {
77 None
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct CapturedCookie {
83 pub name: String,
84 pub value: String,
85 pub domain: String,
86 pub path: String,
87 pub size: u64,
88 pub http_only: bool,
89 pub secure: bool,
90 pub same_site: String,
91}
92
93impl From<&rustenium_bidi_definitions::network::types::Cookie> for CapturedCookie {
94 fn from(c: &rustenium_bidi_definitions::network::types::Cookie) -> Self {
95 let value = match &c.value {
96 rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
97 s.value.clone()
98 }
99 rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
100 b.value.clone()
101 }
102 };
103 Self {
104 name: c.name.clone(),
105 value,
106 domain: c.domain.clone(),
107 path: c.path.clone(),
108 size: c.size,
109 http_only: c.http_only,
110 secure: c.secure,
111 same_site: format!("{:?}", c.same_site).to_lowercase(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct CapturedRequest {
118 pub id: String,
119 pub context: Option<String>,
120 pub method: String,
121 pub url: String,
122 pub headers: Vec<CapturedHeader>,
123 pub post_data: Option<String>,
124 pub timestamp: u64,
125 pub destination: String,
126 pub initiator_type: Option<String>,
127 pub timing: CapturedTiming,
128 pub cookies: Vec<CapturedCookie>,
129}
130
131impl CapturedRequest {
132 pub fn json_body(&self) -> Option<serde_json::Value> {
134 self.post_data
135 .as_ref()
136 .and_then(|s| serde_json::from_str(s).ok())
137 }
138
139 pub fn query_params(&self) -> Result<Vec<(String, String)>, url::ParseError> {
144 let u = url::Url::parse(&self.url)?;
145 Ok(u.query_pairs()
146 .map(|(k, v)| (k.to_string(), v.to_string()))
147 .collect())
148 }
149
150 pub fn request_header(&self, name: &str) -> Option<&str> {
152 let name_lower = name.to_lowercase();
153 self.headers
154 .iter()
155 .find(|h| h.name.to_lowercase() == name_lower)
156 .map(|h| h.value.as_str())
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct CapturedResponse {
170 pub id: String,
171 pub url: String,
172 pub protocol: String,
173 pub status: u16,
174 pub status_text: String,
175 pub headers: Vec<CapturedHeader>,
176 pub mime_type: String,
177 pub body_size: Option<u64>,
181 pub from_cache: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct CapturedError {
186 pub id: String,
187 pub url: String,
188 pub error_text: String,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct NetworkEntry {
193 pub request: CapturedRequest,
194 pub response: Option<CapturedResponse>,
195 pub error: Option<CapturedError>,
196}
197
198impl NetworkEntry {
199 pub fn has_response(&self) -> bool {
200 self.response.is_some()
201 }
202
203 pub fn is_error(&self) -> bool {
204 self.error.is_some()
205 }
206
207 pub fn final_url(&self) -> &str {
208 self.response
209 .as_ref()
210 .map(|r| r.url.as_str())
211 .unwrap_or(&self.request.url)
212 }
213
214 pub fn status(&self) -> Option<u16> {
215 self.response.as_ref().map(|r| r.status)
216 }
217
218 pub fn response_header(&self, name: &str) -> Option<&str> {
219 let name_lower = name.to_lowercase();
220 self.response.as_ref().and_then(|r| {
221 r.headers
222 .iter()
223 .find(|h| h.name.to_lowercase() == name_lower)
224 .map(|h| h.value.as_str())
225 })
226 }
227
228 pub fn request_header(&self, name: &str) -> Option<&str> {
229 self.request.request_header(name)
230 }
231
232 pub fn to_curl(&self) -> String {
234 fn shell_quote(s: &str) -> String {
235 s.replace('\'', "'\\''")
237 }
238 let req = &self.request;
239 let mut parts = vec![format!("curl -X '{}'", shell_quote(&req.method))];
244 for h in &req.headers {
245 if h.name.eq_ignore_ascii_case("host")
246 || h.name.eq_ignore_ascii_case("connection")
247 || h.name.eq_ignore_ascii_case("accept-encoding")
248 {
249 continue;
250 }
251 parts.push(format!(
252 "-H '{}: {}'",
253 shell_quote(&h.name),
254 shell_quote(&h.value)
255 ));
256 }
257 if let Some(ref body) = req.post_data {
258 parts.push(format!("-d '{}'", shell_quote(body)));
259 }
260 parts.push(format!("'{}'", shell_quote(&req.url)));
261 parts.join(" ")
262 }
263}
264
265#[derive(Debug, Clone, Default)]
270pub struct Filter {
271 method: Option<String>,
272 status_range: Option<std::ops::RangeInclusive<u16>>,
273 url_substring: Option<String>,
274 url_regex: Option<regex::Regex>,
275 header_name: Option<String>,
276 header_value_substring: Option<String>,
277 destination: Option<String>,
278 has_response: Option<bool>,
279 has_error: Option<bool>,
280}
281
282impl Filter {
283 pub fn new() -> Self {
284 Self::default()
285 }
286
287 pub fn method(mut self, m: impl Into<String>) -> Self {
288 self.method = Some(m.into().to_uppercase());
289 self
290 }
291
292 pub fn status_range(mut self, r: std::ops::RangeInclusive<u16>) -> Self {
293 self.status_range = Some(r);
294 self
295 }
296
297 pub fn url_contains(mut self, needle: impl Into<String>) -> Self {
298 self.url_substring = Some(needle.into().to_lowercase());
299 self
300 }
301
302 pub fn url_regex(mut self, pattern: &str) -> Result<Self> {
303 self.url_regex = Some(regex::Regex::new(pattern)?);
304 Ok(self)
305 }
306
307 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
308 self.header_name = Some(name.into().to_lowercase());
309 self.header_value_substring = Some(value.into().to_lowercase());
310 self
311 }
312
313 pub fn header_name(mut self, name: impl Into<String>) -> Self {
314 self.header_name = Some(name.into().to_lowercase());
315 self
316 }
317
318 pub fn header_value(mut self, value: impl Into<String>) -> Self {
319 self.header_value_substring = Some(value.into().to_lowercase());
320 self
321 }
322
323 pub fn destination(mut self, d: impl Into<String>) -> Self {
324 self.destination = Some(d.into().to_lowercase());
325 self
326 }
327
328 pub fn with_response(mut self) -> Self {
329 self.has_response = Some(true);
330 self
331 }
332
333 pub fn without_response(mut self) -> Self {
334 self.has_response = Some(false);
335 self
336 }
337
338 pub fn with_error(mut self) -> Self {
339 self.has_error = Some(true);
340 self
341 }
342
343 fn matches(&self, e: &NetworkEntry) -> bool {
344 if let Some(m) = &self.method {
345 if e.request.method.to_uppercase() != *m {
346 return false;
347 }
348 }
349 if let Some(r) = &self.status_range {
350 let Some(st) = e.status() else {
353 return false;
354 };
355 if !r.contains(&st) {
356 return false;
357 }
358 }
359 if let Some(needle) = &self.url_substring {
360 if !e.request.url.to_lowercase().contains(needle) {
361 return false;
362 }
363 }
364 if let Some(re) = &self.url_regex {
365 if !re.is_match(&e.request.url) {
366 return false;
367 }
368 }
369 if let (Some(name), Some(value)) = (&self.header_name, &self.header_value_substring) {
370 let found = e
371 .request
372 .headers
373 .iter()
374 .chain(
375 e.response
376 .as_ref()
377 .map(|r| r.headers.as_slice())
378 .unwrap_or(&[]),
379 )
380 .any(|h| {
381 h.name.to_lowercase().contains(name) && h.value.to_lowercase().contains(value)
382 });
383 if !found {
384 return false;
385 }
386 } else if let Some(name) = &self.header_name {
387 let found = e
388 .request
389 .headers
390 .iter()
391 .chain(
392 e.response
393 .as_ref()
394 .map(|r| r.headers.as_slice())
395 .unwrap_or(&[]),
396 )
397 .any(|h| h.name.to_lowercase().contains(name));
398 if !found {
399 return false;
400 }
401 } else if let Some(value) = &self.header_value_substring {
402 let found = e
403 .request
404 .headers
405 .iter()
406 .chain(
407 e.response
408 .as_ref()
409 .map(|r| r.headers.as_slice())
410 .unwrap_or(&[]),
411 )
412 .any(|h| h.value.to_lowercase().contains(value));
413 if !found {
414 return false;
415 }
416 }
417 if let Some(d) = &self.destination {
418 if e.request.destination.to_lowercase() != *d {
419 return false;
420 }
421 }
422 if let Some(want) = self.has_response {
423 if e.has_response() != want {
424 return false;
425 }
426 }
427 if let Some(want) = self.has_error {
428 if e.is_error() != want {
429 return false;
430 }
431 }
432 true
433 }
434}
435
436#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
441pub struct NetworkMetrics {
442 pub requests_received: u64,
443 pub responses_received: u64,
444 pub errors_received: u64,
445 pub entries_evicted: u64,
446 pub pending_responses_dropped: u64,
447 pub pending_errors_dropped: u64,
448 pub broadcast_drops: u64,
449 pub duplicate_responses: u64,
450 pub duplicate_errors: u64,
451 pub max_entries: usize,
452}
453
454#[derive(Debug)]
459struct Inner {
460 entries: Vec<Arc<NetworkEntry>>,
461 by_id: HashMap<String, usize>,
462 pending_responses: HashMap<String, CapturedResponse>,
463 pending_errors: HashMap<String, CapturedError>,
464 max_entries: usize,
465 max_pending: usize,
466 tx: broadcast::Sender<Arc<NetworkEntry>>,
467 metrics: NetworkMetrics,
468 eviction_warned: bool,
470}
471
472impl Inner {
473 fn new(
474 max_entries: usize,
475 max_pending: usize,
476 tx: broadcast::Sender<Arc<NetworkEntry>>,
477 ) -> Self {
478 let metrics = NetworkMetrics {
479 max_entries,
480 ..NetworkMetrics::default()
481 };
482 Self {
483 entries: if max_entries > 0 {
484 Vec::with_capacity(max_entries)
485 } else {
486 Vec::new()
487 },
488 by_id: HashMap::new(),
489 pending_responses: HashMap::new(),
490 pending_errors: HashMap::new(),
491 max_entries,
492 max_pending,
493 tx,
494 metrics,
495 eviction_warned: false,
496 }
497 }
498
499 fn push_entry(&mut self, entry: Arc<NetworkEntry>) {
500 if self.by_id.contains_key(&entry.request.id) {
502 return;
503 }
504
505 if self.max_entries > 0 && self.entries.len() >= self.max_entries {
507 let remove = self.entries.len().saturating_sub(self.max_entries - 1);
508 let removed = self.entries.drain(..remove).len();
509 self.metrics.entries_evicted += removed as u64;
510 self.by_id.clear();
511 for (i, e) in self.entries.iter().enumerate() {
512 self.by_id.insert(e.request.id.clone(), i);
513 }
514 if !self.eviction_warned {
515 self.eviction_warned = true;
516 tracing::warn!(
517 removed,
518 max_entries = self.max_entries,
519 "[foxdriver] network capture entry evicted; oldest requests are being dropped"
520 );
521 }
522 }
523
524 let idx = self.entries.len();
525 self.by_id.insert(entry.request.id.clone(), idx);
526 self.entries.push(entry.clone());
527
528 if self.tx.send(entry).is_err() {
530 self.metrics.broadcast_drops += 1;
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
540pub struct NetworkLog {
541 inner: Arc<RwLock<Inner>>,
542}
543
544impl NetworkLog {
545 pub fn new() -> Self {
546 Self::with_limits(50_000, 10_000)
547 }
548
549 pub fn with_limits(max_entries: usize, max_pending: usize) -> Self {
550 let (tx, _) = broadcast::channel(1024);
551 Self {
552 inner: Arc::new(RwLock::new(Inner::new(max_entries, max_pending, tx))),
553 }
554 }
555
556 pub async fn subscribe(&self) -> broadcast::Receiver<Arc<NetworkEntry>> {
557 self.inner.read().await.tx.subscribe()
558 }
559
560 pub async fn entries(&self) -> Vec<Arc<NetworkEntry>> {
561 let inner = self.inner.read().await;
562 inner.entries.clone()
563 }
564
565 pub async fn completed(&self) -> Vec<Arc<NetworkEntry>> {
566 let inner = self.inner.read().await;
567 inner
568 .entries
569 .iter()
570 .filter(|e| e.has_response() || e.is_error())
571 .cloned()
572 .collect()
573 }
574
575 pub async fn count(&self, filter: Filter) -> usize {
576 let inner = self.inner.read().await;
577 inner.entries.iter().filter(|e| filter.matches(e)).count()
578 }
579
580 pub async fn filter(&self, f: Filter) -> Vec<Arc<NetworkEntry>> {
581 let inner = self.inner.read().await;
582 inner
583 .entries
584 .iter()
585 .filter(|e| f.matches(e))
586 .cloned()
587 .collect()
588 }
589
590 pub async fn first(&self) -> Option<Arc<NetworkEntry>> {
591 let inner = self.inner.read().await;
592 inner.entries.first().cloned()
593 }
594
595 pub async fn last(&self) -> Option<Arc<NetworkEntry>> {
596 let inner = self.inner.read().await;
597 inner.entries.last().cloned()
598 }
599
600 pub async fn nth(&self, n: usize) -> Option<Arc<NetworkEntry>> {
601 let inner = self.inner.read().await;
602 inner.entries.get(n).cloned()
603 }
604
605 pub async fn find_by_url(&self, substring: &str) -> Option<Arc<NetworkEntry>> {
606 let inner = self.inner.read().await;
607 inner
608 .entries
609 .iter()
610 .find(|e| e.request.url.contains(substring))
611 .cloned()
612 }
613
614 pub async fn find_by_url_regex(&self, re: ®ex::Regex) -> Option<Arc<NetworkEntry>> {
615 let inner = self.inner.read().await;
616 inner
617 .entries
618 .iter()
619 .find(|e| re.is_match(&e.request.url))
620 .cloned()
621 }
622
623 pub async fn endpoints(&self) -> Vec<String> {
624 let inner = self.inner.read().await;
625 let mut seen = std::collections::HashSet::new();
626 inner
627 .entries
628 .iter()
629 .filter(|e| seen.insert(e.request.url.clone()))
630 .map(|e| e.request.url.clone())
631 .collect()
632 }
633
634 pub async fn hostnames(&self) -> Vec<String> {
635 let inner = self.inner.read().await;
636 let mut seen = std::collections::HashSet::new();
637 inner
638 .entries
639 .iter()
640 .filter_map(|e| {
641 url::Url::parse(&e.request.url).ok().and_then(|u| {
642 let host = u.host_str()?.to_string();
643 seen.insert(host.clone()).then_some(host)
644 })
645 })
646 .collect()
647 }
648
649 pub async fn distinct_methods(&self) -> Vec<String> {
650 let inner = self.inner.read().await;
651 let mut seen = std::collections::HashSet::new();
652 let mut out: Vec<String> = inner
653 .entries
654 .iter()
655 .filter_map(|e| {
656 seen.insert(e.request.method.clone())
657 .then_some(e.request.method.clone())
658 })
659 .collect();
660 out.sort_unstable();
661 out.dedup();
662 out
663 }
664
665 pub async fn distinct_statuses(&self) -> Vec<u16> {
666 let inner = self.inner.read().await;
667 let mut seen = std::collections::HashSet::new();
668 let mut out: Vec<u16> = inner
669 .entries
670 .iter()
671 .filter_map(|e| {
672 let status = e.status()?;
673 seen.insert(status).then_some(status)
674 })
675 .collect();
676 out.sort_unstable();
677 out
678 }
679
680 pub async fn total_bytes_in(&self) -> u64 {
681 let inner = self.inner.read().await;
682 inner
683 .entries
684 .iter()
685 .filter_map(|e| e.response.as_ref().and_then(|r| r.body_size))
686 .sum()
687 }
688
689 pub async fn total_bytes_out(&self) -> u64 {
690 let inner = self.inner.read().await;
691 inner
692 .entries
693 .iter()
694 .map(|e| {
695 e.request
696 .post_data
697 .as_ref()
698 .map(|b| b.len() as u64)
699 .unwrap_or(0)
700 })
701 .sum()
702 }
703
704 pub async fn len(&self) -> usize {
705 self.inner.read().await.entries.len()
706 }
707
708 pub async fn is_empty(&self) -> bool {
709 self.len().await == 0
710 }
711
712 pub async fn clear(&self) {
713 let mut inner = self.inner.write().await;
714 inner.entries.clear();
715 inner.by_id.clear();
716 }
717
718 pub async fn metrics(&self) -> NetworkMetrics {
719 self.inner.read().await.metrics.clone()
720 }
721
722 pub async fn contains_id(&self, id: &str) -> bool {
723 self.inner.read().await.by_id.contains_key(id)
724 }
725
726 pub async fn remove_by_id(&self, id: &str) -> Option<Arc<NetworkEntry>> {
727 let mut inner = self.inner.write().await;
728 let idx = inner.by_id.remove(id)?;
729 let entry = inner.entries.remove(idx);
730 let updates: Vec<(String, usize)> = inner
732 .entries
733 .iter()
734 .enumerate()
735 .skip(idx)
736 .map(|(i, e)| (e.request.id.clone(), i))
737 .collect();
738 for (id, i) in updates {
739 inner.by_id.insert(id, i);
740 }
741 Some(entry)
742 }
743
744 pub async fn request_ids(&self) -> Vec<String> {
746 let inner = self.inner.read().await;
747 inner.entries.iter().map(|e| e.request.id.clone()).collect()
748 }
749
750 pub async fn memory_estimate(&self) -> usize {
753 let inner = self.inner.read().await;
754 let mut estimate = 0;
755 estimate += inner.entries.capacity() * std::mem::size_of::<Arc<NetworkEntry>>();
756 estimate +=
757 inner.by_id.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<usize>());
758 estimate += inner.pending_responses.capacity()
759 * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedResponse>());
760 estimate += inner.pending_errors.capacity()
761 * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedError>());
762 for e in &inner.entries {
763 estimate += e.request.url.len();
764 estimate += e.request.method.len();
765 estimate += e
766 .request
767 .headers
768 .iter()
769 .map(|h| h.name.len() + h.value.len())
770 .sum::<usize>();
771 if let Some(ref body) = e.request.post_data {
772 estimate += body.len();
773 }
774 if let Some(ref r) = e.response {
775 estimate +=
776 r.url.len() + r.status_text.len() + r.mime_type.len() + r.protocol.len();
777 estimate += r
778 .headers
779 .iter()
780 .map(|h| h.name.len() + h.value.len())
781 .sum::<usize>();
782 }
783 if let Some(ref err) = e.error {
784 estimate += err.url.len() + err.error_text.len();
785 }
786 }
787 estimate
788 }
789
790 pub async fn has_response(&self, id: &str) -> bool {
792 let inner = self.inner.read().await;
793 inner
794 .by_id
795 .get(id)
796 .map(|&idx| inner.entries[idx].response.is_some())
797 .unwrap_or(false)
798 }
799
800 pub async fn has_error(&self, id: &str) -> bool {
802 let inner = self.inner.read().await;
803 inner
804 .by_id
805 .get(id)
806 .map(|&idx| inner.entries[idx].error.is_some())
807 .unwrap_or(false)
808 }
809
810 pub async fn pending_count(&self) -> usize {
813 let inner = self.inner.read().await;
814 inner.pending_responses.len() + inner.pending_errors.len()
815 }
816
817 pub async fn drain_pending(&self) -> (Vec<CapturedResponse>, Vec<CapturedError>) {
821 let mut inner = self.inner.write().await;
822 let responses: Vec<CapturedResponse> =
823 inner.pending_responses.drain().map(|(_, v)| v).collect();
824 let errors: Vec<CapturedError> = inner.pending_errors.drain().map(|(_, v)| v).collect();
825 (responses, errors)
826 }
827
828 pub async fn retain<F>(&self, mut f: F)
831 where
832 F: FnMut(&NetworkEntry) -> bool,
833 {
834 let mut inner = self.inner.write().await;
835 let mut new_entries = Vec::new();
836 let mut new_by_id = HashMap::new();
837 for entry in inner.entries.drain(..) {
838 if f(&entry) {
839 let idx = new_entries.len();
840 new_by_id.insert(entry.request.id.clone(), idx);
841 new_entries.push(entry);
842 }
843 }
844 inner.entries = new_entries;
845 inner.by_id = new_by_id;
846 }
847
848 pub async fn wait_for_url(
850 &self,
851 substring: &str,
852 timeout: std::time::Duration,
853 ) -> Option<Arc<NetworkEntry>> {
854 let mut rx = self.subscribe().await;
855 let deadline = tokio::time::Instant::now() + timeout;
856
857 if let Some(e) = self.find_by_url(substring).await {
859 return Some(e);
860 }
861
862 loop {
863 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
864 match tokio::time::timeout(remaining, rx.recv()).await {
865 Ok(Ok(entry)) => {
866 if entry.request.url.contains(substring) {
867 return Some(entry);
868 }
869 }
870 _ => return None,
871 }
872 }
873 }
874
875 pub async fn wait_for_response(
877 &self,
878 id: &str,
879 timeout: std::time::Duration,
880 ) -> Option<Arc<NetworkEntry>> {
881 let mut rx = self.subscribe().await;
882 let deadline = tokio::time::Instant::now() + timeout;
883
884 {
886 let inner = self.inner.read().await;
887 if let Some(idx) = inner.by_id.get(id) {
888 if inner.entries[*idx].has_response() || inner.entries[*idx].is_error() {
889 return Some(inner.entries[*idx].clone());
890 }
891 }
892 }
893
894 loop {
895 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
896 match tokio::time::timeout(remaining, rx.recv()).await {
897 Ok(Ok(entry)) => {
898 if entry.request.id == id && (entry.has_response() || entry.is_error()) {
899 return Some(entry);
900 }
901 }
902 _ => return None,
903 }
904 }
905 }
906
907 pub async fn find_by_status(&self, status: u16) -> Vec<Arc<NetworkEntry>> {
913 let inner = self.inner.read().await;
914 inner
915 .entries
916 .iter()
917 .filter(|e| e.status() == Some(status))
918 .cloned()
919 .collect()
920 }
921
922 pub async fn entries_since(&self, timestamp: u64) -> Vec<Arc<NetworkEntry>> {
924 let inner = self.inner.read().await;
925 inner
926 .entries
927 .iter()
928 .filter(|e| e.request.timestamp >= timestamp)
929 .cloned()
930 .collect()
931 }
932
933 pub async fn last_n(&self, n: usize) -> Vec<Arc<NetworkEntry>> {
935 let inner = self.inner.read().await;
936 inner
937 .entries
938 .iter()
939 .rev()
940 .take(n)
941 .cloned()
942 .collect::<Vec<_>>()
943 .into_iter()
944 .rev()
945 .collect()
946 }
947
948 pub async fn unique_urls(&self) -> Vec<String> {
950 let inner = self.inner.read().await;
951 let mut seen = std::collections::HashSet::new();
952 inner
953 .entries
954 .iter()
955 .filter_map(|e| {
956 if seen.insert(e.request.url.clone()) {
957 Some(e.request.url.clone())
958 } else {
959 None
960 }
961 })
962 .collect()
963 }
964
965 pub async fn save_to_json(&self, path: &std::path::Path) -> Result<()> {
970 let entries: Vec<NetworkEntry> =
971 self.entries().await.iter().map(|e| (**e).clone()).collect();
972 let json = serde_json::to_string_pretty(&entries)?;
973 tokio::fs::write(path, json).await?;
974 Ok(())
975 }
976
977 pub async fn save_as_har(
979 &self,
980 path: &std::path::Path,
981 page_title: Option<&str>,
982 ) -> Result<()> {
983 use chrono::Utc;
984
985 let entries = self.entries().await;
986 let started = if let Some(e) = entries.first() {
987 chrono::DateTime::from_timestamp_millis(e.request.timestamp as i64)
988 .unwrap_or_else(Utc::now)
989 } else {
990 Utc::now()
991 };
992
993 let mut har_entries = Vec::new();
994 for e in &entries {
995 let req = &e.request;
996 let resp = e.response.as_ref();
997 let timing = &req.timing;
998
999 let query_string: Vec<_> = match req.query_params() {
1000 Ok(params) => params
1001 .into_iter()
1002 .map(|(name, value)| serde_json::json!({"name": name, "value": value}))
1003 .collect(),
1004 Err(error) => {
1005 tracing::warn!(
1006 "failed to parse request URL for query parameters: {error}; url={}",
1007 req.url
1008 );
1009 Vec::new()
1010 }
1011 };
1012
1013 let request_json = serde_json::json!({
1014 "method": req.method,
1015 "url": req.url,
1016 "httpVersion": "HTTP/1.1",
1017 "headers": req.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
1018 "cookies": req.cookies.iter().map(|c| serde_json::json!({"name": c.name, "value": c.value, "domain": c.domain, "path": c.path})).collect::<Vec<_>>(),
1019 "queryString": query_string,
1020 "headersSize": -1,
1021 "bodySize": req.post_data.as_ref().map(|b| b.len() as i64).unwrap_or(-1),
1022 "postData": req.post_data.as_ref().map(|b| {
1023 let mime = req.request_header("content-type").unwrap_or("application/octet-stream");
1024 serde_json::json!({"mimeType": mime, "text": b})
1025 }),
1026 });
1027
1028 let response_json = resp.map(|r| serde_json::json!({
1029 "status": r.status,
1030 "statusText": r.status_text,
1031 "httpVersion": "HTTP/1.1",
1032 "headers": r.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
1033 "cookies": [],
1034 "content": {
1035 "size": r.body_size.unwrap_or(0),
1036 "mimeType": r.mime_type,
1037 },
1038 "redirectURL": "",
1039 "headersSize": -1,
1040 "bodySize": r.body_size.map(|b| b as i64).unwrap_or(-1),
1041 }));
1042
1043 let timings_json = serde_json::json!({
1044 "blocked": -1,
1045 "dns": option_f64_ms(timing.dns_end_ms, timing.dns_start_ms),
1046 "connect": option_f64_ms(timing.connect_end_ms, timing.connect_start_ms),
1047 "ssl": option_f64_ms(timing.connect_end_ms, timing.tls_start_ms),
1048 "send": -1,
1049 "wait": option_f64_ms(timing.response_start_ms, timing.connect_end_ms),
1050 "receive": option_f64_ms(timing.response_end_ms, timing.response_start_ms),
1051 });
1052
1053 har_entries.push(serde_json::json!({
1054 "startedDateTime": format!("{}", chrono::DateTime::from_timestamp_millis(req.timestamp as i64).unwrap_or_else(Utc::now)),
1055 "time": 0,
1056 "request": request_json,
1057 "response": response_json,
1058 "cache": {},
1059 "timings": timings_json,
1060 }));
1061 }
1062
1063 let har = serde_json::json!({
1064 "log": {
1065 "version": "1.2",
1066 "creator": {
1067 "name": "runtime_foxdriver",
1068 "version": env!("CARGO_PKG_VERSION"),
1069 },
1070 "pages": [{
1071 "startedDateTime": format!("{}", started),
1072 "id": "page_1",
1073 "title": page_title.unwrap_or("unknown"),
1074 "pageTimings": { "onContentLoad": -1, "onLoad": -1 },
1075 }],
1076 "entries": har_entries,
1077 }
1078 });
1079
1080 tokio::fs::write(path, serde_json::to_string_pretty(&har)?).await?;
1081 Ok(())
1082 }
1083
1084 pub(crate) async fn ingest_before_request_sent(&self, evt: &BeforeRequestSent) {
1089 let mut inner = self.inner.write().await;
1090 inner.metrics.requests_received += 1;
1091
1092 let req = build_request(evt);
1093 let resp = inner.pending_responses.remove(&req.id);
1094 let err = inner.pending_errors.remove(&req.id);
1095
1096 let entry = Arc::new(NetworkEntry {
1097 request: req,
1098 response: resp,
1099 error: err,
1100 });
1101 inner.push_entry(entry);
1102 }
1103
1104 pub(crate) async fn ingest_response_completed(&self, evt: &ResponseCompleted) {
1105 let mut inner = self.inner.write().await;
1106 inner.metrics.responses_received += 1;
1107
1108 let id = evt.params.base_parameters.request.request.inner().clone();
1109 let resp = build_response(evt);
1110
1111 if let Some(idx) = inner.by_id.get(&id).copied() {
1112 let mut new_entry = (*inner.entries[idx]).clone();
1113 if new_entry.response.is_some() {
1114 inner.metrics.duplicate_responses += 1;
1115 }
1116 new_entry.response = Some(resp);
1117 let new_arc = Arc::new(new_entry);
1118 inner.entries[idx] = new_arc.clone();
1119 let _ = inner.tx.send(new_arc);
1120 } else {
1121 while inner.max_pending > 0 && inner.pending_responses.len() >= inner.max_pending {
1122 if let Some(k) = inner.pending_responses.keys().next().cloned() {
1123 inner.pending_responses.remove(&k);
1124 inner.metrics.pending_responses_dropped += 1;
1125 }
1126 }
1127 inner.pending_responses.insert(id, resp);
1128 }
1129 }
1130
1131 pub(crate) async fn ingest_fetch_error(&self, evt: &FetchError) {
1132 let mut inner = self.inner.write().await;
1133 inner.metrics.errors_received += 1;
1134
1135 let id = evt.params.base_parameters.request.request.inner().clone();
1136 let err = CapturedError {
1137 id: id.clone(),
1138 url: evt.params.base_parameters.request.url.clone(),
1145 error_text: evt.params.error_text.clone(),
1146 };
1147
1148 if let Some(idx) = inner.by_id.get(&id).copied() {
1149 let mut new_entry = (*inner.entries[idx]).clone();
1150 if new_entry.error.is_some() {
1151 inner.metrics.duplicate_errors += 1;
1152 }
1153 new_entry.error = Some(err);
1154 let new_arc = Arc::new(new_entry);
1155 inner.entries[idx] = new_arc.clone();
1156 let _ = inner.tx.send(new_arc);
1157 } else {
1158 while inner.max_pending > 0 && inner.pending_errors.len() >= inner.max_pending {
1159 if let Some(k) = inner.pending_errors.keys().next().cloned() {
1160 inner.pending_errors.remove(&k);
1161 inner.metrics.pending_errors_dropped += 1;
1162 }
1163 }
1164 inner.pending_errors.insert(id, err);
1165 }
1166 }
1167}
1168
1169impl Default for NetworkLog {
1170 fn default() -> Self {
1171 Self::new()
1172 }
1173}
1174
1175fn option_f64_ms(end: Option<f64>, start: Option<f64>) -> f64 {
1176 match (end, start) {
1177 (Some(e), Some(s)) => (e - s).max(0.0),
1178 _ => -1.0,
1179 }
1180}
1181
1182fn build_request(evt: &BeforeRequestSent) -> CapturedRequest {
1187 let bp = &evt.params.base_parameters;
1188 let id = bp.request.request.inner().clone();
1189 let url = bp.request.url.clone();
1190 let headers = bp
1191 .request
1192 .headers
1193 .iter()
1194 .map(CapturedHeader::from)
1195 .collect();
1196 let post_data = None; let timestamp = bp.timestamp;
1198 let destination = bp.request.destination.clone();
1199 let initiator_type = evt
1200 .params
1201 .initiator
1202 .as_ref()
1203 .and_then(|i| i.r#type.as_ref().map(|t| format!("{:?}", t).to_lowercase()));
1204 let timing = CapturedTiming::from(&bp.request.timings);
1205 let cookies = bp
1206 .request
1207 .cookies
1208 .iter()
1209 .map(CapturedCookie::from)
1210 .collect();
1211
1212 CapturedRequest {
1213 id,
1214 context: bp.context.as_ref().map(|c| c.inner().to_string()),
1215 method: bp.request.method.clone(),
1216 url,
1217 headers,
1218 post_data,
1219 timestamp,
1220 destination,
1221 initiator_type,
1222 timing,
1223 cookies,
1224 }
1225}
1226
1227fn build_response(evt: &ResponseCompleted) -> CapturedResponse {
1228 let bp = &evt.params.base_parameters;
1229 let resp = &evt.params.response;
1230 let id = bp.request.request.inner().clone();
1231 let url = resp.url.clone();
1232 let headers = resp.headers.iter().map(CapturedHeader::from).collect();
1233 let protocol = resp.protocol.clone();
1234 let status = resp.status as u16;
1235 let status_text = resp.status_text.clone();
1236 let mime_type = resp.mime_type.clone();
1237 let body_size = resp.body_size;
1238 let from_cache = resp.from_cache;
1239
1240 CapturedResponse {
1241 id,
1242 url,
1243 protocol,
1244 status,
1245 status_text,
1246 headers,
1247 mime_type,
1248 body_size,
1249 from_cache,
1250 }
1251}
1252
1253pub fn make_network_handler(
1258 log: NetworkLog,
1259) -> impl FnMut(
1260 rustenium_bidi_definitions::Event,
1261) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
1262 use rustenium_bidi_definitions::network::events::NetworkEvent;
1263 move |evt| {
1264 let log = log.clone();
1265 Box::pin(async move {
1266 if let rustenium_bidi_definitions::Event::Network(nev) = evt {
1267 match nev {
1268 NetworkEvent::BeforeRequestSent(evt) => {
1269 log.ingest_before_request_sent(&evt).await
1270 }
1271 NetworkEvent::ResponseCompleted(evt) => {
1272 log.ingest_response_completed(&evt).await
1273 }
1274 NetworkEvent::FetchError(evt) => log.ingest_fetch_error(&evt).await,
1275 _ => {}
1276 }
1277 }
1278 })
1279 }
1280}
1281
1282#[cfg(test)]
1287mod tests {
1288 use super::*;
1289
1290 fn make_request(id: &str, method: &str, url: &str) -> CapturedRequest {
1293 CapturedRequest {
1294 id: id.into(),
1295 context: None,
1296 method: method.into(),
1297 url: url.into(),
1298 headers: vec![],
1299 post_data: None,
1300 timestamp: 0,
1301 destination: "document".into(),
1302 initiator_type: None,
1303 timing: CapturedTiming::default(),
1304 cookies: vec![],
1305 }
1306 }
1307
1308 fn make_request_with_headers(
1309 id: &str,
1310 method: &str,
1311 url: &str,
1312 headers: Vec<CapturedHeader>,
1313 ) -> CapturedRequest {
1314 CapturedRequest {
1315 id: id.into(),
1316 context: None,
1317 method: method.into(),
1318 url: url.into(),
1319 headers,
1320 post_data: None,
1321 timestamp: 0,
1322 destination: "document".into(),
1323 initiator_type: None,
1324 timing: CapturedTiming::default(),
1325 cookies: vec![],
1326 }
1327 }
1328
1329 fn make_response(id: &str, status: u16, url: &str) -> CapturedResponse {
1330 CapturedResponse {
1331 id: id.into(),
1332 url: url.into(),
1333 protocol: "h2".into(),
1334 status,
1335 status_text: "OK".into(),
1336 headers: vec![],
1337 mime_type: "application/json".into(),
1338 body_size: Some(100),
1339 from_cache: false,
1340 }
1341 }
1342
1343 fn make_error(id: &str, url: &str, text: &str) -> CapturedError {
1344 CapturedError {
1345 id: id.into(),
1346 url: url.into(),
1347 error_text: text.into(),
1348 }
1349 }
1350
1351 async fn push_request(log: &NetworkLog, req: CapturedRequest) {
1352 let entry = Arc::new(NetworkEntry {
1353 request: req,
1354 response: None,
1355 error: None,
1356 });
1357 let mut inner = log.inner.write().await;
1358 inner.push_entry(entry);
1359 }
1360
1361 async fn push_entry(
1362 log: &NetworkLog,
1363 req: CapturedRequest,
1364 resp: Option<CapturedResponse>,
1365 err: Option<CapturedError>,
1366 ) {
1367 let entry = Arc::new(NetworkEntry {
1368 request: req,
1369 response: resp,
1370 error: err,
1371 });
1372 let mut inner = log.inner.write().await;
1373 inner.push_entry(entry);
1374 }
1375
1376 #[tokio::test]
1379 async fn test_new_log_is_empty() {
1380 let log = NetworkLog::new();
1381 assert!(log.is_empty().await);
1382 assert_eq!(log.len().await, 0);
1383 }
1384
1385 #[tokio::test]
1386 async fn test_with_limits() {
1387 let log = NetworkLog::with_limits(10, 5);
1388 let m = log.metrics().await;
1389 assert_eq!(m.max_entries, 10);
1390 }
1391
1392 #[tokio::test]
1393 async fn test_clear() {
1394 let log = NetworkLog::new();
1395 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1396 assert_eq!(log.len().await, 1);
1397 log.clear().await;
1398 assert!(log.is_empty().await);
1399 }
1400
1401 #[tokio::test]
1402 async fn test_entries_returns_all() {
1403 let log = NetworkLog::new();
1404 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1405 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1406 let entries = log.entries().await;
1407 assert_eq!(entries.len(), 2);
1408 assert_eq!(entries[0].request.id, "1");
1409 assert_eq!(entries[1].request.id, "2");
1410 }
1411
1412 #[tokio::test]
1413 async fn test_first_last_nth() {
1414 let log = NetworkLog::new();
1415 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1416 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1417 push_request(&log, make_request("3", "PUT", "https://c.com")).await;
1418
1419 assert_eq!(log.first().await.unwrap().request.id, "1");
1420 assert_eq!(log.last().await.unwrap().request.id, "3");
1421 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
1422 assert_eq!(log.nth(1).await.unwrap().request.id, "2");
1423 assert_eq!(log.nth(2).await.unwrap().request.id, "3");
1424 assert!(log.nth(99).await.is_none());
1425 }
1426
1427 #[tokio::test]
1430 async fn test_filter_method() {
1431 let log = NetworkLog::new();
1432 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1433 push_request(&log, make_request("2", "POST", "https://example.com")).await;
1434 let get = log.filter(Filter::new().method("GET")).await;
1435 assert_eq!(get.len(), 1);
1436 assert_eq!(get[0].request.method, "GET");
1437 }
1438
1439 #[tokio::test]
1440 async fn test_filter_status_range() {
1441 let log = NetworkLog::new();
1442 push_entry(
1443 &log,
1444 make_request("1", "GET", "https://a.com"),
1445 Some(make_response("1", 200, "https://a.com")),
1446 None,
1447 )
1448 .await;
1449 push_entry(
1450 &log,
1451 make_request("2", "GET", "https://b.com"),
1452 Some(make_response("2", 404, "https://b.com")),
1453 None,
1454 )
1455 .await;
1456 push_entry(&log, make_request("3", "GET", "https://c.com"), None, None).await;
1457 let f = log.filter(Filter::new().status_range(200..=299)).await;
1458 assert_eq!(f.len(), 1);
1459 assert_eq!(f[0].request.id, "1");
1460 }
1461
1462 #[tokio::test]
1463 async fn test_filter_url_contains() {
1464 let log = NetworkLog::new();
1465 push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1466 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1467 let f = log.filter(Filter::new().url_contains("api")).await;
1468 assert_eq!(f.len(), 1);
1469 assert_eq!(f[0].request.id, "1");
1470 }
1471
1472 #[tokio::test]
1473 async fn test_filter_url_regex() {
1474 let log = NetworkLog::new();
1475 push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1476 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1477 let f = log
1478 .filter(Filter::new().url_regex(r"api\.\w+\.com").unwrap())
1479 .await;
1480 assert_eq!(f.len(), 1);
1481 }
1482
1483 #[tokio::test]
1484 async fn test_filter_header() {
1485 let log = NetworkLog::new();
1486 let h1 = vec![CapturedHeader {
1487 name: "Authorization".into(),
1488 value: "Bearer abc".into(),
1489 }];
1490 let h2 = vec![CapturedHeader {
1491 name: "Content-Type".into(),
1492 value: "application/json".into(),
1493 }];
1494 push_request(
1495 &log,
1496 make_request_with_headers("1", "GET", "https://a.com", h1),
1497 )
1498 .await;
1499 push_request(
1500 &log,
1501 make_request_with_headers("2", "GET", "https://b.com", h2),
1502 )
1503 .await;
1504 let f = log
1505 .filter(Filter::new().header("authorization", "bearer"))
1506 .await;
1507 assert_eq!(f.len(), 1);
1508 assert_eq!(f[0].request.id, "1");
1509 }
1510
1511 #[tokio::test]
1512 async fn test_filter_destination() {
1513 let log = NetworkLog::new();
1514 let mut r1 = make_request("1", "GET", "https://a.com");
1515 r1.destination = "image".into();
1516 let mut r2 = make_request("2", "GET", "https://b.com");
1517 r2.destination = "document".into();
1518 push_request(&log, r1).await;
1519 push_request(&log, r2).await;
1520 let f = log.filter(Filter::new().destination("image")).await;
1521 assert_eq!(f.len(), 1);
1522 assert_eq!(f[0].request.id, "1");
1523 }
1524
1525 #[tokio::test]
1526 async fn test_filter_with_response() {
1527 let log = NetworkLog::new();
1528 push_entry(
1529 &log,
1530 make_request("1", "GET", "https://a.com"),
1531 Some(make_response("1", 200, "https://a.com")),
1532 None,
1533 )
1534 .await;
1535 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1536 let f = log.filter(Filter::new().with_response()).await;
1537 assert_eq!(f.len(), 1);
1538 assert_eq!(f[0].request.id, "1");
1539 }
1540
1541 #[tokio::test]
1542 async fn test_filter_without_response() {
1543 let log = NetworkLog::new();
1544 push_entry(
1545 &log,
1546 make_request("1", "GET", "https://a.com"),
1547 Some(make_response("1", 200, "https://a.com")),
1548 None,
1549 )
1550 .await;
1551 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1552 let f = log.filter(Filter::new().without_response()).await;
1553 assert_eq!(f.len(), 1);
1554 assert_eq!(f[0].request.id, "2");
1555 }
1556
1557 #[tokio::test]
1558 async fn test_filter_with_error() {
1559 let log = NetworkLog::new();
1560 push_entry(
1561 &log,
1562 make_request("1", "GET", "https://a.com"),
1563 None,
1564 Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
1565 )
1566 .await;
1567 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1568 let f = log.filter(Filter::new().with_error()).await;
1569 assert_eq!(f.len(), 1);
1570 assert_eq!(f[0].request.id, "1");
1571 }
1572
1573 #[tokio::test]
1574 async fn test_filter_composition() {
1575 let log = NetworkLog::new();
1576 push_entry(
1577 &log,
1578 make_request("1", "POST", "https://api.example.com/login"),
1579 Some(make_response("1", 200, "https://api.example.com/login")),
1580 None,
1581 )
1582 .await;
1583 push_request(
1584 &log,
1585 make_request("2", "GET", "https://api.example.com/login"),
1586 )
1587 .await;
1588 let f = log
1589 .filter(
1590 Filter::new()
1591 .method("POST")
1592 .url_contains("login")
1593 .with_response(),
1594 )
1595 .await;
1596 assert_eq!(f.len(), 1);
1597 assert_eq!(f[0].request.id, "1");
1598 }
1599
1600 #[tokio::test]
1601 async fn test_count() {
1602 let log = NetworkLog::new();
1603 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1604 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1605 assert_eq!(log.count(Filter::new().method("GET")).await, 1);
1606 assert_eq!(log.count(Filter::new()).await, 2);
1607 }
1608
1609 #[tokio::test]
1612 async fn test_completed() {
1613 let log = NetworkLog::new();
1614 push_entry(
1615 &log,
1616 make_request("1", "GET", "https://a.com"),
1617 Some(make_response("1", 200, "https://a.com")),
1618 None,
1619 )
1620 .await;
1621 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1622 push_entry(
1623 &log,
1624 make_request("3", "GET", "https://c.com"),
1625 None,
1626 Some(make_error("3", "https://c.com", "err")),
1627 )
1628 .await;
1629 let c = log.completed().await;
1630 assert_eq!(c.len(), 2);
1631 }
1632
1633 #[tokio::test]
1636 async fn test_find_by_url() {
1637 let log = NetworkLog::new();
1638 push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1639 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1640 assert_eq!(log.find_by_url("api").await.unwrap().request.id, "1");
1641 assert!(log.find_by_url("notfound").await.is_none());
1642 }
1643
1644 #[tokio::test]
1645 async fn test_find_by_url_regex() {
1646 let log = NetworkLog::new();
1647 push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1648 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1649 let re = regex::Regex::new(r"api\.\w+\.com").unwrap();
1650 assert_eq!(log.find_by_url_regex(&re).await.unwrap().request.id, "1");
1651 }
1652
1653 #[tokio::test]
1656 async fn test_endpoints() {
1657 let log = NetworkLog::new();
1658 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1659 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1660 push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1661 let ep = log.endpoints().await;
1662 assert_eq!(ep.len(), 3);
1663 }
1664
1665 #[tokio::test]
1666 async fn test_hostnames() {
1667 let log = NetworkLog::new();
1668 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1669 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1670 push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1671 let h = log.hostnames().await;
1672 assert_eq!(h.len(), 2);
1673 assert!(h.contains(&"a.com".into()));
1674 assert!(h.contains(&"b.com".into()));
1675 }
1676
1677 #[tokio::test]
1678 async fn test_distinct_methods() {
1679 let log = NetworkLog::new();
1680 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1681 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1682 push_request(&log, make_request("3", "GET", "https://c.com")).await;
1683 let m = log.distinct_methods().await;
1684 assert_eq!(m, vec!["GET", "POST"]);
1685 }
1686
1687 #[tokio::test]
1688 async fn test_distinct_statuses() {
1689 let log = NetworkLog::new();
1690 push_entry(
1691 &log,
1692 make_request("1", "GET", "https://a.com"),
1693 Some(make_response("1", 200, "https://a.com")),
1694 None,
1695 )
1696 .await;
1697 push_entry(
1698 &log,
1699 make_request("2", "GET", "https://b.com"),
1700 Some(make_response("2", 404, "https://b.com")),
1701 None,
1702 )
1703 .await;
1704 push_entry(
1705 &log,
1706 make_request("3", "GET", "https://c.com"),
1707 Some(make_response("3", 200, "https://c.com")),
1708 None,
1709 )
1710 .await;
1711 let s = log.distinct_statuses().await;
1712 assert_eq!(s, vec![200, 404]);
1713 }
1714
1715 #[tokio::test]
1716 async fn test_total_bytes() {
1717 let log = NetworkLog::new();
1718 let mut r1 = make_request("1", "POST", "https://a.com");
1719 r1.post_data = Some("hello".into());
1720 push_entry(
1721 &log,
1722 r1,
1723 Some(make_response("1", 200, "https://a.com")),
1724 None,
1725 )
1726 .await;
1727 let mut r2 = make_request("2", "POST", "https://b.com");
1728 r2.post_data = Some("world!!".into());
1729 push_entry(
1730 &log,
1731 r2,
1732 Some(make_response("2", 200, "https://b.com")),
1733 None,
1734 )
1735 .await;
1736 assert_eq!(log.total_bytes_in().await, 200);
1737 assert_eq!(log.total_bytes_out().await, 12); }
1739
1740 #[tokio::test]
1743 async fn test_entry_status_and_final_url() {
1744 let entry = NetworkEntry {
1745 request: make_request("1", "GET", "https://a.com"),
1746 response: Some(make_response("1", 301, "https://b.com")),
1747 error: None,
1748 };
1749 assert_eq!(entry.status(), Some(301));
1750 assert_eq!(entry.final_url(), "https://b.com");
1751 assert!(entry.has_response());
1752 assert!(!entry.is_error());
1753 }
1754
1755 #[tokio::test]
1756 async fn test_entry_request_header() {
1757 let req = make_request_with_headers(
1758 "1",
1759 "GET",
1760 "https://a.com",
1761 vec![CapturedHeader {
1762 name: "X-Custom".into(),
1763 value: "123".into(),
1764 }],
1765 );
1766 let entry = NetworkEntry {
1767 request: req,
1768 response: None,
1769 error: None,
1770 };
1771 assert_eq!(entry.request_header("x-custom"), Some("123"));
1772 assert_eq!(entry.request_header("missing"), None);
1773 }
1774
1775 #[tokio::test]
1776 async fn test_entry_response_header() {
1777 let req = make_request("1", "GET", "https://a.com");
1778 let resp = CapturedResponse {
1779 id: "1".into(),
1780 url: "https://a.com".into(),
1781 protocol: "h2".into(),
1782 status: 200,
1783 status_text: "OK".into(),
1784 headers: vec![CapturedHeader {
1785 name: "Content-Type".into(),
1786 value: "application/json".into(),
1787 }],
1788 mime_type: "application/json".into(),
1789 body_size: Some(10),
1790 from_cache: false,
1791 };
1792 let entry = NetworkEntry {
1793 request: req,
1794 response: Some(resp),
1795 error: None,
1796 };
1797 assert_eq!(
1798 entry.response_header("content-type"),
1799 Some("application/json")
1800 );
1801 assert_eq!(entry.response_header("missing"), None);
1802 }
1803
1804 #[tokio::test]
1805 async fn test_entry_to_curl() {
1806 let req = CapturedRequest {
1807 id: "1".into(),
1808 context: None,
1809 method: "POST".into(),
1810 url: "https://api.example.com/login".into(),
1811 headers: vec![
1812 CapturedHeader {
1813 name: "Content-Type".into(),
1814 value: "application/json".into(),
1815 },
1816 CapturedHeader {
1817 name: "Host".into(),
1818 value: "api.example.com".into(),
1819 },
1820 ],
1821 post_data: Some(r#"{"user":"admin"}"#.into()),
1822 timestamp: 0,
1823 destination: "document".into(),
1824 initiator_type: None,
1825 timing: CapturedTiming::default(),
1826 cookies: vec![],
1827 };
1828 let entry = NetworkEntry {
1829 request: req,
1830 response: None,
1831 error: None,
1832 };
1833 let curl = entry.to_curl();
1834 assert!(curl.starts_with("curl -X 'POST'"));
1835 assert!(curl.contains("-H 'Content-Type: application/json'"));
1836 assert!(!curl.contains("Host")); assert!(curl.contains(r#"-d '{"user":"admin"}'"#));
1838 assert!(curl.contains("'https://api.example.com/login'"));
1839 }
1840
1841 #[tokio::test]
1844 async fn test_request_json_body() {
1845 let mut req = make_request("1", "POST", "https://a.com");
1846 req.post_data = Some(r#"{"key":"value"}"#.into());
1847 assert_eq!(req.json_body(), Some(serde_json::json!({"key": "value"})));
1848 }
1849
1850 #[tokio::test]
1851 async fn test_request_json_body_invalid() {
1852 let mut req = make_request("1", "POST", "https://a.com");
1853 req.post_data = Some("not json".into());
1854 assert!(req.json_body().is_none());
1855 }
1856
1857 #[tokio::test]
1858 async fn test_request_query_params() {
1859 let req = make_request("1", "GET", "https://a.com?foo=bar&baz=qux");
1860 let params = req.query_params().unwrap();
1861 assert_eq!(params.len(), 2);
1862 assert!(params.contains(&("foo".into(), "bar".into())));
1863 assert!(params.contains(&("baz".into(), "qux".into())));
1864 }
1865
1866 #[tokio::test]
1867 async fn test_request_query_params_malformed_url() {
1868 let mut req = make_request("1", "GET", "https://a.com?foo=bar");
1869 req.url = "not a valid url".into();
1870 assert!(
1871 req.query_params().is_err(),
1872 "query_params must report a parse failure, not silently return an empty vec"
1873 );
1874 }
1875
1876 #[tokio::test]
1883 async fn test_max_entries_eviction() {
1884 let log = NetworkLog::with_limits(4, 10);
1885 for i in 0..6 {
1886 push_request(
1887 &log,
1888 make_request(&format!("{}", i), "GET", "https://example.com"),
1889 )
1890 .await;
1891 }
1892 let entries = log.entries().await;
1897 let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
1898 assert_eq!(ids, vec!["2", "3", "4", "5"]);
1899
1900 let m = log.metrics().await;
1901 assert_eq!(m.entries_evicted, 2);
1902 }
1903
1904 #[tokio::test]
1907 async fn test_broadcast_receives_entries() {
1908 let log = NetworkLog::new();
1909 let mut rx = log.subscribe().await;
1910 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1911 let received = rx.recv().await.unwrap();
1912 assert_eq!(received.request.id, "1");
1913 }
1914
1915 #[tokio::test]
1916 async fn test_broadcast_multiple_receivers() {
1917 let log = NetworkLog::new();
1918 let mut rx1 = log.subscribe().await;
1919 let mut rx2 = log.subscribe().await;
1920 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1921 assert_eq!(rx1.recv().await.unwrap().request.id, "1");
1922 assert_eq!(rx2.recv().await.unwrap().request.id, "1");
1923 }
1924
1925 #[tokio::test]
1928 async fn test_metrics_requests_received() {
1929 let log = NetworkLog::new();
1930 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1931 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1932 let m = log.metrics().await;
1933 assert_eq!(m.requests_received, 0); assert_eq!(m.max_entries, 50_000);
1935 }
1936
1937 #[tokio::test]
1940 async fn test_network_entry_serialize_roundtrip() {
1941 let entry = NetworkEntry {
1942 request: make_request("1", "GET", "https://example.com"),
1943 response: Some(make_response("1", 200, "https://example.com")),
1944 error: None,
1945 };
1946 let json = serde_json::to_string(&entry).unwrap();
1947 let de: NetworkEntry = serde_json::from_str(&json).unwrap();
1948 assert_eq!(de.request.id, entry.request.id);
1949 assert_eq!(de.status(), Some(200));
1950 }
1951
1952 #[tokio::test]
1955 async fn test_log_clone_shares_state() {
1956 let log1 = NetworkLog::new();
1957 let log2 = log1.clone();
1958 push_request(&log1, make_request("1", "GET", "https://example.com")).await;
1959 assert_eq!(log2.len().await, 1);
1960 }
1961
1962 #[tokio::test]
1965 async fn test_non_neg() {
1966 assert_eq!(non_neg(5.0), Some(5.0));
1967 assert_eq!(non_neg(-1.0), None);
1968 assert_eq!(non_neg(0.0), Some(0.0));
1969 }
1970
1971 #[tokio::test]
1972 async fn test_option_f64_ms() {
1973 assert_eq!(option_f64_ms(Some(10.0), Some(3.0)), 7.0);
1974 assert_eq!(option_f64_ms(Some(3.0), Some(10.0)), 0.0); assert_eq!(option_f64_ms(None, Some(3.0)), -1.0);
1976 }
1977
1978 #[tokio::test]
1981 async fn test_empty_filter_matches_all() {
1982 let log = NetworkLog::new();
1983 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1984 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1985 assert_eq!(log.filter(Filter::new()).await.len(), 2);
1986 assert_eq!(log.count(Filter::new()).await, 2);
1987 }
1988
1989 #[tokio::test]
1990 async fn test_eviction_rebuilds_by_id() {
1991 let log = NetworkLog::with_limits(4, 10);
1992 for i in 0..6 {
1993 push_request(
1994 &log,
1995 make_request(&format!("{}", i), "GET", "https://example.com"),
1996 )
1997 .await;
1998 }
1999 let entries = log.entries().await;
2001 assert_eq!(entries.len(), 4);
2002 for e in &entries {
2003 assert!(log.find_by_url(&e.request.url).await.is_some());
2004 }
2005 }
2006
2007 #[tokio::test]
2008 async fn test_broadcast_dropped_receiver_does_not_panic() {
2009 let log = NetworkLog::new();
2010 {
2011 let _rx = log.subscribe().await;
2012 } push_request(&log, make_request("1", "GET", "https://example.com")).await;
2014 }
2016
2017 #[tokio::test]
2018 async fn test_save_to_json_empty() {
2019 let log = NetworkLog::new();
2020 let path = std::path::Path::new("/tmp/foxdriver_network_empty.json");
2021 log.save_to_json(path).await.unwrap();
2022 let content = std::fs::read_to_string(path).unwrap();
2023 assert_eq!(content.trim(), "[]");
2024 }
2025
2026 #[tokio::test]
2027 async fn test_save_as_har_empty() {
2028 let log = NetworkLog::new();
2029 let path = std::path::Path::new("/tmp/foxdriver_network_empty.har");
2030 log.save_as_har(path, Some("test")).await.unwrap();
2031 let content = std::fs::read_to_string(path).unwrap();
2032 assert!(content.contains("1.2"));
2033 assert!(content.contains("runtime_foxdriver"));
2034 }
2035
2036 #[tokio::test]
2037 async fn test_captured_timing_default() {
2038 let t = CapturedTiming::default();
2039 assert!(t.dns_start_ms.is_none());
2040 assert!(t.response_end_ms.is_none());
2041 }
2042
2043 #[tokio::test]
2044 async fn test_network_entry_no_response_final_url() {
2045 let entry = NetworkEntry {
2046 request: make_request("1", "GET", "https://a.com"),
2047 response: None,
2048 error: None,
2049 };
2050 assert_eq!(entry.final_url(), "https://a.com");
2051 assert_eq!(entry.status(), None);
2052 assert!(!entry.has_response());
2053 assert!(!entry.is_error());
2054 }
2055
2056 #[tokio::test]
2057 async fn test_distinct_methods_empty() {
2058 let log = NetworkLog::new();
2059 assert!(log.distinct_methods().await.is_empty());
2060 }
2061
2062 #[tokio::test]
2063 async fn test_distinct_statuses_empty() {
2064 let log = NetworkLog::new();
2065 assert!(log.distinct_statuses().await.is_empty());
2066 }
2067
2068 #[tokio::test]
2069 async fn test_hostnames_malformed_url() {
2070 let log = NetworkLog::new();
2071 push_request(&log, make_request("1", "GET", "not-a-url")).await;
2072 assert!(log.hostnames().await.is_empty());
2073 }
2074
2075 #[tokio::test]
2076 async fn test_total_bytes_no_response() {
2077 let log = NetworkLog::new();
2078 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2079 assert_eq!(log.total_bytes_in().await, 0);
2080 }
2081
2082 #[tokio::test]
2085 async fn test_contains_id() {
2086 let log = NetworkLog::new();
2087 push_request(&log, make_request("abc", "GET", "https://a.com")).await;
2088 assert!(log.contains_id("abc").await);
2089 assert!(!log.contains_id("xyz").await);
2090 }
2091
2092 #[tokio::test]
2093 async fn test_remove_by_id() {
2094 let log = NetworkLog::new();
2095 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2096 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2097 push_request(&log, make_request("3", "GET", "https://c.com")).await;
2098
2099 let removed = log.remove_by_id("2").await;
2100 assert_eq!(removed.unwrap().request.id, "2");
2101 assert_eq!(log.len().await, 2);
2102 assert!(!log.contains_id("2").await);
2103
2104 assert!(log.contains_id("1").await);
2106 assert!(log.contains_id("3").await);
2107 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
2108 assert_eq!(log.nth(1).await.unwrap().request.id, "3");
2109 }
2110
2111 #[tokio::test]
2112 async fn test_remove_by_id_unknown() {
2113 let log = NetworkLog::new();
2114 assert!(log.remove_by_id("nope").await.is_none());
2115 }
2116
2117 #[tokio::test]
2120 async fn test_wait_for_url_existing() {
2121 let log = NetworkLog::new();
2122 push_request(&log, make_request("1", "GET", "https://target.com/page")).await;
2123 let found = log
2124 .wait_for_url("target.com", std::time::Duration::from_secs(1))
2125 .await;
2126 assert_eq!(found.unwrap().request.id, "1");
2127 }
2128
2129 #[tokio::test]
2130 async fn test_wait_for_url_future() {
2131 let log = NetworkLog::new();
2132 let log2 = log.clone();
2133 tokio::spawn(async move {
2134 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2135 push_request(&log2, make_request("1", "GET", "https://target.com/page")).await;
2136 });
2137 let found = log
2138 .wait_for_url("target.com", std::time::Duration::from_secs(1))
2139 .await;
2140 assert_eq!(found.unwrap().request.id, "1");
2141 }
2142
2143 #[tokio::test]
2144 async fn test_wait_for_url_timeout() {
2145 let log = NetworkLog::new();
2146 let found = log
2147 .wait_for_url("never", std::time::Duration::from_millis(50))
2148 .await;
2149 assert!(found.is_none());
2150 }
2151
2152 #[tokio::test]
2153 async fn test_wait_for_response_existing() {
2154 let log = NetworkLog::new();
2155 push_entry(
2156 &log,
2157 make_request("1", "GET", "https://a.com"),
2158 Some(make_response("1", 200, "https://a.com")),
2159 None,
2160 )
2161 .await;
2162 let found = log
2163 .wait_for_response("1", std::time::Duration::from_secs(1))
2164 .await;
2165 assert_eq!(found.unwrap().request.id, "1");
2166 }
2167
2168 #[tokio::test]
2169 async fn test_wait_for_response_future() {
2170 let log = NetworkLog::new();
2171 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2172 let log2 = log.clone();
2173 tokio::spawn(async move {
2174 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2175 let mut inner = log2.inner.write().await;
2177 if let Some(idx) = inner.by_id.get("1").copied() {
2178 let mut new_entry = (*inner.entries[idx]).clone();
2179 new_entry.response = Some(make_response("1", 200, "https://a.com"));
2180 let new_arc = Arc::new(new_entry);
2181 inner.entries[idx] = new_arc.clone();
2182 let _ = inner.tx.send(new_arc);
2183 }
2184 });
2185 let found = log
2186 .wait_for_response("1", std::time::Duration::from_secs(1))
2187 .await;
2188 assert_eq!(found.unwrap().request.id, "1");
2189 }
2190
2191 #[tokio::test]
2192 async fn test_wait_for_response_timeout() {
2193 let log = NetworkLog::new();
2194 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2195 let found = log
2196 .wait_for_response("1", std::time::Duration::from_millis(50))
2197 .await;
2198 assert!(found.is_none());
2199 }
2200
2201 #[tokio::test]
2204 async fn test_concurrent_pushes() {
2205 let log = NetworkLog::new();
2206 let mut handles = Vec::new();
2207 for t in 0..10 {
2208 let log = log.clone();
2209 handles.push(tokio::spawn(async move {
2210 for i in 0..100 {
2211 push_request(
2212 &log,
2213 make_request(&format!("{}-{}", t, i), "GET", "https://example.com"),
2214 )
2215 .await;
2216 }
2217 }));
2218 }
2219 for h in handles {
2220 h.await.unwrap();
2221 }
2222 assert_eq!(log.len().await, 1000);
2223 }
2224
2225 #[tokio::test]
2226 async fn test_concurrent_read_while_write() {
2227 let log = NetworkLog::new();
2228 let log2 = log.clone();
2229
2230 let writer = tokio::spawn(async move {
2231 for i in 0..500 {
2232 push_request(
2233 &log,
2234 make_request(&format!("{}", i), "GET", "https://example.com"),
2235 )
2236 .await;
2237 if i % 50 == 0 {
2238 tokio::task::yield_now().await;
2239 }
2240 }
2241 });
2242
2243 let reader = tokio::spawn(async move {
2244 let mut last_len = 0;
2245 for _ in 0..50 {
2246 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
2247 let len = log2.len().await;
2248 assert!(len >= last_len); last_len = len;
2250 }
2251 });
2252
2253 let (r1, r2) = tokio::join!(writer, reader);
2254 r1.unwrap();
2255 r2.unwrap();
2256 }
2257
2258 #[tokio::test]
2261 async fn test_save_to_json_roundtrip() {
2262 let log = NetworkLog::new();
2263 push_entry(
2264 &log,
2265 make_request("1", "GET", "https://example.com"),
2266 Some(make_response("1", 200, "https://example.com")),
2267 None,
2268 )
2269 .await;
2270 let path = std::path::Path::new("/tmp/foxdriver_network_real.json");
2271 log.save_to_json(path).await.unwrap();
2272 let content = std::fs::read_to_string(path).unwrap();
2273 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
2274 assert_eq!(parsed.len(), 1);
2275 assert_eq!(parsed[0].request.id, "1");
2276 assert_eq!(parsed[0].status(), Some(200));
2277 }
2278
2279 #[tokio::test]
2280 async fn test_save_as_har_with_data() {
2281 let log = NetworkLog::new();
2282 let mut req = make_request("1", "POST", "https://api.example.com/data?key=val");
2283 req.post_data = Some(r#"{"hello":"world"}"#.into());
2284 req.headers = vec![CapturedHeader {
2285 name: "Content-Type".into(),
2286 value: "application/json".into(),
2287 }];
2288 push_entry(
2289 &log,
2290 req,
2291 Some(make_response(
2292 "1",
2293 201,
2294 "https://api.example.com/data?key=val",
2295 )),
2296 None,
2297 )
2298 .await;
2299
2300 let path = std::path::Path::new("/tmp/foxdriver_network_real.har");
2301 log.save_as_har(path, Some("test page")).await.unwrap();
2302 let content = std::fs::read_to_string(path).unwrap();
2303 assert!(content.contains("1.2"));
2304 assert!(content.contains("POST"));
2305 assert!(content.contains("201"));
2306 assert!(content.contains("key=val"));
2307 assert!(content.contains("hello"));
2308 assert!(content.contains("test page"));
2309 }
2310
2311 #[tokio::test]
2314 async fn test_to_curl_no_headers_no_body() {
2315 let req = make_request("1", "GET", "https://example.com");
2316 let entry = NetworkEntry {
2317 request: req,
2318 response: None,
2319 error: None,
2320 };
2321 let curl = entry.to_curl();
2322 assert_eq!(curl, "curl -X 'GET' 'https://example.com'");
2323 }
2324
2325 #[tokio::test]
2326 async fn test_to_curl_special_chars() {
2327 let req = CapturedRequest {
2328 id: "1".into(),
2329 context: None,
2330 method: "POST".into(),
2331 url: "https://example.com?a=1&b=2".into(),
2332 headers: vec![CapturedHeader {
2333 name: "X-Special".into(),
2334 value: "val'ue\".txt".into(),
2335 }],
2336 post_data: Some("data='quoted'".into()),
2337 timestamp: 0,
2338 destination: "document".into(),
2339 initiator_type: None,
2340 timing: CapturedTiming::default(),
2341 cookies: vec![],
2342 };
2343 let entry = NetworkEntry {
2344 request: req,
2345 response: None,
2346 error: None,
2347 };
2348 let curl = entry.to_curl();
2349 assert!(curl.contains("-H 'X-Special: val'\\''ue\".txt'"));
2350 assert!(curl.contains("-d 'data='\\''quoted'\\'''"));
2351 }
2352
2353 #[tokio::test]
2358 async fn test_filter_empty_method_matches_all() {
2359 let log = NetworkLog::new();
2360 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2361 push_request(&log, make_request("2", "POST", "https://b.com")).await;
2362 assert_eq!(log.filter(Filter::new()).await.len(), 2);
2364 }
2365
2366 #[tokio::test]
2367 async fn test_filter_status_range_no_matches() {
2368 let log = NetworkLog::new();
2369 push_entry(
2370 &log,
2371 make_request("1", "GET", "https://a.com"),
2372 Some(make_response("1", 200, "https://a.com")),
2373 None,
2374 )
2375 .await;
2376 let f = log.filter(Filter::new().status_range(500..=599)).await;
2377 assert!(f.is_empty());
2378 }
2379
2380 #[tokio::test]
2381 async fn test_filter_url_contains_case_insensitive() {
2382 let log = NetworkLog::new();
2383 push_request(&log, make_request("1", "GET", "https://API.example.com")).await;
2384 let f = log.filter(Filter::new().url_contains("api")).await;
2385 assert_eq!(f.len(), 1);
2386 }
2387
2388 #[tokio::test]
2389 async fn test_filter_destination_case_insensitive() {
2390 let log = NetworkLog::new();
2391 let mut r = make_request("1", "GET", "https://a.com");
2392 r.destination = "IMAGE".into();
2393 push_request(&log, r).await;
2394 let f = log.filter(Filter::new().destination("image")).await;
2395 assert_eq!(f.len(), 1);
2396 }
2397
2398 #[tokio::test]
2401 async fn test_query_params_empty() {
2402 let req = make_request("1", "GET", "https://a.com");
2403 assert!(req.query_params().unwrap().is_empty());
2404 }
2405
2406 #[tokio::test]
2407 async fn test_query_params_url_encoded() {
2408 let req = make_request("1", "GET", "https://a.com?foo=%20bar&baz=qux");
2409 let params = req.query_params().unwrap();
2410 assert!(params.contains(&("foo".into(), " bar".into())));
2411 assert!(params.contains(&("baz".into(), "qux".into())));
2412 }
2413
2414 #[tokio::test]
2417 async fn test_multiple_evictions() {
2418 let log = NetworkLog::with_limits(4, 10);
2419 for i in 0..20 {
2420 push_request(
2421 &log,
2422 make_request(&format!("{}", i), "GET", "https://example.com"),
2423 )
2424 .await;
2425 }
2426 let entries = log.entries().await;
2427 assert_eq!(entries.len(), 4);
2428 let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
2429 assert_eq!(ids, vec!["16", "17", "18", "19"]);
2430
2431 let m = log.metrics().await;
2432 assert_eq!(m.entries_evicted, 16);
2433 }
2434
2435 #[tokio::test]
2438 async fn test_request_header_multiple_same_name() {
2439 let req = make_request_with_headers(
2440 "1",
2441 "GET",
2442 "https://a.com",
2443 vec![
2444 CapturedHeader {
2445 name: "X-Dup".into(),
2446 value: "first".into(),
2447 },
2448 CapturedHeader {
2449 name: "X-Dup".into(),
2450 value: "second".into(),
2451 },
2452 ],
2453 );
2454 assert_eq!(req.request_header("x-dup"), Some("first")); }
2456
2457 #[tokio::test]
2458 async fn test_response_header_no_response() {
2459 let entry = NetworkEntry {
2460 request: make_request("1", "GET", "https://a.com"),
2461 response: None,
2462 error: None,
2463 };
2464 assert_eq!(entry.response_header("anything"), None);
2465 }
2466
2467 #[tokio::test]
2470 async fn test_option_f64_ms_both_none() {
2471 assert_eq!(option_f64_ms(None, None), -1.0);
2472 }
2473
2474 #[tokio::test]
2475 async fn test_option_f64_ms_end_none() {
2476 assert_eq!(option_f64_ms(None, Some(5.0)), -1.0);
2477 }
2478
2479 fn make_bidi_request_data(
2482 id: &str,
2483 url: &str,
2484 method: &str,
2485 ) -> rustenium_bidi_definitions::network::types::RequestData {
2486 use rustenium_bidi_definitions::network::types::{FetchTimingInfo, Request, RequestData};
2487 RequestData {
2488 request: Request::new(id),
2489 url: url.into(),
2490 method: method.into(),
2491 headers: vec![],
2492 cookies: vec![],
2493 headers_size: 0,
2494 body_size: None,
2495 destination: "document".into(),
2496 initiator_type: None,
2497 timings: FetchTimingInfo {
2498 time_origin: 0.0,
2499 request_time: 0.0,
2500 redirect_start: 0.0,
2501 redirect_end: 0.0,
2502 fetch_start: 0.0,
2503 dns_start: 0.0,
2504 dns_end: 0.0,
2505 connect_start: 0.0,
2506 connect_end: 0.0,
2507 tls_start: 0.0,
2508 request_start: 0.0,
2509 response_start: 0.0,
2510 response_end: 0.0,
2511 },
2512 extensible: std::collections::HashMap::new(),
2513 }
2514 }
2515
2516 fn make_before_request_sent(id: &str, url: &str, method: &str) -> BeforeRequestSent {
2517 use rustenium_bidi_definitions::network::events::BeforeRequestSentParams;
2518 use rustenium_bidi_definitions::network::types::BaseParameters;
2519 BeforeRequestSent {
2520 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
2521 params: BeforeRequestSentParams {
2522 base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, method), 0u64),
2523 initiator: None,
2524 },
2525 }
2526 }
2527
2528 fn make_response_completed(id: &str, url: &str, status: u64) -> ResponseCompleted {
2529 use rustenium_bidi_definitions::network::events::ResponseCompletedParams;
2530 use rustenium_bidi_definitions::network::types::{
2531 BaseParameters, ResponseContent, ResponseData,
2532 };
2533 ResponseCompleted {
2534 method: rustenium_bidi_definitions::network::events::ResponseCompletedMethod::ResponseCompleted,
2535 params: ResponseCompletedParams {
2536 base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, "GET"), 0u64),
2537 response: ResponseData {
2538 url: url.into(),
2539 protocol: "h2".into(),
2540 status,
2541 status_text: "OK".into(),
2542 from_cache: false,
2543 headers: vec![],
2544 mime_type: "application/json".into(),
2545 bytes_received: 100,
2546 headers_size: None,
2547 body_size: Some(100),
2548 content: ResponseContent::new(100u64),
2549 auth_challenges: None,
2550 },
2551 },
2552 }
2553 }
2554
2555 fn make_fetch_error(id: &str, url: &str, error_text: &str) -> FetchError {
2556 use rustenium_bidi_definitions::network::events::FetchErrorParams;
2557 use rustenium_bidi_definitions::network::types::BaseParameters;
2558 FetchError {
2559 method: rustenium_bidi_definitions::network::events::FetchErrorMethod::FetchError,
2560 params: FetchErrorParams {
2561 base_parameters: BaseParameters::new(
2562 false,
2563 0u64,
2564 make_bidi_request_data(id, url, "GET"),
2565 0u64,
2566 ),
2567 error_text: error_text.into(),
2568 },
2569 }
2570 }
2571
2572 #[tokio::test]
2573 async fn test_ingest_before_request_sent_creates_entry() {
2574 let log = NetworkLog::new();
2575 let evt = make_before_request_sent("req-1", "https://example.com", "GET");
2576 log.ingest_before_request_sent(&evt).await;
2577 assert_eq!(log.len().await, 1);
2578 assert!(log.contains_id("req-1").await);
2579 let m = log.metrics().await;
2580 assert_eq!(m.requests_received, 1);
2581 }
2582
2583 #[tokio::test]
2584 async fn test_ingest_response_completed_attaches_to_existing() {
2585 let log = NetworkLog::new();
2586 log.ingest_before_request_sent(&make_before_request_sent(
2587 "req-1",
2588 "https://example.com",
2589 "GET",
2590 ))
2591 .await;
2592 log.ingest_response_completed(&make_response_completed(
2593 "req-1",
2594 "https://example.com",
2595 200,
2596 ))
2597 .await;
2598
2599 let entry = log.first().await.unwrap();
2600 assert_eq!(entry.status(), Some(200));
2601 assert!(entry.has_response());
2602 let m = log.metrics().await;
2603 assert_eq!(m.responses_received, 1);
2604 }
2605
2606 #[tokio::test]
2607 async fn test_ingest_fetch_error_attaches_to_existing() {
2608 let log = NetworkLog::new();
2609 log.ingest_before_request_sent(&make_before_request_sent(
2610 "req-1",
2611 "https://example.com",
2612 "GET",
2613 ))
2614 .await;
2615 log.ingest_fetch_error(&make_fetch_error(
2616 "req-1",
2617 "https://example.com",
2618 "net::ERR_FAILED",
2619 ))
2620 .await;
2621
2622 let entry = log.first().await.unwrap();
2623 assert!(entry.is_error());
2624 assert_eq!(entry.error.as_ref().unwrap().error_text, "net::ERR_FAILED");
2625 let m = log.metrics().await;
2626 assert_eq!(m.errors_received, 1);
2627 }
2628
2629 #[tokio::test]
2630 async fn test_fetch_error_url_comes_from_request_not_navigation() {
2631 let log = NetworkLog::new();
2637 let failed_url = "https://cdn.example.com/app.js";
2638 log.ingest_fetch_error(&make_fetch_error("sub-1", failed_url, "net::ERR_FAILED"))
2639 .await;
2640
2641 let (_resp, errors) = log.drain_pending().await;
2642 assert_eq!(errors.len(), 1);
2643 assert_eq!(
2644 errors[0].url, failed_url,
2645 "error URL must be the request URL, not an empty navigation fallback"
2646 );
2647 }
2648
2649 #[tokio::test]
2650 async fn test_out_of_order_response_reconciliation() {
2651 let log = NetworkLog::new();
2652 log.ingest_response_completed(&make_response_completed(
2654 "req-1",
2655 "https://example.com",
2656 200,
2657 ))
2658 .await;
2659 assert_eq!(log.len().await, 0); assert_eq!(log.metrics().await.responses_received, 1);
2661
2662 log.ingest_before_request_sent(&make_before_request_sent(
2664 "req-1",
2665 "https://example.com",
2666 "GET",
2667 ))
2668 .await;
2669 assert_eq!(log.len().await, 1);
2670 let entry = log.first().await.unwrap();
2671 assert_eq!(entry.status(), Some(200));
2672 }
2673
2674 #[tokio::test]
2675 async fn test_out_of_order_error_reconciliation() {
2676 let log = NetworkLog::new();
2677 log.ingest_fetch_error(&make_fetch_error(
2678 "req-1",
2679 "https://example.com",
2680 "net::ERR_ABORTED",
2681 ))
2682 .await;
2683 assert_eq!(log.len().await, 0);
2684
2685 log.ingest_before_request_sent(&make_before_request_sent(
2686 "req-1",
2687 "https://example.com",
2688 "GET",
2689 ))
2690 .await;
2691 let entry = log.first().await.unwrap();
2692 assert!(entry.is_error());
2693 }
2694
2695 #[tokio::test]
2696 async fn test_response_update_rebroadcasts() {
2697 let log = NetworkLog::new();
2698 let mut rx = log.subscribe().await;
2699
2700 log.ingest_before_request_sent(&make_before_request_sent(
2701 "req-1",
2702 "https://example.com",
2703 "GET",
2704 ))
2705 .await;
2706 let first = rx.recv().await.unwrap();
2707 assert!(!first.has_response());
2708
2709 log.ingest_response_completed(&make_response_completed(
2710 "req-1",
2711 "https://example.com",
2712 200,
2713 ))
2714 .await;
2715 let updated = rx.recv().await.unwrap();
2716 assert!(updated.has_response());
2717 assert_eq!(updated.status(), Some(200));
2718 }
2719
2720 #[tokio::test]
2721 async fn test_pending_response_overflow() {
2722 let log = NetworkLog::with_limits(100, 3);
2723 for i in 0..5 {
2725 log.ingest_response_completed(&make_response_completed(
2726 &format!("req-{}", i),
2727 "https://example.com",
2728 200,
2729 ))
2730 .await;
2731 }
2732 let m = log.metrics().await;
2733 assert_eq!(m.pending_responses_dropped, 2); assert_eq!(m.responses_received, 5);
2735 }
2736
2737 #[tokio::test]
2738 async fn test_pending_error_overflow() {
2739 let log = NetworkLog::with_limits(100, 2);
2740 for i in 0..4 {
2741 log.ingest_fetch_error(&make_fetch_error(
2742 &format!("req-{}", i),
2743 "https://example.com",
2744 "err",
2745 ))
2746 .await;
2747 }
2748 let m = log.metrics().await;
2749 assert_eq!(m.pending_errors_dropped, 2); assert_eq!(m.errors_received, 4);
2751 }
2752
2753 #[tokio::test]
2754 async fn test_clear_then_ingest() {
2755 let log = NetworkLog::new();
2756 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2757 .await;
2758 log.clear().await;
2759 log.ingest_before_request_sent(&make_before_request_sent("req-2", "https://b.com", "POST"))
2760 .await;
2761 assert_eq!(log.len().await, 1);
2762 assert!(!log.contains_id("req-1").await);
2763 assert!(log.contains_id("req-2").await);
2764 }
2765
2766 #[tokio::test]
2767 async fn test_entry_with_both_response_and_error() {
2768 let log = NetworkLog::new();
2769 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2770 .await;
2771 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
2772 .await;
2773 log.ingest_fetch_error(&make_fetch_error(
2774 "req-1",
2775 "https://a.com",
2776 "net::ERR_FAILED",
2777 ))
2778 .await;
2779
2780 let entry = log.first().await.unwrap();
2781 assert!(entry.has_response());
2782 assert!(entry.is_error());
2783 assert_eq!(entry.status(), Some(200));
2784 }
2785
2786 #[tokio::test]
2789 async fn test_captured_cookie_roundtrip() {
2790 let cookie = CapturedCookie {
2791 name: "session".into(),
2792 value: "abc123".into(),
2793 domain: ".example.com".into(),
2794 path: "/".into(),
2795 size: 42,
2796 http_only: true,
2797 secure: true,
2798 same_site: "strict".into(),
2799 };
2800 let json = serde_json::to_string(&cookie).unwrap();
2801 let de: CapturedCookie = serde_json::from_str(&json).unwrap();
2802 assert_eq!(de, cookie);
2803 }
2804
2805 #[tokio::test]
2806 async fn test_captured_header_roundtrip() {
2807 let h = CapturedHeader {
2808 name: "X-Test".into(),
2809 value: "value".into(),
2810 };
2811 let json = serde_json::to_string(&h).unwrap();
2812 let de: CapturedHeader = serde_json::from_str(&json).unwrap();
2813 assert_eq!(de, h);
2814 }
2815
2816 #[tokio::test]
2817 async fn test_filter_url_regex_invalid_pattern() {
2818 let result = Filter::new().url_regex("[");
2819 assert!(result.is_err());
2820 }
2821
2822 #[tokio::test]
2823 async fn test_large_url_does_not_panic() {
2824 let log = NetworkLog::new();
2825 let long_url = format!("https://example.com/{}", "a".repeat(10000));
2826 push_request(&log, make_request("1", "GET", &long_url)).await;
2827 assert_eq!(log.len().await, 1);
2828 assert!(log.find_by_url("example.com").await.is_some());
2829 }
2830
2831 #[tokio::test]
2832 async fn test_eviction_with_subscriber() {
2833 let log = NetworkLog::with_limits(2, 10);
2834 let mut rx = log.subscribe().await;
2835 for i in 0..5 {
2836 push_request(
2837 &log,
2838 make_request(&format!("{}", i), "GET", "https://example.com"),
2839 )
2840 .await;
2841 }
2842 let mut count = 0;
2844 while rx.try_recv().is_ok() {
2845 count += 1;
2846 }
2847 assert_eq!(count, 5);
2848 }
2849
2850 #[tokio::test]
2853 async fn test_filter_method_case_insensitive() {
2854 let log = NetworkLog::new();
2855 push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
2856 assert_eq!(log.filter(Filter::new().method("delete")).await.len(), 1);
2857 assert_eq!(log.filter(Filter::new().method("DELETE")).await.len(), 1);
2858 }
2859
2860 #[tokio::test]
2861 async fn test_filter_header_in_response() {
2862 let log = NetworkLog::new();
2863 let req = make_request("1", "GET", "https://a.com");
2864 let mut resp = make_response("1", 200, "https://a.com");
2865 resp.headers = vec![CapturedHeader {
2866 name: "X-Resp".into(),
2867 value: "secret-val".into(),
2868 }];
2869 push_entry(&log, req, Some(resp), None).await;
2870 let f = log.filter(Filter::new().header("x-resp", "secret")).await;
2871 assert_eq!(f.len(), 1);
2872 }
2873
2874 #[tokio::test]
2875 async fn test_filter_composition_conflicting() {
2876 let log = NetworkLog::new();
2877 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2878 let f = log.filter(Filter::new().method("GET").method("POST")).await;
2880 assert!(f.is_empty());
2881 }
2882
2883 #[tokio::test]
2884 async fn test_count_zero() {
2885 let log = NetworkLog::new();
2886 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2887 assert_eq!(log.count(Filter::new().method("POST")).await, 0);
2888 }
2889
2890 #[tokio::test]
2891 async fn test_is_empty_false() {
2892 let log = NetworkLog::new();
2893 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2894 assert!(!log.is_empty().await);
2895 }
2896
2897 #[tokio::test]
2898 async fn test_endpoints_dedupes() {
2899 let log = NetworkLog::new();
2900 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2901 push_request(&log, make_request("2", "GET", "https://a.com/x")).await;
2902 push_request(&log, make_request("3", "GET", "https://a.com/y")).await;
2903 let ep = log.endpoints().await;
2904 assert_eq!(ep.len(), 2);
2905 }
2906
2907 #[tokio::test]
2908 async fn test_hostnames_with_port() {
2909 let log = NetworkLog::new();
2910 push_request(
2911 &log,
2912 make_request("1", "GET", "https://example.com:8443/path"),
2913 )
2914 .await;
2915 let h = log.hostnames().await;
2916 assert_eq!(h.len(), 1);
2917 assert!(h.contains(&"example.com".into()));
2918 }
2919
2920 #[tokio::test]
2921 async fn test_total_bytes_out_none() {
2922 let log = NetworkLog::new();
2923 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2924 assert_eq!(log.total_bytes_out().await, 0);
2925 }
2926
2927 #[tokio::test]
2928 async fn test_completed_mixed_states() {
2929 let log = NetworkLog::new();
2930 push_entry(
2931 &log,
2932 make_request("1", "GET", "https://a.com"),
2933 Some(make_response("1", 200, "https://a.com")),
2934 None,
2935 )
2936 .await;
2937 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2938 push_entry(
2939 &log,
2940 make_request("3", "GET", "https://c.com"),
2941 None,
2942 Some(make_error("3", "https://c.com", "err")),
2943 )
2944 .await;
2945 push_entry(
2946 &log,
2947 make_request("4", "GET", "https://d.com"),
2948 Some(make_response("4", 500, "https://d.com")),
2949 Some(make_error("4", "https://d.com", "err")),
2950 )
2951 .await;
2952 let c = log.completed().await;
2953 assert_eq!(c.len(), 3);
2954 }
2955
2956 #[tokio::test]
2957 async fn test_distinct_statuses_with_no_response() {
2958 let log = NetworkLog::new();
2959 push_entry(
2960 &log,
2961 make_request("1", "GET", "https://a.com"),
2962 Some(make_response("1", 200, "https://a.com")),
2963 None,
2964 )
2965 .await;
2966 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2967 push_entry(
2968 &log,
2969 make_request("3", "GET", "https://c.com"),
2970 Some(make_response("3", 404, "https://c.com")),
2971 None,
2972 )
2973 .await;
2974 let s = log.distinct_statuses().await;
2975 assert_eq!(s, vec![200, 404]);
2976 }
2977
2978 #[tokio::test]
2979 async fn test_distinct_methods_dedupes() {
2980 let log = NetworkLog::new();
2981 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2982 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2983 push_request(&log, make_request("3", "POST", "https://c.com")).await;
2984 push_request(&log, make_request("4", "POST", "https://d.com")).await;
2985 let m = log.distinct_methods().await;
2986 assert_eq!(m, vec!["GET", "POST"]);
2987 }
2988
2989 #[tokio::test]
2990 async fn test_find_by_url_empty_substring() {
2991 let log = NetworkLog::new();
2992 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2993 assert!(log.find_by_url("").await.is_some());
2995 }
2996
2997 #[tokio::test]
2998 async fn test_find_by_url_regex_no_match() {
2999 let log = NetworkLog::new();
3000 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3001 let re = regex::Regex::new(r"zzz").unwrap();
3002 assert!(log.find_by_url_regex(&re).await.is_none());
3003 }
3004
3005 #[tokio::test]
3006 async fn test_filter_url_contains_empty() {
3007 let log = NetworkLog::new();
3008 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3009 let f = log.filter(Filter::new().url_contains("")).await;
3010 assert_eq!(f.len(), 1);
3011 }
3012
3013 #[tokio::test]
3014 async fn test_to_curl_binary_body() {
3015 let req = CapturedRequest {
3016 id: "1".into(),
3017 context: None,
3018 method: "POST".into(),
3019 url: "https://example.com".into(),
3020 headers: vec![],
3021 post_data: Some("\x00\x01\x02".into()),
3022 timestamp: 0,
3023 destination: "document".into(),
3024 initiator_type: None,
3025 timing: CapturedTiming::default(),
3026 cookies: vec![],
3027 };
3028 let entry = NetworkEntry {
3029 request: req,
3030 response: None,
3031 error: None,
3032 };
3033 let curl = entry.to_curl();
3034 assert!(curl.contains("-d '"));
3035 }
3036
3037 #[tokio::test]
3038 async fn test_json_body_empty_string() {
3039 let mut req = make_request("1", "POST", "https://a.com");
3040 req.post_data = Some("".into());
3041 assert!(req.json_body().is_none());
3042 }
3043
3044 #[tokio::test]
3045 async fn test_query_params_duplicate_keys() {
3046 let req = make_request("1", "GET", "https://a.com?foo=1&foo=2");
3047 let params = req.query_params().unwrap();
3048 assert_eq!(params.len(), 2);
3049 assert!(params.contains(&("foo".into(), "1".into())));
3050 assert!(params.contains(&("foo".into(), "2".into())));
3051 }
3052
3053 #[tokio::test]
3054 async fn test_request_header_empty_name() {
3055 let req = make_request_with_headers("1", "GET", "https://a.com", vec![]);
3056 assert_eq!(req.request_header(""), None);
3057 }
3058
3059 #[tokio::test]
3060 async fn test_response_header_empty_name() {
3061 let entry = NetworkEntry {
3062 request: make_request("1", "GET", "https://a.com"),
3063 response: Some(make_response("1", 200, "https://a.com")),
3064 error: None,
3065 };
3066 assert_eq!(entry.response_header(""), None);
3067 }
3068
3069 #[tokio::test]
3070 async fn test_remove_by_id_first() {
3071 let log = NetworkLog::new();
3072 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3073 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3074 log.remove_by_id("1").await;
3075 assert_eq!(log.first().await.unwrap().request.id, "2");
3076 }
3077
3078 #[tokio::test]
3079 async fn test_remove_by_id_last() {
3080 let log = NetworkLog::new();
3081 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3082 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3083 log.remove_by_id("2").await;
3084 assert_eq!(log.last().await.unwrap().request.id, "1");
3085 }
3086
3087 #[tokio::test]
3088 async fn test_contains_id_after_eviction() {
3089 let log = NetworkLog::with_limits(4, 10);
3090 for i in 0..10 {
3091 push_request(
3092 &log,
3093 make_request(&format!("{}", i), "GET", "https://example.com"),
3094 )
3095 .await;
3096 }
3097 for i in 0..4 {
3099 assert!(!log.contains_id(&format!("{}", i)).await);
3100 }
3101 for i in 6..10 {
3102 assert!(log.contains_id(&format!("{}", i)).await);
3103 }
3104 }
3105
3106 #[tokio::test]
3107 async fn test_wait_for_response_error_instead_of_response() {
3108 let log = NetworkLog::new();
3109 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3110 let log2 = log.clone();
3111 tokio::spawn(async move {
3112 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3113 let mut inner = log2.inner.write().await;
3114 if let Some(idx) = inner.by_id.get("1").copied() {
3115 let mut new_entry = (*inner.entries[idx]).clone();
3116 new_entry.error = Some(make_error("1", "https://a.com", "net::ERR_ABORTED"));
3117 let new_arc = Arc::new(new_entry);
3118 inner.entries[idx] = new_arc.clone();
3119 let _ = inner.tx.send(new_arc);
3120 }
3121 });
3122 let found = log
3123 .wait_for_response("1", std::time::Duration::from_secs(1))
3124 .await;
3125 assert!(found.unwrap().is_error());
3126 }
3127
3128 #[tokio::test]
3129 async fn test_captured_timing_from_bidi() {
3130 use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3131 let info = FetchTimingInfo {
3132 time_origin: 0.0,
3133 request_time: 10.0,
3134 redirect_start: 0.0,
3135 redirect_end: 0.0,
3136 fetch_start: 10.0,
3137 dns_start: 11.0,
3138 dns_end: 12.0,
3139 connect_start: 12.0,
3140 connect_end: 14.0,
3141 tls_start: 13.0,
3142 request_start: 14.0,
3143 response_start: 15.0,
3144 response_end: 16.0,
3145 };
3146 let t = CapturedTiming::from(&info);
3147 assert_eq!(t.dns_start_ms, Some(1.0)); assert_eq!(t.dns_end_ms, Some(2.0)); assert_eq!(t.connect_start_ms, Some(2.0)); assert_eq!(t.connect_end_ms, Some(4.0)); assert_eq!(t.tls_start_ms, Some(3.0)); assert_eq!(t.response_start_ms, Some(5.0)); assert_eq!(t.response_end_ms, Some(6.0)); }
3155
3156 #[tokio::test]
3157 async fn test_captured_timing_negative_clamped() {
3158 use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3159 let info = FetchTimingInfo {
3160 time_origin: 0.0,
3161 request_time: 10.0,
3162 redirect_start: 0.0,
3163 redirect_end: 0.0,
3164 fetch_start: 10.0,
3165 dns_start: 9.0, dns_end: 8.0, connect_start: 0.0,
3168 connect_end: 0.0,
3169 tls_start: 0.0,
3170 request_start: 0.0,
3171 response_start: 0.0,
3172 response_end: 0.0,
3173 };
3174 let t = CapturedTiming::from(&info);
3175 assert_eq!(t.dns_start_ms, None); assert_eq!(t.dns_end_ms, None);
3177 }
3178
3179 #[tokio::test]
3180 async fn test_har_timings_correct() {
3181 let log = NetworkLog::new();
3182 let mut req = make_request("1", "GET", "https://api.example.com");
3183 req.timing = CapturedTiming {
3184 dns_start_ms: Some(1.0),
3185 dns_end_ms: Some(3.0),
3186 connect_start_ms: Some(3.0),
3187 connect_end_ms: Some(7.0),
3188 tls_start_ms: Some(5.0),
3189 response_start_ms: Some(8.0),
3190 response_end_ms: Some(10.0),
3191 };
3192 push_entry(
3193 &log,
3194 req,
3195 Some(make_response("1", 200, "https://api.example.com")),
3196 None,
3197 )
3198 .await;
3199
3200 let path = std::path::Path::new("/tmp/foxdriver_har_timings.har");
3201 log.save_as_har(path, None).await.unwrap();
3202 let content = std::fs::read_to_string(path).unwrap();
3203 assert!(content.contains("\"dns\": 2.0") || content.contains("\"dns\": 2"));
3205 assert!(content.contains("\"connect\": 4.0") || content.contains("\"connect\": 4"));
3207 assert!(content.contains("\"ssl\": 2.0") || content.contains("\"ssl\": 2"));
3209 }
3210
3211 #[tokio::test]
3212 async fn test_concurrent_ingestion_stress() {
3213 let log = NetworkLog::new();
3214 let mut handles = Vec::new();
3215 for t in 0..20 {
3216 let log = log.clone();
3217 handles.push(tokio::spawn(async move {
3218 for i in 0..50 {
3219 let id = format!("{}-{}", t, i);
3220 log.ingest_before_request_sent(&make_before_request_sent(
3221 &id,
3222 "https://example.com",
3223 "GET",
3224 ))
3225 .await;
3226 if i % 2 == 0 {
3227 log.ingest_response_completed(&make_response_completed(
3228 &id,
3229 "https://example.com",
3230 200,
3231 ))
3232 .await;
3233 } else {
3234 log.ingest_fetch_error(&make_fetch_error(
3235 &id,
3236 "https://example.com",
3237 "err",
3238 ))
3239 .await;
3240 }
3241 }
3242 }));
3243 }
3244 for h in handles {
3245 h.await.unwrap();
3246 }
3247 assert_eq!(log.len().await, 1000);
3248 let completed = log.completed().await;
3249 assert_eq!(completed.len(), 1000); let m = log.metrics().await;
3251 assert_eq!(m.requests_received, 1000);
3252 assert_eq!(m.responses_received, 500);
3253 assert_eq!(m.errors_received, 500);
3254 }
3255
3256 #[tokio::test]
3257 async fn test_property_filter_then_count_matches() {
3258 let log = NetworkLog::new();
3259 for i in 0..100 {
3260 let method = if i % 2 == 0 { "GET" } else { "POST" };
3261 push_request(
3262 &log,
3263 make_request(&format!("{}", i), method, "https://example.com"),
3264 )
3265 .await;
3266 }
3267 let f = Filter::new().method("GET");
3268 let filtered = log.filter(f.clone()).await;
3269 let counted = log.count(f).await;
3270 assert_eq!(filtered.len(), counted);
3271 assert_eq!(counted, 50);
3272 }
3273
3274 #[tokio::test]
3275 async fn test_property_first_last_consistency() {
3276 let log = NetworkLog::new();
3277 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3278 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3279 push_request(&log, make_request("3", "GET", "https://c.com")).await;
3280
3281 assert_eq!(
3282 log.first().await.unwrap().request.id,
3283 log.nth(0).await.unwrap().request.id
3284 );
3285 assert_eq!(
3286 log.last().await.unwrap().request.id,
3287 log.nth(2).await.unwrap().request.id
3288 );
3289 }
3290
3291 #[tokio::test]
3292 async fn test_property_endpoints_is_subset_of_entries() {
3293 let log = NetworkLog::new();
3294 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3295 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
3296 let entries = log.entries().await;
3297 let endpoints = log.endpoints().await;
3298 assert!(endpoints.len() <= entries.len());
3299 for ep in &endpoints {
3300 assert!(entries.iter().any(|e| e.request.url == *ep));
3301 }
3302 }
3303
3304 #[tokio::test]
3305 async fn test_property_hostnames_is_subset_of_endpoints() {
3306 let log = NetworkLog::new();
3307 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3308 push_request(&log, make_request("2", "GET", "https://b.com/y")).await;
3309 let hostnames = log.hostnames().await;
3310 let endpoints = log.endpoints().await;
3311 for h in &hostnames {
3312 assert!(endpoints.iter().any(|e| e.contains(h)));
3313 }
3314 }
3315
3316 #[tokio::test]
3317 async fn test_stress_many_entries() {
3318 let log = NetworkLog::with_limits(100_000, 10_000);
3319 for i in 0..10_000 {
3320 push_request(
3321 &log,
3322 make_request(
3323 &format!("{}", i),
3324 "GET",
3325 &format!("https://example.com/{}", i),
3326 ),
3327 )
3328 .await;
3329 }
3330 assert_eq!(log.len().await, 10_000);
3331 assert!(log.find_by_url("example.com").await.is_some());
3332 assert_eq!(log.distinct_methods().await, vec!["GET"]);
3333 }
3334
3335 #[tokio::test]
3338 async fn test_handler_ignores_non_network_events() {
3339 let log = NetworkLog::new();
3340 let mut handler = make_network_handler(log.clone());
3341 let evt = rustenium_bidi_definitions::Event::Log(
3343 rustenium_bidi_definitions::log::events::LogEvent::EntryAdded(
3344 rustenium_bidi_definitions::log::events::EntryAdded {
3345 method: rustenium_bidi_definitions::log::events::EntryAddedMethod::EntryAdded,
3346 params: rustenium_bidi_definitions::log::events::EntryAddedParams {},
3347 },
3348 ),
3349 );
3350 handler(evt).await;
3351 assert!(log.is_empty().await);
3352 }
3353
3354 #[tokio::test]
3355 async fn test_handler_processes_before_request_sent() {
3356 let log = NetworkLog::new();
3357 let mut handler = make_network_handler(log.clone());
3358 let evt = rustenium_bidi_definitions::Event::Network(
3359 rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3360 make_before_request_sent("req-1", "https://example.com", "GET"),
3361 ),
3362 );
3363 handler(evt).await;
3364 assert_eq!(log.len().await, 1);
3365 assert!(log.contains_id("req-1").await);
3366 }
3367
3368 #[tokio::test]
3369 async fn test_handler_processes_response_completed() {
3370 let log = NetworkLog::new();
3371 let mut handler = make_network_handler(log.clone());
3372 handler(rustenium_bidi_definitions::Event::Network(
3373 rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3374 make_before_request_sent("req-1", "https://example.com", "GET"),
3375 ),
3376 ))
3377 .await;
3378 handler(rustenium_bidi_definitions::Event::Network(
3379 rustenium_bidi_definitions::network::events::NetworkEvent::ResponseCompleted(
3380 make_response_completed("req-1", "https://example.com", 200),
3381 ),
3382 ))
3383 .await;
3384 let entry = log.first().await.unwrap();
3385 assert_eq!(entry.status(), Some(200));
3386 }
3387
3388 #[tokio::test]
3389 async fn test_handler_ignores_unknown_network_events() {
3390 let log = NetworkLog::new();
3391 let mut handler = make_network_handler(log.clone());
3392 handler(rustenium_bidi_definitions::Event::Network(
3394 rustenium_bidi_definitions::network::events::NetworkEvent::ResponseStarted(
3395 rustenium_bidi_definitions::network::events::ResponseStarted {
3396 method: rustenium_bidi_definitions::network::events::ResponseStartedMethod::ResponseStarted,
3397 params: rustenium_bidi_definitions::network::events::ResponseStartedParams {
3398 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(
3399 false, 0u64, make_bidi_request_data("req-1", "https://example.com", "GET"), 0u64
3400 ),
3401 response: rustenium_bidi_definitions::network::types::ResponseData {
3402 url: "https://example.com".into(),
3403 protocol: "h2".into(),
3404 status: 200,
3405 status_text: "OK".into(),
3406 from_cache: false,
3407 headers: vec![],
3408 mime_type: "application/json".into(),
3409 bytes_received: 100,
3410 headers_size: None,
3411 body_size: Some(100),
3412 content: rustenium_bidi_definitions::network::types::ResponseContent::new(100u64),
3413 auth_challenges: None,
3414 },
3415 },
3416 }
3417 )
3418 )).await;
3419 assert!(log.is_empty().await);
3420 }
3421
3422 #[tokio::test]
3425 async fn test_har_with_null_response() {
3426 let log = NetworkLog::new();
3427 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3428 let path = std::path::Path::new("/tmp/foxdriver_har_null_resp.har");
3429 log.save_as_har(path, None).await.unwrap();
3430 let content = std::fs::read_to_string(path).unwrap();
3431 assert!(content.contains("\"response\": null"));
3432 }
3433
3434 #[tokio::test]
3435 async fn test_har_with_error_entry() {
3436 let log = NetworkLog::new();
3437 push_entry(
3438 &log,
3439 make_request("1", "GET", "https://a.com"),
3440 None,
3441 Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
3442 )
3443 .await;
3444 let path = std::path::Path::new("/tmp/foxdriver_har_error.har");
3445 log.save_as_har(path, None).await.unwrap();
3446 let content = std::fs::read_to_string(path).unwrap();
3447 assert!(content.contains("\"response\": null"));
3448 }
3449
3450 #[tokio::test]
3451 async fn test_default_impl() {
3452 let log: NetworkLog = Default::default();
3453 assert!(log.is_empty().await);
3454 let m = log.metrics().await;
3455 assert_eq!(m.max_entries, 50_000);
3456 }
3457
3458 #[tokio::test]
3459 async fn test_clear_does_not_break_broadcast() {
3460 let log = NetworkLog::new();
3461 let mut rx = log.subscribe().await;
3462 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3463 log.clear().await;
3464 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3465 let first = rx.recv().await.unwrap();
3467 assert_eq!(first.request.id, "1");
3468 let second = rx.recv().await.unwrap();
3469 assert_eq!(second.request.id, "2");
3470 }
3471
3472 #[tokio::test]
3473 async fn test_entries_returns_arc_clones() {
3474 let log = NetworkLog::new();
3475 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3476 let e1 = log.entries().await;
3477 let e2 = log.entries().await;
3478 assert!(Arc::ptr_eq(&e1[0], &e2[0]));
3480 }
3481
3482 #[tokio::test]
3483 async fn test_filter_header_only_response() {
3484 let log = NetworkLog::new();
3485 let req = make_request("1", "GET", "https://a.com");
3486 let mut resp = make_response("1", 200, "https://a.com");
3487 resp.headers = vec![CapturedHeader {
3488 name: "X-Resp-Only".into(),
3489 value: "found-me".into(),
3490 }];
3491 push_entry(&log, req, Some(resp), None).await;
3492 let f = log
3494 .filter(Filter::new().header("x-resp-only", "found"))
3495 .await;
3496 assert_eq!(f.len(), 1);
3497 }
3498
3499 #[tokio::test]
3500 async fn test_find_by_url_after_remove() {
3501 let log = NetworkLog::new();
3502 push_request(&log, make_request("1", "GET", "https://a.com/page")).await;
3503 push_request(&log, make_request("2", "GET", "https://b.com/page")).await;
3504 log.remove_by_id("1").await;
3505 assert!(log.find_by_url("a.com").await.is_none());
3506 assert!(log.find_by_url("b.com").await.is_some());
3507 }
3508
3509 #[tokio::test]
3510 async fn test_concurrent_wait_for_url() {
3511 let log = NetworkLog::new();
3512 let log2 = log.clone();
3513 let log3 = log.clone();
3514 let h1 = tokio::spawn(async move {
3515 log2.wait_for_url("target", std::time::Duration::from_secs(1))
3516 .await
3517 });
3518 let h2 = tokio::spawn(async move {
3519 log3.wait_for_url("target", std::time::Duration::from_secs(1))
3520 .await
3521 });
3522 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3523 push_request(&log, make_request("1", "GET", "https://target.com")).await;
3524 let r1 = h1.await.unwrap();
3525 let r2 = h2.await.unwrap();
3526 assert!(r1.is_some());
3527 assert!(r2.is_some());
3528 }
3529
3530 #[tokio::test]
3531 async fn test_build_response_from_cache() {
3532 let log = NetworkLog::new();
3533 let mut resp = make_response("1", 200, "https://a.com");
3534 resp.from_cache = true;
3535 push_entry(
3536 &log,
3537 make_request("1", "GET", "https://a.com"),
3538 Some(resp),
3539 None,
3540 )
3541 .await;
3542 let entry = log.first().await.unwrap();
3543 assert!(entry.response.as_ref().unwrap().from_cache);
3544 }
3545
3546 #[tokio::test]
3547 async fn test_build_response_no_body_size() {
3548 let log = NetworkLog::new();
3549 let mut resp = make_response("1", 204, "https://a.com");
3550 resp.body_size = None;
3551 push_entry(
3552 &log,
3553 make_request("1", "GET", "https://a.com"),
3554 Some(resp),
3555 None,
3556 )
3557 .await;
3558 let entry = log.first().await.unwrap();
3559 assert_eq!(entry.response.as_ref().unwrap().body_size, None);
3560 assert_eq!(log.total_bytes_in().await, 0);
3561 }
3562
3563 #[tokio::test]
3564 async fn test_filter_has_response_and_has_error_same_entry() {
3565 let log = NetworkLog::new();
3566 push_entry(
3567 &log,
3568 make_request("1", "GET", "https://a.com"),
3569 Some(make_response("1", 500, "https://a.com")),
3570 Some(make_error("1", "https://a.com", "err")),
3571 )
3572 .await;
3573 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
3575 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
3577 }
3578
3579 #[tokio::test]
3580 async fn test_nth_after_remove() {
3581 let log = NetworkLog::new();
3582 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3583 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3584 push_request(&log, make_request("3", "GET", "https://c.com")).await;
3585 log.remove_by_id("2").await;
3586 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
3587 assert_eq!(log.nth(1).await.unwrap().request.id, "3");
3588 assert!(log.nth(2).await.is_none());
3589 }
3590
3591 #[tokio::test]
3592 async fn test_pending_response_then_error_then_request() {
3593 let log = NetworkLog::new();
3594 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3596 .await;
3597 log.ingest_fetch_error(&make_fetch_error(
3599 "req-1",
3600 "https://a.com",
3601 "net::ERR_ABORTED",
3602 ))
3603 .await;
3604 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3606 .await;
3607 let entry = log.first().await.unwrap();
3608 assert!(entry.has_response());
3609 assert!(entry.is_error());
3610 }
3611
3612 #[tokio::test]
3613 async fn test_metrics_persist_after_eviction() {
3614 let log = NetworkLog::with_limits(4, 10);
3615 for i in 0..10 {
3616 log.ingest_before_request_sent(&make_before_request_sent(
3617 &format!("{}", i),
3618 "https://a.com",
3619 "GET",
3620 ))
3621 .await;
3622 }
3623 let m = log.metrics().await;
3624 assert_eq!(m.requests_received, 10);
3625 assert_eq!(m.entries_evicted, 6); }
3627
3628 #[tokio::test]
3629 async fn test_curl_strips_accept_encoding() {
3630 let req = CapturedRequest {
3631 id: "1".into(),
3632 context: None,
3633 method: "GET".into(),
3634 url: "https://example.com".into(),
3635 headers: vec![CapturedHeader {
3636 name: "Accept-Encoding".into(),
3637 value: "gzip".into(),
3638 }],
3639 post_data: None,
3640 timestamp: 0,
3641 destination: "document".into(),
3642 initiator_type: None,
3643 timing: CapturedTiming::default(),
3644 cookies: vec![],
3645 };
3646 let entry = NetworkEntry {
3647 request: req,
3648 response: None,
3649 error: None,
3650 };
3651 let curl = entry.to_curl();
3652 assert!(!curl.contains("Accept-Encoding"));
3653 }
3654
3655 #[tokio::test]
3658 async fn test_max_entries_zero_does_not_panic() {
3659 let log = NetworkLog::with_limits(0, 10);
3660 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3661 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3662 assert_eq!(log.len().await, 2);
3664 }
3665
3666 #[tokio::test]
3667 async fn test_max_pending_zero_does_not_hang() {
3668 let log = NetworkLog::with_limits(100, 0);
3669 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3671 .await;
3672 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3673 .await;
3674 assert_eq!(log.len().await, 1);
3675 assert!(log.first().await.unwrap().has_response());
3676 }
3677
3678 #[tokio::test]
3679 async fn test_max_entries_one() {
3680 let log = NetworkLog::with_limits(1, 10);
3681 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3682 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3683 assert_eq!(log.len().await, 1);
3685 assert_eq!(log.last().await.unwrap().request.id, "2");
3686 }
3687
3688 #[tokio::test]
3691 async fn test_ingest_request_with_headers() {
3692 let log = NetworkLog::new();
3693 let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3694 req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3695 name: "X-Test".into(),
3696 value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3697 rustenium_bidi_definitions::network::types::StringValue::new(
3698 rustenium_bidi_definitions::network::types::StringValueType::String,
3699 "hello",
3700 ),
3701 ),
3702 }];
3703 let evt = BeforeRequestSent {
3704 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3705 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3706 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3707 initiator: None,
3708 },
3709 };
3710 log.ingest_before_request_sent(&evt).await;
3711 let entry = log.first().await.unwrap();
3712 assert_eq!(entry.request.headers.len(), 1);
3713 assert_eq!(entry.request.headers[0].name, "X-Test");
3714 assert_eq!(entry.request.headers[0].value, "hello");
3715 }
3716
3717 #[tokio::test]
3718 async fn test_ingest_request_with_base64_header() {
3719 let log = NetworkLog::new();
3720 let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3721 req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3722 name: "X-Binary".into(),
3723 value: rustenium_bidi_definitions::network::types::BytesValue::Base64Value(
3724 rustenium_bidi_definitions::network::types::Base64Value::new(
3725 rustenium_bidi_definitions::network::types::Base64ValueType::Base64,
3726 "SGVsbG8=",
3727 ),
3728 ),
3729 }];
3730 let evt = BeforeRequestSent {
3731 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3732 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3733 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3734 initiator: None,
3735 },
3736 };
3737 log.ingest_before_request_sent(&evt).await;
3738 let entry = log.first().await.unwrap();
3739 assert_eq!(entry.request.headers[0].value, "SGVsbG8=");
3740 }
3741
3742 #[tokio::test]
3743 async fn test_ingest_request_with_context() {
3744 let log = NetworkLog::new();
3745 let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3746 let mut base = rustenium_bidi_definitions::network::types::BaseParameters::new(
3747 false, 0u64, req_data, 0u64,
3748 );
3749 base.context = Some(
3750 rustenium_bidi_definitions::browsing_context::types::BrowsingContext::new("ctx-1"),
3751 );
3752 let evt = BeforeRequestSent {
3753 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3754 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3755 base_parameters: base,
3756 initiator: None,
3757 },
3758 };
3759 log.ingest_before_request_sent(&evt).await;
3760 let entry = log.first().await.unwrap();
3761 assert_eq!(entry.request.context, Some("ctx-1".into()));
3762 }
3763
3764 #[tokio::test]
3765 async fn test_ingest_request_with_initiator() {
3766 let log = NetworkLog::new();
3767 let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3768 let evt = BeforeRequestSent {
3769 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3770 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3771 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3772 initiator: Some(rustenium_bidi_definitions::network::types::Initiator {
3773 column_number: None,
3774 line_number: None,
3775 request: None,
3776 stack_trace: None,
3777 r#type: Some(rustenium_bidi_definitions::network::types::InitiatorType::Script),
3778 }),
3779 },
3780 };
3781 log.ingest_before_request_sent(&evt).await;
3782 let entry = log.first().await.unwrap();
3783 assert_eq!(entry.request.initiator_type, Some("script".into()));
3784 }
3785
3786 #[tokio::test]
3787 async fn test_ingest_response_with_headers() {
3788 let log = NetworkLog::new();
3789 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3790 .await;
3791
3792 let mut resp_data = make_response_completed("req-1", "https://a.com", 200);
3793 resp_data.params.response.headers =
3794 vec![rustenium_bidi_definitions::network::types::Header {
3795 name: "X-Response".into(),
3796 value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3797 rustenium_bidi_definitions::network::types::StringValue::new(
3798 rustenium_bidi_definitions::network::types::StringValueType::String,
3799 "world",
3800 ),
3801 ),
3802 }];
3803 log.ingest_response_completed(&resp_data).await;
3804
3805 let entry = log.first().await.unwrap();
3806 assert_eq!(entry.response.as_ref().unwrap().headers.len(), 1);
3807 assert_eq!(
3808 entry.response.as_ref().unwrap().headers[0].name,
3809 "X-Response"
3810 );
3811 assert_eq!(entry.response.as_ref().unwrap().headers[0].value, "world");
3812 }
3813
3814 #[tokio::test]
3815 async fn test_ingest_response_from_cache() {
3816 let log = NetworkLog::new();
3817 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3818 .await;
3819 let mut resp = make_response_completed("req-1", "https://a.com", 200);
3820 resp.params.response.from_cache = true;
3821 log.ingest_response_completed(&resp).await;
3822 let entry = log.first().await.unwrap();
3823 assert!(entry.response.as_ref().unwrap().from_cache);
3824 }
3825
3826 #[tokio::test]
3829 async fn test_first_on_empty_log() {
3830 let log = NetworkLog::new();
3831 assert!(log.first().await.is_none());
3832 }
3833
3834 #[tokio::test]
3835 async fn test_last_on_empty_log() {
3836 let log = NetworkLog::new();
3837 assert!(log.last().await.is_none());
3838 }
3839
3840 #[tokio::test]
3841 async fn test_nth_on_empty_log() {
3842 let log = NetworkLog::new();
3843 assert!(log.nth(0).await.is_none());
3844 }
3845
3846 #[tokio::test]
3847 async fn test_find_by_url_on_empty_log() {
3848 let log = NetworkLog::new();
3849 assert!(log.find_by_url("anything").await.is_none());
3850 }
3851
3852 #[tokio::test]
3853 async fn test_filter_on_empty_log() {
3854 let log = NetworkLog::new();
3855 assert!(log.filter(Filter::new()).await.is_empty());
3856 }
3857
3858 #[tokio::test]
3859 async fn test_count_on_empty_log() {
3860 let log = NetworkLog::new();
3861 assert_eq!(log.count(Filter::new()).await, 0);
3862 }
3863
3864 #[tokio::test]
3865 async fn test_completed_on_empty_log() {
3866 let log = NetworkLog::new();
3867 assert!(log.completed().await.is_empty());
3868 }
3869
3870 #[tokio::test]
3871 async fn test_endpoints_on_empty_log() {
3872 let log = NetworkLog::new();
3873 assert!(log.endpoints().await.is_empty());
3874 }
3875
3876 #[tokio::test]
3877 async fn test_hostnames_on_empty_log() {
3878 let log = NetworkLog::new();
3879 assert!(log.hostnames().await.is_empty());
3880 }
3881
3882 #[tokio::test]
3883 async fn test_distinct_methods_on_empty_log() {
3884 let log = NetworkLog::new();
3885 assert!(log.distinct_methods().await.is_empty());
3886 }
3887
3888 #[tokio::test]
3889 async fn test_distinct_statuses_on_empty_log() {
3890 let log = NetworkLog::new();
3891 assert!(log.distinct_statuses().await.is_empty());
3892 }
3893
3894 #[tokio::test]
3895 async fn test_total_bytes_on_empty_log() {
3896 let log = NetworkLog::new();
3897 assert_eq!(log.total_bytes_in().await, 0);
3898 assert_eq!(log.total_bytes_out().await, 0);
3899 }
3900
3901 #[tokio::test]
3902 async fn test_contains_id_on_empty_log() {
3903 let log = NetworkLog::new();
3904 assert!(!log.contains_id("anything").await);
3905 }
3906
3907 #[tokio::test]
3908 async fn test_remove_by_id_on_empty_log() {
3909 let log = NetworkLog::new();
3910 assert!(log.remove_by_id("anything").await.is_none());
3911 }
3912
3913 #[tokio::test]
3914 async fn test_clear_on_empty_log() {
3915 let log = NetworkLog::new();
3916 log.clear().await;
3917 assert!(log.is_empty().await);
3918 }
3919
3920 #[tokio::test]
3921 async fn test_subscribe_on_empty_log() {
3922 let log = NetworkLog::new();
3923 let mut rx = log.subscribe().await;
3924 assert!(rx.try_recv().is_err());
3925 }
3926
3927 #[tokio::test]
3930 async fn test_remove_by_id_twice() {
3931 let log = NetworkLog::new();
3932 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3933 assert!(log.remove_by_id("1").await.is_some());
3934 assert!(log.remove_by_id("1").await.is_none());
3935 }
3936
3937 #[tokio::test]
3938 async fn test_remove_by_id_only_entry() {
3939 let log = NetworkLog::new();
3940 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3941 log.remove_by_id("1").await;
3942 assert!(log.is_empty().await);
3943 assert!(!log.contains_id("1").await);
3944 }
3945
3946 #[tokio::test]
3947 async fn test_concurrent_remove_by_id() {
3948 let log = NetworkLog::new();
3949 for i in 0..100 {
3950 push_request(
3951 &log,
3952 make_request(&format!("{}", i), "GET", "https://a.com"),
3953 )
3954 .await;
3955 }
3956 let mut handles = Vec::new();
3957 for i in 0..100 {
3958 let log = log.clone();
3959 handles.push(tokio::spawn(async move {
3960 log.remove_by_id(&format!("{}", i)).await
3961 }));
3962 }
3963 for h in handles {
3964 h.await.unwrap();
3965 }
3966 assert!(log.is_empty().await);
3967 }
3968
3969 #[tokio::test]
3972 async fn test_wait_for_url_empty_substring() {
3973 let log = NetworkLog::new();
3974 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3975 let found = log
3976 .wait_for_url("", std::time::Duration::from_secs(1))
3977 .await;
3978 assert!(found.is_some());
3979 }
3980
3981 #[tokio::test]
3982 async fn test_wait_for_response_nonexistent_id() {
3983 let log = NetworkLog::new();
3984 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3985 let found = log
3986 .wait_for_response("does-not-exist", std::time::Duration::from_millis(50))
3987 .await;
3988 assert!(found.is_none());
3989 }
3990
3991 #[tokio::test]
3994 async fn test_json_body_array() {
3995 let mut req = make_request("1", "POST", "https://a.com");
3996 req.post_data = Some("[1, 2, 3]".into());
3997 assert_eq!(req.json_body(), Some(serde_json::json!([1, 2, 3])));
3998 }
3999
4000 #[tokio::test]
4001 async fn test_json_body_nested() {
4002 let mut req = make_request("1", "POST", "https://a.com");
4003 req.post_data = Some(r#"{"outer": {"inner": [true, false]}}"#.into());
4004 assert_eq!(
4005 req.json_body(),
4006 Some(serde_json::json!({"outer": {"inner": [true, false]}}))
4007 );
4008 }
4009
4010 #[tokio::test]
4013 async fn test_to_curl_delete_method() {
4014 let req = make_request("1", "DELETE", "https://api.example.com/resource/1");
4015 let entry = NetworkEntry {
4016 request: req,
4017 response: None,
4018 error: None,
4019 };
4020 assert!(entry.to_curl().starts_with("curl -X 'DELETE'"));
4021 }
4022
4023 #[tokio::test]
4024 async fn test_to_curl_url_with_single_quotes() {
4025 let req = make_request("1", "GET", "https://example.com?foo='bar'");
4026 let entry = NetworkEntry {
4027 request: req,
4028 response: None,
4029 error: None,
4030 };
4031 let curl = entry.to_curl();
4032 assert!(curl.contains("'https://example.com?foo='\\''bar'\\'''"));
4033 }
4034
4035 #[tokio::test]
4036 async fn test_to_curl_header_with_single_quotes() {
4037 let req = make_request_with_headers(
4038 "1",
4039 "GET",
4040 "https://example.com",
4041 vec![CapturedHeader {
4042 name: "X-Quote".into(),
4043 value: "it's working".into(),
4044 }],
4045 );
4046 let entry = NetworkEntry {
4047 request: req,
4048 response: None,
4049 error: None,
4050 };
4051 let curl = entry.to_curl();
4052 assert!(curl.contains("-H 'X-Quote: it'\\''s working'"));
4053 }
4054
4055 #[tokio::test]
4058 async fn test_network_metrics_roundtrip() {
4059 let m = NetworkMetrics {
4060 requests_received: 10,
4061 responses_received: 8,
4062 errors_received: 2,
4063 entries_evicted: 5,
4064 pending_responses_dropped: 1,
4065 pending_errors_dropped: 0,
4066 broadcast_drops: 3,
4067 duplicate_responses: 1,
4068 duplicate_errors: 0,
4069 max_entries: 100,
4070 };
4071 let json = serde_json::to_string(&m).unwrap();
4072 let de: NetworkMetrics = serde_json::from_str(&json).unwrap();
4073 assert_eq!(de, m);
4074 }
4075
4076 #[tokio::test]
4079 async fn test_find_by_url_unicode() {
4080 let log = NetworkLog::new();
4081 push_request(&log, make_request("1", "GET", "https://例子.com/path")).await;
4082 assert!(log.find_by_url("例子").await.is_some());
4083 }
4084
4085 #[tokio::test]
4086 async fn test_hostnames_idn() {
4087 let log = NetworkLog::new();
4088 push_request(
4089 &log,
4090 make_request("1", "GET", "https://xn--fsq092h.com/path"),
4091 )
4092 .await;
4093 let h = log.hostnames().await;
4094 assert_eq!(h.len(), 1);
4095 assert!(h.contains(&"xn--fsq092h.com".into()));
4096 }
4097
4098 #[tokio::test]
4101 async fn test_total_bytes_out_multibyte() {
4102 let log = NetworkLog::new();
4103 let mut req = make_request("1", "POST", "https://a.com");
4104 req.post_data = Some("日本語".into()); push_entry(&log, req, None, None).await;
4106 assert_eq!(log.total_bytes_out().await, 9);
4107 }
4108
4109 #[tokio::test]
4112 async fn test_completed_only_errors() {
4113 let log = NetworkLog::new();
4114 push_entry(
4115 &log,
4116 make_request("1", "GET", "https://a.com"),
4117 None,
4118 Some(make_error("1", "https://a.com", "err")),
4119 )
4120 .await;
4121 push_entry(
4122 &log,
4123 make_request("2", "GET", "https://b.com"),
4124 None,
4125 Some(make_error("2", "https://b.com", "err")),
4126 )
4127 .await;
4128 let c = log.completed().await;
4129 assert_eq!(c.len(), 2);
4130 }
4131
4132 #[tokio::test]
4135 async fn test_distinct_statuses_all_same() {
4136 let log = NetworkLog::new();
4137 for i in 0..5 {
4138 push_entry(
4139 &log,
4140 make_request(&format!("{}", i), "GET", "https://a.com"),
4141 Some(make_response(&format!("{}", i), 200, "https://a.com")),
4142 None,
4143 )
4144 .await;
4145 }
4146 assert_eq!(log.distinct_statuses().await, vec![200]);
4147 }
4148
4149 #[tokio::test]
4150 async fn test_distinct_methods_all_same() {
4151 let log = NetworkLog::new();
4152 for i in 0..5 {
4153 push_request(
4154 &log,
4155 make_request(&format!("{}", i), "POST", "https://a.com"),
4156 )
4157 .await;
4158 }
4159 assert_eq!(log.distinct_methods().await, vec!["POST"]);
4160 }
4161
4162 #[tokio::test]
4165 async fn test_clear_subscribe_push() {
4166 let log = NetworkLog::new();
4167 log.clear().await;
4168 let mut rx = log.subscribe().await;
4169 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4170 assert_eq!(rx.recv().await.unwrap().request.id, "1");
4171 }
4172
4173 #[tokio::test]
4176 async fn test_total_bytes_in_from_cache() {
4177 let log = NetworkLog::new();
4178 let mut resp = make_response("1", 200, "https://a.com");
4179 resp.from_cache = true;
4180 resp.body_size = Some(500);
4181 push_entry(
4182 &log,
4183 make_request("1", "GET", "https://a.com"),
4184 Some(resp),
4185 None,
4186 )
4187 .await;
4188 assert_eq!(log.total_bytes_in().await, 500);
4189 }
4190
4191 #[tokio::test]
4194 async fn test_to_curl_produces_valid_shell_with_quotes() {
4195 let req = CapturedRequest {
4196 id: "1".into(),
4197 context: None,
4198 method: "POST".into(),
4199 url: "https://example.com".into(),
4200 headers: vec![CapturedHeader {
4201 name: "X-Quote".into(),
4202 value: "it's".into(),
4203 }],
4204 post_data: Some("data='val'".into()),
4205 timestamp: 0,
4206 destination: "document".into(),
4207 initiator_type: None,
4208 timing: CapturedTiming::default(),
4209 cookies: vec![],
4210 };
4211 let entry = NetworkEntry {
4212 request: req,
4213 response: None,
4214 error: None,
4215 };
4216 let curl = entry.to_curl();
4217 assert!(curl.contains("it'\\''s"));
4221 assert!(curl.contains("data='\\''val'\\'''"));
4222 }
4223
4224 #[tokio::test]
4225 async fn test_save_as_har_postdata_uses_content_type_header() {
4226 let log = NetworkLog::new();
4227 let mut req = make_request("1", "POST", "https://api.example.com");
4228 req.post_data = Some(r#"{"key":"value"}"#.into());
4229 req.headers = vec![CapturedHeader {
4230 name: "Content-Type".into(),
4231 value: "application/vnd.api+json".into(),
4232 }];
4233 push_entry(
4234 &log,
4235 req,
4236 Some(make_response("1", 201, "https://api.example.com")),
4237 None,
4238 )
4239 .await;
4240
4241 let path = std::path::Path::new("/tmp/foxdriver_har_mime.har");
4242 log.save_as_har(path, None).await.unwrap();
4243 let content = std::fs::read_to_string(path).unwrap();
4244 assert!(content.contains("\"mimeType\": \"application/vnd.api+json\""));
4246 }
4247
4248 #[tokio::test]
4249 async fn test_save_as_har_postdata_defaults_octet_stream() {
4250 let log = NetworkLog::new();
4251 let mut req = make_request("1", "POST", "https://api.example.com");
4252 req.post_data = Some("raw bytes".into());
4253 push_entry(
4255 &log,
4256 req,
4257 Some(make_response("1", 201, "https://api.example.com")),
4258 None,
4259 )
4260 .await;
4261
4262 let path = std::path::Path::new("/tmp/foxdriver_har_default_mime.har");
4263 log.save_as_har(path, None).await.unwrap();
4264 let content = std::fs::read_to_string(path).unwrap();
4265 assert!(content.contains("\"mimeType\": \"application/octet-stream\""));
4266 }
4267
4268 #[tokio::test]
4269 async fn test_concurrent_reads_with_rwlock() {
4270 let log = NetworkLog::new();
4271 for i in 0..100 {
4272 push_request(
4273 &log,
4274 make_request(&format!("{}", i), "GET", "https://example.com"),
4275 )
4276 .await;
4277 }
4278 let mut handles = Vec::new();
4279 for _ in 0..20 {
4280 let log = log.clone();
4281 handles.push(tokio::spawn(async move {
4282 for _ in 0..50 {
4283 let _ = log.len().await;
4284 let _ = log.entries().await;
4285 let _ = log.distinct_methods().await;
4286 let _ = log.total_bytes_in().await;
4287 }
4288 }));
4289 }
4290 for h in handles {
4291 h.await.unwrap();
4292 }
4293 assert_eq!(log.len().await, 100);
4295 }
4296
4297 #[tokio::test]
4302 async fn test_find_by_status() {
4303 let log = NetworkLog::new();
4304 push_entry(
4305 &log,
4306 make_request("1", "GET", "https://a.com"),
4307 Some(make_response("1", 200, "https://a.com")),
4308 None,
4309 )
4310 .await;
4311 push_entry(
4312 &log,
4313 make_request("2", "GET", "https://b.com"),
4314 Some(make_response("2", 404, "https://b.com")),
4315 None,
4316 )
4317 .await;
4318 push_entry(
4319 &log,
4320 make_request("3", "GET", "https://c.com"),
4321 None,
4322 Some(make_error("3", "https://c.com", "err")),
4323 )
4324 .await;
4325 assert_eq!(log.find_by_status(200).await.len(), 1);
4326 assert_eq!(log.find_by_status(404).await.len(), 1);
4327 assert_eq!(log.find_by_status(500).await.len(), 0);
4328 }
4329
4330 #[tokio::test]
4331 async fn test_entries_since() {
4332 let log = NetworkLog::new();
4333 let mut req1 = make_request("1", "GET", "https://a.com");
4334 req1.timestamp = 1000;
4335 let mut req2 = make_request("2", "GET", "https://b.com");
4336 req2.timestamp = 2000;
4337 let mut req3 = make_request("3", "GET", "https://c.com");
4338 req3.timestamp = 3000;
4339 push_request(&log, req1).await;
4340 push_request(&log, req2).await;
4341 push_request(&log, req3).await;
4342 assert_eq!(log.entries_since(0).await.len(), 3);
4343 assert_eq!(log.entries_since(2000).await.len(), 2);
4344 assert_eq!(log.entries_since(3001).await.len(), 0);
4345 }
4346
4347 #[tokio::test]
4348 async fn test_last_n() {
4349 let log = NetworkLog::new();
4350 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4351 push_request(&log, make_request("2", "GET", "https://b.com")).await;
4352 push_request(&log, make_request("3", "GET", "https://c.com")).await;
4353 let last = log.last_n(2).await;
4354 assert_eq!(last.len(), 2);
4355 assert_eq!(last[0].request.id, "2");
4356 assert_eq!(last[1].request.id, "3");
4357 assert_eq!(log.last_n(10).await.len(), 3);
4358 assert!(log.last_n(0).await.is_empty());
4359 }
4360
4361 #[tokio::test]
4366 async fn test_unique_urls() {
4367 let log = NetworkLog::new();
4368 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4369 push_request(&log, make_request("2", "GET", "https://b.com")).await;
4370 push_request(&log, make_request("3", "GET", "https://a.com")).await;
4371 let urls = log.unique_urls().await;
4372 assert_eq!(urls.len(), 2);
4373 assert!(urls.contains(&"https://a.com".to_string()));
4374 assert!(urls.contains(&"https://b.com".to_string()));
4375 }
4376
4377 #[tokio::test]
4378 async fn test_unique_urls_empty() {
4379 let log = NetworkLog::new();
4380 assert!(log.unique_urls().await.is_empty());
4381 }
4382
4383 #[tokio::test]
4386 async fn test_broadcast_drops_tracked_when_no_receivers() {
4387 let log = NetworkLog::new();
4388 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4390 let m = log.metrics().await;
4391 assert_eq!(m.broadcast_drops, 1);
4392 }
4393
4394 #[tokio::test]
4395 async fn test_broadcast_no_drop_when_receiver_active() {
4396 let log = NetworkLog::new();
4397 let _rx = log.subscribe().await;
4398 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4399 let m = log.metrics().await;
4400 assert_eq!(m.broadcast_drops, 0);
4401 }
4402
4403 #[tokio::test]
4404 async fn test_pending_count_and_drain() {
4405 let log = NetworkLog::new();
4406 {
4408 let mut inner = log.inner.write().await;
4409 inner.pending_responses.insert(
4410 "orphan-1".into(),
4411 make_response("orphan-1", 200, "https://a.com"),
4412 );
4413 inner.pending_errors.insert(
4414 "orphan-2".into(),
4415 make_error("orphan-2", "https://b.com", "err"),
4416 );
4417 }
4418 assert_eq!(log.pending_count().await, 2);
4419
4420 let (responses, errors) = log.drain_pending().await;
4421 assert_eq!(responses.len(), 1);
4422 assert_eq!(errors.len(), 1);
4423 assert_eq!(log.pending_count().await, 0);
4424 }
4425
4426 #[tokio::test]
4427 async fn test_pending_count_empty() {
4428 let log = NetworkLog::new();
4429 assert_eq!(log.pending_count().await, 0);
4430 let (r, e) = log.drain_pending().await;
4431 assert!(r.is_empty() && e.is_empty());
4432 }
4433
4434 #[tokio::test]
4435 async fn test_retain_filters_entries() {
4436 let log = NetworkLog::new();
4437 push_request(&log, make_request("1", "GET", "https://keep.com")).await;
4438 push_request(&log, make_request("2", "GET", "https://drop.com")).await;
4439 push_request(&log, make_request("3", "GET", "https://keep.com/path")).await;
4440 log.retain(|e| e.request.url.contains("keep")).await;
4441 assert_eq!(log.len().await, 2);
4442 assert!(log.contains_id("1").await);
4443 assert!(!log.contains_id("2").await);
4444 assert!(log.contains_id("3").await);
4445 }
4446
4447 #[tokio::test]
4448 async fn test_retain_rebuilds_indices() {
4449 let log = NetworkLog::new();
4450 for i in 0..5 {
4451 push_request(
4452 &log,
4453 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4454 )
4455 .await;
4456 }
4457 log.retain(|e| e.request.id.parse::<i32>().unwrap() % 2 == 0)
4458 .await;
4459 assert_eq!(log.len().await, 3);
4460 assert!(log.contains_id("0").await);
4461 assert!(log.contains_id("2").await);
4462 assert!(log.contains_id("4").await);
4463 }
4464
4465 #[tokio::test]
4466 async fn test_deduplication_guard_ignores_duplicate_request_id() {
4467 let log = NetworkLog::new();
4468 push_request(&log, make_request("1", "GET", "https://first.com")).await;
4469 push_request(&log, make_request("1", "GET", "https://second.com")).await;
4470 assert_eq!(log.len().await, 1);
4471 assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
4472 }
4473
4474 #[tokio::test]
4475 async fn test_duplicate_response_tracked_in_metrics() {
4476 let log = NetworkLog::new();
4477 push_entry(
4478 &log,
4479 make_request("1", "GET", "https://a.com"),
4480 Some(make_response("1", 200, "https://a.com")),
4481 None,
4482 )
4483 .await;
4484 {
4486 let mut inner = log.inner.write().await;
4487 if let Some(idx) = inner.by_id.get("1").copied() {
4488 let mut new_entry = (*inner.entries[idx]).clone();
4489 new_entry.response = Some(make_response("1", 201, "https://a.com"));
4490 inner.entries[idx] = Arc::new(new_entry);
4491 }
4492 }
4493 let m = log.metrics().await;
4496 assert_eq!(m.duplicate_responses, 0); }
4498
4499 #[tokio::test]
4500 async fn test_request_ids_returns_all_ids() {
4501 let log = NetworkLog::new();
4502 push_request(&log, make_request("a", "GET", "https://a.com")).await;
4503 push_request(&log, make_request("b", "GET", "https://b.com")).await;
4504 let ids = log.request_ids().await;
4505 assert_eq!(ids, vec!["a", "b"]);
4506 }
4507
4508 #[tokio::test]
4509 async fn test_has_response_and_has_error() {
4510 let log = NetworkLog::new();
4511 push_entry(
4512 &log,
4513 make_request("1", "GET", "https://a.com"),
4514 Some(make_response("1", 200, "https://a.com")),
4515 None,
4516 )
4517 .await;
4518 push_entry(
4519 &log,
4520 make_request("2", "GET", "https://b.com"),
4521 None,
4522 Some(make_error("2", "https://b.com", "err")),
4523 )
4524 .await;
4525 push_request(&log, make_request("3", "GET", "https://c.com")).await;
4526 assert!(log.has_response("1").await);
4527 assert!(!log.has_error("1").await);
4528 assert!(!log.has_response("2").await);
4529 assert!(log.has_error("2").await);
4530 assert!(!log.has_response("3").await);
4531 assert!(!log.has_error("3").await);
4532 assert!(!log.has_response("missing").await);
4533 assert!(!log.has_error("missing").await);
4534 }
4535
4536 #[tokio::test]
4539 async fn test_invariant_len_equals_entries_len() {
4540 let log = NetworkLog::new();
4541 for i in 0..50 {
4542 push_request(
4543 &log,
4544 make_request(&format!("{}", i), "GET", "https://x.com"),
4545 )
4546 .await;
4547 assert_eq!(log.len().await, log.entries().await.len());
4548 }
4549 }
4550
4551 #[tokio::test]
4552 async fn test_invariant_is_empty_iff_len_zero() {
4553 let log = NetworkLog::new();
4554 assert_eq!(log.is_empty().await, log.len().await == 0);
4555 push_request(&log, make_request("1", "GET", "https://x.com")).await;
4556 assert_eq!(log.is_empty().await, log.len().await == 0);
4557 log.clear().await;
4558 assert_eq!(log.is_empty().await, log.len().await == 0);
4559 }
4560
4561 #[tokio::test]
4562 async fn test_invariant_by_id_indices_are_valid() {
4563 let log = NetworkLog::new();
4564 for i in 0..100 {
4565 push_request(
4566 &log,
4567 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4568 )
4569 .await;
4570 }
4571 let inner = log.inner.read().await;
4572 for (id, idx) in &inner.by_id {
4573 assert!(
4574 *idx < inner.entries.len(),
4575 "id {} has invalid index {}",
4576 id,
4577 idx
4578 );
4579 assert_eq!(inner.entries[*idx].request.id, *id);
4580 }
4581 }
4582
4583 #[tokio::test]
4584 async fn test_invariant_no_duplicate_ids() {
4585 let log = NetworkLog::new();
4586 for i in 0..100 {
4587 push_request(
4588 &log,
4589 make_request(&format!("{}", i), "GET", "https://x.com"),
4590 )
4591 .await;
4592 }
4593 let inner = log.inner.read().await;
4594 let mut seen = std::collections::HashSet::new();
4595 for e in &inner.entries {
4596 assert!(seen.insert(&e.request.id), "duplicate id {}", e.request.id);
4597 }
4598 }
4599
4600 #[tokio::test]
4601 async fn test_invariant_first_and_last_consistent() {
4602 let log = NetworkLog::new();
4603 assert_eq!(log.first().await, None);
4604 assert_eq!(log.last().await, None);
4605 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4606 push_request(&log, make_request("2", "GET", "https://b.com")).await;
4607 let entries = log.entries().await;
4608 assert_eq!(
4609 log.first().await.as_deref(),
4610 entries.first().map(Arc::as_ref)
4611 );
4612 assert_eq!(log.last().await.as_deref(), entries.last().map(Arc::as_ref));
4613 }
4614
4615 #[tokio::test]
4616 async fn test_invariant_filter_count_consistent() {
4617 let log = NetworkLog::new();
4618 for i in 0..20 {
4619 let status = if i % 2 == 0 { 200 } else { 404 };
4620 push_entry(
4621 &log,
4622 make_request(&format!("{}", i), "GET", "https://x.com"),
4623 Some(make_response(&format!("{}", i), status, "https://x.com")),
4624 None,
4625 )
4626 .await;
4627 }
4628 let f = Filter::new().status_range(200..=200);
4629 let filtered = log.filter(f.clone()).await;
4630 let count = log.count(f).await;
4631 assert_eq!(filtered.len(), count);
4632 }
4633
4634 #[tokio::test]
4635 async fn test_invariant_metrics_monotonic() {
4636 let log = NetworkLog::new();
4637 let m0 = log.metrics().await;
4638 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4639 let m1 = log.metrics().await;
4640 assert!(m1.requests_received >= m0.requests_received);
4641 assert!(m1.responses_received >= m0.responses_received);
4642 assert!(m1.errors_received >= m0.errors_received);
4643 }
4644
4645 #[tokio::test]
4646 async fn test_invariant_pending_count_bounded() {
4647 let log = NetworkLog::with_limits(100, 10);
4648 {
4650 let mut inner = log.inner.write().await;
4651 for i in 0..20 {
4652 inner.pending_responses.insert(
4653 format!("orphan-{}", i),
4654 make_response(&format!("{}", i), 200, "https://x.com"),
4655 );
4656 }
4657 }
4658 assert_eq!(log.pending_count().await, 20);
4660 let (r, _) = log.drain_pending().await;
4661 assert_eq!(r.len(), 20);
4662 assert_eq!(log.pending_count().await, 0);
4663 }
4664
4665 #[tokio::test]
4666 async fn test_invariant_after_clear_all_empty() {
4667 let log = NetworkLog::new();
4668 for i in 0..50 {
4669 push_request(
4670 &log,
4671 make_request(&format!("{}", i), "GET", "https://x.com"),
4672 )
4673 .await;
4674 }
4675 log.clear().await;
4676 assert!(log.is_empty().await);
4677 assert_eq!(log.pending_count().await, 0);
4678 assert!(!log.contains_id("1").await);
4679 let inner = log.inner.read().await;
4680 assert!(inner.by_id.is_empty());
4681 }
4682
4683 #[tokio::test]
4684 async fn test_invariant_after_remove_id_not_present() {
4685 let log = NetworkLog::new();
4686 for i in 0..50 {
4687 push_request(
4688 &log,
4689 make_request(&format!("{}", i), "GET", "https://x.com"),
4690 )
4691 .await;
4692 }
4693 log.remove_by_id("25").await;
4694 assert!(!log.contains_id("25").await);
4695 let inner = log.inner.read().await;
4696 assert!(!inner.by_id.contains_key("25"));
4697 assert_eq!(inner.entries.len(), 49);
4698 for (id, idx) in &inner.by_id {
4700 assert!(inner.entries[*idx].request.id == *id);
4701 }
4702 }
4703
4704 #[tokio::test]
4705 async fn test_invariant_retain_all_satisfy_predicate() {
4706 let log = NetworkLog::new();
4707 for i in 0..50 {
4708 push_request(
4709 &log,
4710 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 3)),
4711 )
4712 .await;
4713 }
4714 log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4715 .await;
4716 for e in log.entries().await {
4717 assert!(!e.request.url.contains("2.com"));
4718 }
4719 assert_eq!(log.len().await, 34); }
4721
4722 #[tokio::test]
4723 async fn test_invariant_nth_matches_entries_index() {
4724 let log = NetworkLog::new();
4725 for i in 0..20 {
4726 push_request(
4727 &log,
4728 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4729 )
4730 .await;
4731 }
4732 let entries = log.entries().await;
4733 for (i, entry) in entries.iter().enumerate() {
4734 assert_eq!(log.nth(i).await.as_deref(), Some(entry.as_ref()));
4735 }
4736 assert!(log.nth(100).await.is_none());
4737 }
4738
4739 #[tokio::test]
4740 async fn test_invariant_last_n_order_preserved() {
4741 let log = NetworkLog::new();
4742 for i in 0..10 {
4743 push_request(
4744 &log,
4745 make_request(&format!("{}", i), "GET", "https://x.com"),
4746 )
4747 .await;
4748 }
4749 let last = log.last_n(3).await;
4750 assert_eq!(last.len(), 3);
4751 assert_eq!(last[0].request.id, "7");
4752 assert_eq!(last[1].request.id, "8");
4753 assert_eq!(last[2].request.id, "9");
4754 }
4755
4756 #[tokio::test]
4757 async fn test_invariant_entries_since_time_based() {
4758 let log = NetworkLog::new();
4759 for i in 0..10 {
4760 let mut req = make_request(&format!("{}", i), "GET", "https://x.com");
4761 req.timestamp = i as u64 * 1000;
4762 push_request(&log, req).await;
4763 }
4764 let since = log.entries_since(5000).await;
4765 assert_eq!(since.len(), 5); for e in &since {
4767 assert!(e.request.timestamp >= 5000);
4768 }
4769 }
4770
4771 #[tokio::test]
4774 async fn test_stress_concurrent_ingestion_and_query() {
4775 let log = NetworkLog::new();
4776 let mut handles = Vec::new();
4777
4778 for t in 0..10 {
4780 let log = log.clone();
4781 handles.push(tokio::spawn(async move {
4782 for i in 0..100 {
4783 let id = format!("t{}-{}", t, i);
4784 push_request(&log, make_request(&id, "GET", "https://x.com")).await;
4785 }
4786 }));
4787 }
4788
4789 for _ in 0..10 {
4791 let log = log.clone();
4792 handles.push(tokio::spawn(async move {
4793 for _ in 0..100 {
4794 let _ = log.len().await;
4795 let _ = log.entries().await;
4796 let _ = log.first().await;
4797 let _ = log.last().await;
4798 let _ = log.metrics().await;
4799 tokio::task::yield_now().await;
4800 }
4801 }));
4802 }
4803
4804 for h in handles {
4805 h.await.unwrap();
4806 }
4807
4808 assert_eq!(log.len().await, 1000);
4809 let inner = log.inner.read().await;
4810 assert_eq!(inner.by_id.len(), 1000);
4811 }
4812
4813 #[tokio::test]
4814 async fn test_stress_concurrent_subscribers() {
4815 let log = NetworkLog::new();
4816 let mut rxs = Vec::new();
4817 for _ in 0..50 {
4818 rxs.push(log.subscribe().await);
4819 }
4820
4821 for i in 0..100 {
4822 push_request(
4823 &log,
4824 make_request(&format!("{}", i), "GET", "https://x.com"),
4825 )
4826 .await;
4827 }
4828
4829 for mut rx in rxs {
4832 let mut count = 0;
4833 while rx.try_recv().is_ok() {
4834 count += 1;
4835 }
4836 assert!(count <= 100);
4837 }
4838 }
4839
4840 #[tokio::test]
4841 async fn test_stress_eviction_under_load() {
4842 let log = NetworkLog::with_limits(100, 10);
4843 for i in 0..500 {
4844 push_request(
4845 &log,
4846 make_request(&format!("{}", i), "GET", "https://x.com"),
4847 )
4848 .await;
4849 }
4850 assert_eq!(log.len().await, 100);
4851 let m = log.metrics().await;
4852 assert!(m.entries_evicted > 0);
4853 }
4854
4855 #[tokio::test]
4856 async fn test_stress_retain_under_load() {
4857 let log = NetworkLog::new();
4858 for i in 0..1000 {
4859 push_request(
4860 &log,
4861 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 10)),
4862 )
4863 .await;
4864 }
4865 log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4866 .await;
4867 assert_eq!(log.len().await, 200);
4868 let inner = log.inner.read().await;
4869 assert_eq!(inner.by_id.len(), 200);
4870 }
4871
4872 #[tokio::test]
4873 async fn test_stress_memory_estimate_grows_with_entries() {
4874 let log = NetworkLog::new();
4875 let m0 = log.memory_estimate().await;
4876 for i in 0..100 {
4877 let mut req = make_request(&format!("{}", i), "POST", "https://x.com");
4878 req.post_data = Some("x".repeat(1000));
4879 push_request(&log, req).await;
4880 }
4881 let m1 = log.memory_estimate().await;
4882 assert!(m1 > m0);
4883 }
4884
4885 #[tokio::test]
4888 async fn test_har_is_valid_json() {
4889 let log = NetworkLog::new();
4890 push_entry(
4891 &log,
4892 make_request("1", "GET", "https://a.com"),
4893 Some(make_response("1", 200, "https://a.com")),
4894 None,
4895 )
4896 .await;
4897 let path = std::path::Path::new("/tmp/foxdriver_har_valid.json");
4898 log.save_as_har(path, Some("test")).await.unwrap();
4899 let content = std::fs::read_to_string(path).unwrap();
4900 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4901 assert_eq!(parsed["log"]["version"], "1.2");
4902 assert!(parsed["log"]["entries"].is_array());
4903 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 1);
4904 }
4905
4906 #[tokio::test]
4907 async fn test_har_has_required_fields() {
4908 let log = NetworkLog::new();
4909 push_entry(
4910 &log,
4911 make_request("1", "POST", "https://api.example.com"),
4912 Some(make_response("1", 201, "https://api.example.com")),
4913 None,
4914 )
4915 .await;
4916 let path = std::path::Path::new("/tmp/foxdriver_har_fields.json");
4917 log.save_as_har(path, None).await.unwrap();
4918 let content = std::fs::read_to_string(path).unwrap();
4919 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4920 let entry = &parsed["log"]["entries"][0];
4921 assert!(entry["startedDateTime"].is_string());
4922 assert!(entry["request"]["method"].is_string());
4923 assert!(entry["request"]["url"].is_string());
4924 assert!(entry["response"]["status"].is_number());
4925 assert!(entry["response"]["statusText"].is_string());
4926 assert!(entry["timings"].is_object());
4927 }
4928
4929 #[tokio::test]
4930 async fn test_har_empty_log_has_zero_entries() {
4931 let log = NetworkLog::new();
4932 let path = std::path::Path::new("/tmp/foxdriver_har_empty_valid.json");
4933 log.save_as_har(path, None).await.unwrap();
4934 let content = std::fs::read_to_string(path).unwrap();
4935 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4936 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 0);
4937 }
4938
4939 #[tokio::test]
4940 async fn test_har_post_data_present() {
4941 let log = NetworkLog::new();
4942 let mut req = make_request("1", "POST", "https://api.example.com");
4943 req.post_data = Some(r#"{"key":"value"}"#.into());
4944 req.headers = vec![CapturedHeader {
4945 name: "Content-Type".into(),
4946 value: "application/json".into(),
4947 }];
4948 push_entry(
4949 &log,
4950 req,
4951 Some(make_response("1", 200, "https://api.example.com")),
4952 None,
4953 )
4954 .await;
4955 let path = std::path::Path::new("/tmp/foxdriver_har_postdata.json");
4956 log.save_as_har(path, None).await.unwrap();
4957 let content = std::fs::read_to_string(path).unwrap();
4958 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4959 let post_data = &parsed["log"]["entries"][0]["request"]["postData"];
4960 assert!(post_data["text"].is_string());
4961 assert!(post_data["mimeType"].is_string());
4962 }
4963
4964 #[tokio::test]
4967 async fn test_ingest_response_for_unknown_request_stashes_pending() {
4968 let log = NetworkLog::new();
4969 {
4971 let mut inner = log.inner.write().await;
4972 inner.pending_responses.insert(
4973 "future-req".into(),
4974 make_response("future-req", 200, "https://a.com"),
4975 );
4976 }
4977 assert_eq!(log.pending_count().await, 1);
4978 {
4980 let mut inner = log.inner.write().await;
4981 let resp = inner.pending_responses.remove("future-req");
4982 let entry = Arc::new(NetworkEntry {
4983 request: make_request("future-req", "GET", "https://a.com"),
4984 response: resp,
4985 error: None,
4986 });
4987 inner.push_entry(entry);
4988 }
4989 assert_eq!(log.pending_count().await, 0);
4990 assert!(log.has_response("future-req").await);
4991 }
4992
4993 #[tokio::test]
4994 async fn test_ingest_error_for_unknown_request_stashes_pending() {
4995 let log = NetworkLog::new();
4996 {
4997 let mut inner = log.inner.write().await;
4998 inner.pending_errors.insert(
4999 "future-err".into(),
5000 make_error("future-err", "https://a.com", "timeout"),
5001 );
5002 }
5003 assert_eq!(log.pending_count().await, 1);
5004 {
5006 let mut inner = log.inner.write().await;
5007 let err = inner.pending_errors.remove("future-err");
5008 let entry = Arc::new(NetworkEntry {
5009 request: make_request("future-err", "GET", "https://a.com"),
5010 response: None,
5011 error: err,
5012 });
5013 inner.push_entry(entry);
5014 }
5015 assert_eq!(log.pending_count().await, 0);
5016 assert!(log.has_error("future-err").await);
5017 }
5018
5019 #[tokio::test]
5020 async fn test_ingest_duplicate_request_ignored() {
5021 let log = NetworkLog::new();
5022 push_request(&log, make_request("1", "GET", "https://first.com")).await;
5023 push_request(&log, make_request("1", "GET", "https://second.com")).await;
5024 assert_eq!(log.len().await, 1);
5025 assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
5026 }
5027
5028 #[tokio::test]
5031 async fn test_filter_empty_log() {
5032 let log = NetworkLog::new();
5033 let f = Filter::new().method("GET").status_range(200..=200);
5034 assert!(log.filter(f).await.is_empty());
5035 assert_eq!(log.count(Filter::new()).await, 0);
5036 }
5037
5038 #[tokio::test]
5039 async fn test_find_by_url_empty_log() {
5040 let log = NetworkLog::new();
5041 assert!(log.find_by_url("anything").await.is_none());
5042 }
5043
5044 #[tokio::test]
5045 async fn test_find_by_url_regex_empty_log() {
5046 let log = NetworkLog::new();
5047 let re = regex::Regex::new(".*").unwrap();
5048 assert!(log.find_by_url_regex(&re).await.is_none());
5049 }
5050
5051 #[tokio::test]
5052 async fn test_endpoints_empty_log() {
5053 let log = NetworkLog::new();
5054 assert!(log.endpoints().await.is_empty());
5055 }
5056
5057 #[tokio::test]
5058 async fn test_hostnames_empty_log() {
5059 let log = NetworkLog::new();
5060 assert!(log.hostnames().await.is_empty());
5061 }
5062
5063 #[tokio::test]
5064 async fn test_distinct_methods_empty_log_returns_empty() {
5065 let log = NetworkLog::new();
5066 assert!(log.distinct_methods().await.is_empty());
5067 }
5068
5069 #[tokio::test]
5070 async fn test_distinct_statuses_empty_log() {
5071 let log = NetworkLog::new();
5072 assert!(log.distinct_statuses().await.is_empty());
5073 }
5074
5075 #[tokio::test]
5076 async fn test_total_bytes_empty_log() {
5077 let log = NetworkLog::new();
5078 assert_eq!(log.total_bytes_in().await, 0);
5079 assert_eq!(log.total_bytes_out().await, 0);
5080 }
5081
5082 #[tokio::test]
5083 async fn test_nth_out_of_bounds() {
5084 let log = NetworkLog::new();
5085 assert!(log.nth(0).await.is_none());
5086 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5087 assert!(log.nth(1).await.is_none());
5088 assert!(log.nth(100).await.is_none());
5089 }
5090
5091 #[tokio::test]
5092 async fn test_last_n_greater_than_len() {
5093 let log = NetworkLog::new();
5094 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5095 assert_eq!(log.last_n(100).await.len(), 1);
5096 }
5097
5098 #[tokio::test]
5099 async fn test_find_by_status_no_matches() {
5100 let log = NetworkLog::new();
5101 push_entry(
5102 &log,
5103 make_request("1", "GET", "https://a.com"),
5104 Some(make_response("1", 200, "https://a.com")),
5105 None,
5106 )
5107 .await;
5108 assert!(log.find_by_status(404).await.is_empty());
5109 }
5110
5111 #[tokio::test]
5112 async fn test_entries_since_future_timestamp() {
5113 let log = NetworkLog::new();
5114 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5115 assert!(log.entries_since(u64::MAX).await.is_empty());
5116 }
5117
5118 #[tokio::test]
5119 async fn test_contains_id_false() {
5120 let log = NetworkLog::new();
5121 assert!(!log.contains_id("nonexistent").await);
5122 }
5123
5124 #[tokio::test]
5125 async fn test_remove_by_id_nonexistent() {
5126 let log = NetworkLog::new();
5127 assert!(log.remove_by_id("nonexistent").await.is_none());
5128 }
5129
5130 #[tokio::test]
5131 async fn test_metrics_on_empty_log() {
5132 let log = NetworkLog::new();
5133 let m = log.metrics().await;
5134 assert_eq!(m.requests_received, 0);
5135 assert_eq!(m.responses_received, 0);
5136 assert_eq!(m.errors_received, 0);
5137 assert_eq!(m.entries_evicted, 0);
5138 assert_eq!(m.broadcast_drops, 0);
5139 assert_eq!(m.duplicate_responses, 0);
5140 assert_eq!(m.duplicate_errors, 0);
5141 }
5142
5143 #[tokio::test]
5144 async fn test_memory_estimate_grows_with_entries() {
5145 let log = NetworkLog::new();
5146 let empty_est = log.memory_estimate().await;
5147 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5149 let grown_est = log.memory_estimate().await;
5150 assert!(
5151 grown_est > empty_est,
5152 "estimate must grow: empty={empty_est}, after one entry={grown_est}"
5153 );
5154 }
5155
5156 #[tokio::test]
5157 async fn test_with_limits_zero_zero() {
5158 let log = NetworkLog::with_limits(0, 0);
5159 for i in 0..100 {
5160 push_request(
5161 &log,
5162 make_request(&format!("{}", i), "GET", "https://x.com"),
5163 )
5164 .await;
5165 }
5166 assert_eq!(log.len().await, 100);
5167 }
5168
5169 #[tokio::test]
5170 async fn test_with_limits_small() {
5171 let log = NetworkLog::with_limits(5, 5);
5172 for i in 0..20 {
5173 push_request(
5174 &log,
5175 make_request(&format!("{}", i), "GET", "https://x.com"),
5176 )
5177 .await;
5178 }
5179 assert!(log.len().await <= 5);
5180 }
5181
5182 #[tokio::test]
5183 async fn test_wait_for_url_timeout_zero() {
5184 let log = NetworkLog::new();
5185 let result = log
5186 .wait_for_url("nonexistent", std::time::Duration::from_secs(0))
5187 .await;
5188 assert!(result.is_none());
5189 }
5190
5191 #[tokio::test]
5192 async fn test_wait_for_response_timeout_zero() {
5193 let log = NetworkLog::new();
5194 let result = log
5195 .wait_for_response("nonexistent", std::time::Duration::from_secs(0))
5196 .await;
5197 assert!(result.is_none());
5198 }
5199
5200 #[tokio::test]
5203 async fn test_filter_header_case_insensitive() {
5204 let log = NetworkLog::new();
5205 let mut req = make_request("1", "GET", "https://a.com");
5206 req.headers = vec![CapturedHeader {
5207 name: "X-Custom".into(),
5208 value: "SecretValue".into(),
5209 }];
5210 push_request(&log, req).await;
5211 assert_eq!(
5212 log.filter(Filter::new().header("x-custom", "secretvalue"))
5213 .await
5214 .len(),
5215 1
5216 );
5217 assert_eq!(
5218 log.filter(Filter::new().header("X-CUSTOM", "SECRETVALUE"))
5219 .await
5220 .len(),
5221 1
5222 );
5223 }
5224
5225 #[tokio::test]
5226 async fn test_filter_combined_conditions() {
5227 let log = NetworkLog::new();
5228 push_entry(
5229 &log,
5230 make_request("1", "POST", "https://api.example.com"),
5231 Some(make_response("1", 201, "https://api.example.com")),
5232 None,
5233 )
5234 .await;
5235 push_entry(
5236 &log,
5237 make_request("2", "GET", "https://api.example.com"),
5238 Some(make_response("2", 200, "https://api.example.com")),
5239 None,
5240 )
5241 .await;
5242 push_entry(
5243 &log,
5244 make_request("3", "POST", "https://api.example.com"),
5245 Some(make_response("3", 500, "https://api.example.com")),
5246 None,
5247 )
5248 .await;
5249 let f = Filter::new().method("POST").status_range(200..=299);
5250 assert_eq!(log.filter(f).await.len(), 1);
5251 }
5252
5253 #[tokio::test]
5254 async fn test_filter_with_response_only() {
5255 let log = NetworkLog::new();
5256 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5257 push_entry(
5258 &log,
5259 make_request("2", "GET", "https://b.com"),
5260 Some(make_response("2", 200, "https://b.com")),
5261 None,
5262 )
5263 .await;
5264 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
5265 assert_eq!(log.filter(Filter::new().without_response()).await.len(), 1);
5266 }
5267
5268 #[tokio::test]
5269 async fn test_filter_with_error_only() {
5270 let log = NetworkLog::new();
5271 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5272 push_entry(
5273 &log,
5274 make_request("2", "GET", "https://b.com"),
5275 None,
5276 Some(make_error("2", "https://b.com", "err")),
5277 )
5278 .await;
5279 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
5280 }
5281
5282 #[tokio::test]
5285 async fn test_to_curl_empty_body() {
5286 let req = make_request("1", "GET", "https://a.com");
5287 let entry = NetworkEntry {
5288 request: req,
5289 response: None,
5290 error: None,
5291 };
5292 let curl = entry.to_curl();
5293 assert!(!curl.contains("-d"));
5294 assert!(curl.contains("curl -X 'GET'"));
5295 }
5296
5297 #[tokio::test]
5298 async fn test_to_curl_unicode_url() {
5299 let req = make_request("1", "GET", "https://例え.jp/テスト");
5300 let entry = NetworkEntry {
5301 request: req,
5302 response: None,
5303 error: None,
5304 };
5305 let curl = entry.to_curl();
5306 assert!(curl.contains("https://例え.jp/テスト"));
5307 }
5308
5309 #[tokio::test]
5310 async fn test_to_curl_strips_auto_headers() {
5311 let mut req = make_request("1", "GET", "https://a.com");
5312 req.headers = vec![
5313 CapturedHeader {
5314 name: "Host".into(),
5315 value: "a.com".into(),
5316 },
5317 CapturedHeader {
5318 name: "Accept-Encoding".into(),
5319 value: "gzip".into(),
5320 },
5321 CapturedHeader {
5322 name: "Connection".into(),
5323 value: "keep-alive".into(),
5324 },
5325 CapturedHeader {
5326 name: "X-Custom".into(),
5327 value: "value".into(),
5328 },
5329 ];
5330 let entry = NetworkEntry {
5331 request: req,
5332 response: None,
5333 error: None,
5334 };
5335 let curl = entry.to_curl();
5336 assert!(!curl.contains("Host:"));
5337 assert!(!curl.contains("Accept-Encoding:"));
5338 assert!(!curl.contains("Connection:"));
5339 assert!(curl.contains("X-Custom:"));
5340 }
5341
5342 #[tokio::test]
5345 async fn test_query_params_multiple() {
5346 let req = make_request("1", "GET", "https://a.com?a=1&b=2&c=3");
5347 let params = req.query_params().unwrap();
5348 assert_eq!(params.len(), 3);
5349 assert!(params.contains(&("a".into(), "1".into())));
5350 assert!(params.contains(&("b".into(), "2".into())));
5351 assert!(params.contains(&("c".into(), "3".into())));
5352 }
5353
5354 #[tokio::test]
5355 async fn test_json_body_invalid() {
5356 let mut req = make_request("1", "POST", "https://a.com");
5357 req.post_data = Some("not json".into());
5358 assert!(req.json_body().is_none());
5359 }
5360
5361 #[tokio::test]
5362 async fn test_json_body_valid() {
5363 let mut req = make_request("1", "POST", "https://a.com");
5364 req.post_data = Some(r#"{"key":"value"}"#.into());
5365 let json = req.json_body().unwrap();
5366 assert_eq!(json["key"], "value");
5367 }
5368
5369 #[tokio::test]
5372 async fn test_request_header_case_insensitive() {
5373 let mut req = make_request("1", "GET", "https://a.com");
5374 req.headers = vec![CapturedHeader {
5375 name: "Content-Type".into(),
5376 value: "application/json".into(),
5377 }];
5378 assert_eq!(req.request_header("content-type"), Some("application/json"));
5379 assert_eq!(req.request_header("Content-Type"), Some("application/json"));
5380 assert_eq!(req.request_header("CONTENT-TYPE"), Some("application/json"));
5381 }
5382
5383 #[tokio::test]
5384 async fn test_request_header_missing() {
5385 let req = make_request("1", "GET", "https://a.com");
5386 assert_eq!(req.request_header("x-missing"), None);
5387 }
5388
5389 #[tokio::test]
5390 async fn test_response_header_case_insensitive() {
5391 let mut resp = make_response("1", 200, "https://a.com");
5392 resp.headers = vec![CapturedHeader {
5393 name: "X-Response-Header".into(),
5394 value: "val".into(),
5395 }];
5396 let entry = NetworkEntry {
5397 request: make_request("1", "GET", "https://a.com"),
5398 response: Some(resp),
5399 error: None,
5400 };
5401 assert_eq!(entry.response_header("x-response-header"), Some("val"));
5402 assert_eq!(entry.response_header("X-RESPONSE-HEADER"), Some("val"));
5403 }
5404
5405 #[tokio::test]
5408 async fn test_network_entry_final_url() {
5409 let req = make_request("1", "GET", "https://a.com");
5410 let resp = make_response("1", 200, "https://b.com");
5411 let entry = NetworkEntry {
5412 request: req.clone(),
5413 response: Some(resp),
5414 error: None,
5415 };
5416 assert_eq!(entry.final_url(), "https://b.com");
5417 let entry2 = NetworkEntry {
5418 request: req,
5419 response: None,
5420 error: None,
5421 };
5422 assert_eq!(entry2.final_url(), "https://a.com");
5423 }
5424
5425 #[tokio::test]
5426 async fn test_network_entry_status() {
5427 let entry = NetworkEntry {
5428 request: make_request("1", "GET", "https://a.com"),
5429 response: Some(make_response("1", 404, "https://a.com")),
5430 error: None,
5431 };
5432 assert_eq!(entry.status(), Some(404));
5433 let entry2 = NetworkEntry {
5434 request: make_request("1", "GET", "https://a.com"),
5435 response: None,
5436 error: None,
5437 };
5438 assert_eq!(entry2.status(), None);
5439 }
5440
5441 use proptest::prelude::*;
5444
5445 #[tokio::test]
5446 async fn test_proptest_filter_never_panics() {
5447 let log = NetworkLog::new();
5448 for i in 0..50 {
5449 let mut req = make_request(
5450 &format!("{}", i),
5451 ["GET", "POST", "PUT", "DELETE"][i % 4],
5452 &format!("https://{}.com", i),
5453 );
5454 req.headers = vec![CapturedHeader {
5455 name: "X-Id".into(),
5456 value: format!("{}", i),
5457 }];
5458 push_request(&log, req).await;
5459 }
5460
5461 let _ = log.filter(Filter::new().method("GET")).await;
5463 let _ = log.filter(Filter::new().status_range(100..=599)).await;
5464 let _ = log.filter(Filter::new().url_contains("com")).await;
5465 let _ = log.filter(Filter::new().header("x-id", "25")).await;
5466 let _ = log.filter(Filter::new().with_response()).await;
5467 let _ = log.filter(Filter::new().without_response()).await;
5468 let _ = log.filter(Filter::new().with_error()).await;
5469 let _ = log
5470 .filter(
5471 Filter::new()
5472 .method("GET")
5473 .url_contains("com")
5474 .with_response(),
5475 )
5476 .await;
5477 }
5478
5479 #[tokio::test]
5480 async fn test_proptest_concurrent_subscribe_and_push() {
5481 let log = NetworkLog::new();
5482 let mut handles = Vec::new();
5483
5484 for _ in 0..20 {
5485 let log = log.clone();
5486 handles.push(tokio::spawn(async move {
5487 let mut rx = log.subscribe().await;
5488 let mut count = 0;
5489 while count < 50 {
5490 if rx.try_recv().is_ok() {
5491 count += 1;
5492 } else {
5493 tokio::task::yield_now().await;
5494 }
5495 }
5496 }));
5497 }
5498
5499 let log2 = log.clone();
5500 let pusher = tokio::spawn(async move {
5501 for i in 0..100 {
5502 push_request(
5503 &log2,
5504 make_request(&format!("{}", i), "GET", "https://x.com"),
5505 )
5506 .await;
5507 if i % 10 == 0 {
5508 tokio::task::yield_now().await;
5509 }
5510 }
5511 });
5512
5513 pusher.await.unwrap();
5514 for h in handles {
5515 let _ = h.await;
5516 }
5517 }
5518
5519 #[tokio::test]
5520 async fn test_proptest_random_operations() {
5521 let log = NetworkLog::new();
5522 for i in 0..100 {
5523 push_request(
5524 &log,
5525 make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
5526 )
5527 .await;
5528 }
5529
5530 log.remove_by_id("50").await;
5532 log.retain(|e| e.request.id != "25").await;
5533 let _ = log.drain_pending().await;
5534 log.clear().await;
5535
5536 assert!(log.is_empty().await);
5537 assert_eq!(log.pending_count().await, 0);
5538 }
5539
5540 proptest! {
5543 #![proptest_config(ProptestConfig::with_cases(256))]
5544
5545 #[test]
5546 fn prop_query_params_no_crash(url in r"https://[a-z0-9]+\.com(/[a-z]*)*(\?[a-z]+=[a-z0-9]*)*") {
5547 let req = CapturedRequest {
5548 id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5549 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5550 destination: "document".into(), initiator_type: None,
5551 timing: CapturedTiming::default(),
5552 };
5553 let _ = req.query_params(); }
5555
5556 #[test]
5557 fn prop_request_header_case_insensitive(
5558 name in "[A-Za-z_-]{1,20}",
5559 value in "[a-z0-9]{1,30}",
5560 lookup in "[A-Za-z_-]{1,20}"
5561 ) {
5562 let req = CapturedRequest {
5563 id: "1".into(), context: None, url: "https://a.com".into(), method: "GET".into(),
5564 headers: vec![CapturedHeader { name: name.clone(), value: value.clone() }],
5565 cookies: vec![], post_data: None, timestamp: 0,
5566 destination: "document".into(), initiator_type: None,
5567 timing: CapturedTiming::default(),
5568 };
5569 let result = req.request_header(&lookup);
5570 if name.to_lowercase() == lookup.to_lowercase() {
5571 prop_assert_eq!(result, Some(value.as_str()));
5572 }
5573 }
5574
5575 #[test]
5576 fn prop_json_body_valid_json_roundtrips(obj in "\\{.*\\}") {
5577 let req = CapturedRequest {
5578 id: "1".into(), context: None, url: "https://a.com".into(), method: "POST".into(),
5579 headers: vec![], cookies: vec![], post_data: Some(obj.clone()),
5580 timestamp: 0, destination: "document".into(), initiator_type: None,
5581 timing: CapturedTiming::default(),
5582 };
5583 if let Some(val) = req.json_body() {
5585 let re_encoded = serde_json::to_string(&val).unwrap();
5586 let re_parsed: serde_json::Value = serde_json::from_str(&re_encoded).unwrap();
5587 prop_assert_eq!(val, re_parsed);
5588 }
5589 }
5590
5591 #[test]
5592 fn prop_filter_empty_matches_anything(
5593 method in "(GET|POST|PUT|DELETE|PATCH)",
5594 url in r"https://[a-z]+\.com",
5595 status in 100u16..599u16
5596 ) {
5597 let entry = NetworkEntry {
5598 request: CapturedRequest {
5599 id: "1".into(), context: None, url: url.clone(), method,
5600 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5601 destination: "document".into(), initiator_type: None,
5602 timing: CapturedTiming::default(),
5603 },
5604 response: Some(CapturedResponse {
5605 id: "1".into(), url, protocol: "h2".into(), status,
5606 status_text: "OK".into(), headers: vec![], mime_type: "text/html".into(),
5607 body_size: None, from_cache: false,
5608 }),
5609 error: None,
5610 };
5611 let filter = Filter::new();
5612 prop_assert!(filter.matches(&entry));
5613 }
5614
5615 #[test]
5616 fn prop_to_curl_always_contains_url(url in r"https://[a-z]+\.com(/[a-z]*)*") {
5617 let entry = NetworkEntry {
5618 request: CapturedRequest {
5619 id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5620 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5621 destination: "document".into(), initiator_type: None,
5622 timing: CapturedTiming::default(),
5623 },
5624 response: None, error: None,
5625 };
5626 let curl = entry.to_curl();
5627 prop_assert!(curl.contains(&url), "curl missing URL: {}", curl);
5628 }
5629
5630 #[test]
5631 fn prop_to_curl_get_has_no_data_flag(url in r"https://[a-z]+\.com") {
5632 let entry = NetworkEntry {
5633 request: CapturedRequest {
5634 id: "1".into(), context: None, url, method: "GET".into(),
5635 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5636 destination: "document".into(), initiator_type: None,
5637 timing: CapturedTiming::default(),
5638 },
5639 response: None, error: None,
5640 };
5641 let curl = entry.to_curl();
5642 prop_assert!(!curl.contains(" -d "), "GET curl should not have -d: {}", curl);
5643 }
5644 }
5645
5646 #[tokio::test]
5649 async fn test_push_entry_with_very_long_url() {
5650 let log = NetworkLog::new();
5651 let long_url = format!("https://a.com/{}", "x".repeat(10_000));
5652 push_request(&log, make_request("1", "GET", &long_url)).await;
5653 assert_eq!(log.first().await.unwrap().request.url.len(), 14 + 10_000);
5654 }
5655
5656 #[tokio::test]
5657 async fn test_push_entry_with_unicode_url() {
5658 let log = NetworkLog::new();
5659 push_request(&log, make_request("1", "GET", "https://例え.jp/テスト")).await;
5660 assert_eq!(
5661 log.first().await.unwrap().request.url,
5662 "https://例え.jp/テスト"
5663 );
5664 }
5665
5666 #[tokio::test]
5667 async fn test_filter_url_regex_complex() {
5668 let log = NetworkLog::new();
5669 push_request(
5670 &log,
5671 make_request("1", "GET", "https://api.example.com/v1/users"),
5672 )
5673 .await;
5674 push_request(
5675 &log,
5676 make_request("2", "GET", "https://api.example.com/v2/items"),
5677 )
5678 .await;
5679 push_request(&log, make_request("3", "GET", "https://other.com/v1/users")).await;
5680 let re = regex::Regex::new(r"api\.example\.com/v\d+/users").unwrap();
5681 let found = log.find_by_url_regex(&re).await;
5682 assert_eq!(found.unwrap().request.id, "1");
5683 }
5684
5685 #[tokio::test]
5686 async fn test_filter_url_regex_no_match() {
5687 let log = NetworkLog::new();
5688 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5689 let re = regex::Regex::new(r"nomatch").unwrap();
5690 assert!(log.find_by_url_regex(&re).await.is_none());
5691 }
5692
5693 #[tokio::test]
5694 async fn test_hostnames_with_subdomains() {
5695 let log = NetworkLog::new();
5696 push_request(
5697 &log,
5698 make_request("1", "GET", "https://sub1.sub2.example.com/path"),
5699 )
5700 .await;
5701 push_request(&log, make_request("2", "GET", "https://other.org")).await;
5702 let hosts = log.hostnames().await;
5703 assert_eq!(hosts.len(), 2);
5704 assert!(hosts.contains(&"sub1.sub2.example.com".into()));
5705 }
5706
5707 #[tokio::test]
5708 async fn test_endpoints_deduplicates_full_url() {
5709 let log = NetworkLog::new();
5710 push_request(&log, make_request("1", "GET", "https://a.com/path?x=1")).await;
5711 push_request(&log, make_request("2", "GET", "https://a.com/path?y=2")).await;
5712 let endpoints = log.endpoints().await;
5713 assert_eq!(endpoints.len(), 2);
5715 }
5716
5717 #[tokio::test]
5718 async fn test_endpoints_with_ports() {
5719 let log = NetworkLog::new();
5720 push_request(&log, make_request("1", "GET", "https://a.com:8080/path")).await;
5721 push_request(&log, make_request("2", "GET", "https://a.com:9090/path")).await;
5722 let endpoints = log.endpoints().await;
5723 assert_eq!(endpoints.len(), 2);
5724 }
5725
5726 #[tokio::test]
5727 async fn test_distinct_methods_sorted() {
5728 let log = NetworkLog::new();
5729 push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
5730 push_request(&log, make_request("2", "GET", "https://a.com")).await;
5731 push_request(&log, make_request("3", "POST", "https://a.com")).await;
5732 push_request(&log, make_request("4", "GET", "https://a.com")).await;
5733 assert_eq!(log.distinct_methods().await, vec!["DELETE", "GET", "POST"]);
5734 }
5735
5736 #[tokio::test]
5737 async fn test_distinct_statuses_sorted() {
5738 let log = NetworkLog::new();
5739 push_entry(
5740 &log,
5741 make_request("1", "GET", "https://a.com"),
5742 Some(make_response("1", 500, "https://a.com")),
5743 None,
5744 )
5745 .await;
5746 push_entry(
5747 &log,
5748 make_request("2", "GET", "https://a.com"),
5749 Some(make_response("2", 200, "https://a.com")),
5750 None,
5751 )
5752 .await;
5753 push_entry(
5754 &log,
5755 make_request("3", "GET", "https://a.com"),
5756 Some(make_response("3", 404, "https://a.com")),
5757 None,
5758 )
5759 .await;
5760 push_entry(
5761 &log,
5762 make_request("4", "GET", "https://a.com"),
5763 Some(make_response("4", 200, "https://a.com")),
5764 None,
5765 )
5766 .await;
5767 assert_eq!(log.distinct_statuses().await, vec![200, 404, 500]);
5768 }
5769
5770 #[tokio::test]
5771 async fn test_total_bytes_in_with_none_body_size() {
5772 let log = NetworkLog::new();
5773 let mut resp = make_response("1", 200, "https://a.com");
5774 resp.body_size = None;
5775 push_entry(
5776 &log,
5777 make_request("1", "GET", "https://a.com"),
5778 Some(resp),
5779 None,
5780 )
5781 .await;
5782 assert_eq!(log.total_bytes_in().await, 0);
5783 }
5784
5785 #[tokio::test]
5786 async fn test_total_bytes_out_with_post_data() {
5787 let log = NetworkLog::new();
5788 let mut req = make_request("1", "POST", "https://a.com");
5789 req.post_data = Some("payload".into());
5790 push_request(&log, req).await;
5791 assert_eq!(log.total_bytes_out().await, 7);
5792 }
5793
5794 #[tokio::test]
5795 async fn test_total_bytes_out_empty() {
5796 let log = NetworkLog::new();
5797 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5798 assert_eq!(log.total_bytes_out().await, 0);
5799 }
5800
5801 #[tokio::test]
5802 async fn test_completed_returns_only_with_response_or_error() {
5803 let log = NetworkLog::new();
5804 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5805 push_entry(
5806 &log,
5807 make_request("2", "GET", "https://b.com"),
5808 Some(make_response("2", 200, "https://b.com")),
5809 None,
5810 )
5811 .await;
5812 push_entry(
5813 &log,
5814 make_request("3", "GET", "https://c.com"),
5815 None,
5816 Some(make_error("3", "https://c.com", "err")),
5817 )
5818 .await;
5819 let completed = log.completed().await;
5820 assert_eq!(completed.len(), 2);
5821 }
5822
5823 #[tokio::test]
5824 async fn test_count_with_no_filter() {
5825 let log = NetworkLog::new();
5826 for i in 0..50 {
5827 push_request(
5828 &log,
5829 make_request(&format!("{}", i), "GET", "https://x.com"),
5830 )
5831 .await;
5832 }
5833 assert_eq!(log.count(Filter::new()).await, 50);
5834 }
5835
5836 #[tokio::test]
5837 async fn test_find_by_url_partial_match() {
5838 let log = NetworkLog::new();
5839 push_request(
5840 &log,
5841 make_request("1", "GET", "https://api.example.com/users"),
5842 )
5843 .await;
5844 push_request(&log, make_request("2", "GET", "https://other.com/users")).await;
5845 assert_eq!(
5846 log.find_by_url("api.example").await.unwrap().request.id,
5847 "1"
5848 );
5849 }
5850
5851 #[tokio::test]
5852 async fn test_subscribe_receives_existing_and_new() {
5853 let log = NetworkLog::new();
5854 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5855 let mut rx = log.subscribe().await;
5856 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5857 let received = rx.try_recv().unwrap();
5859 assert_eq!(received.request.id, "2");
5860 assert!(rx.try_recv().is_err());
5861 }
5862
5863 #[tokio::test]
5864 async fn test_metrics_after_eviction() {
5865 let log = NetworkLog::with_limits(10, 5);
5866 for i in 0..25 {
5867 push_request(
5868 &log,
5869 make_request(&format!("{}", i), "GET", "https://x.com"),
5870 )
5871 .await;
5872 }
5873 let m = log.metrics().await;
5874 assert!(m.entries_evicted > 0);
5875 }
5876
5877 #[tokio::test]
5878 async fn test_eviction_preserves_order_of_remaining() {
5879 let log = NetworkLog::with_limits(10, 5);
5880 for i in 0..20 {
5881 push_request(
5882 &log,
5883 make_request(&format!("{}", i), "GET", "https://x.com"),
5884 )
5885 .await;
5886 }
5887 let ids = log.request_ids().await;
5888 for pair in ids.windows(2) {
5890 let a: u32 = pair[0].parse().unwrap();
5891 let b: u32 = pair[1].parse().unwrap();
5892 assert!(a < b, "order violated: {a} before {b}");
5893 }
5894 }
5895
5896 #[tokio::test]
5897 async fn test_captured_timing_defaults() {
5898 let timing = CapturedTiming::default();
5899 assert!(timing.dns_start_ms.is_none());
5900 assert!(timing.dns_end_ms.is_none());
5901 assert!(timing.connect_start_ms.is_none());
5902 assert!(timing.connect_end_ms.is_none());
5903 assert!(timing.tls_start_ms.is_none());
5904 assert!(timing.response_start_ms.is_none());
5905 assert!(timing.response_end_ms.is_none());
5906 }
5907
5908 #[tokio::test]
5909 async fn test_captured_request_header_missing() {
5910 let req = make_request("1", "GET", "https://a.com");
5911 assert_eq!(req.request_header("X-Missing"), None);
5912 }
5913
5914 #[tokio::test]
5915 async fn test_network_entry_is_error() {
5916 let entry = NetworkEntry {
5917 request: make_request("1", "GET", "https://a.com"),
5918 response: None,
5919 error: Some(make_error("1", "https://a.com", "err")),
5920 };
5921 assert!(entry.is_error());
5922 assert!(!entry.has_response());
5923 }
5924
5925 #[tokio::test]
5926 async fn test_network_entry_has_response() {
5927 let entry = NetworkEntry {
5928 request: make_request("1", "GET", "https://a.com"),
5929 response: Some(make_response("1", 200, "https://a.com")),
5930 error: None,
5931 };
5932 assert!(entry.has_response());
5933 assert!(!entry.is_error());
5934 }
5935
5936 #[tokio::test]
5937 async fn test_save_to_json_empty_log() {
5938 let log = NetworkLog::new();
5939 let path = std::path::Path::new("/tmp/foxdriver_json_empty.json");
5940 log.save_to_json(path).await.unwrap();
5941 let content = std::fs::read_to_string(path).unwrap();
5942 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5943 assert!(parsed.is_empty());
5944 }
5945
5946 #[tokio::test]
5947 async fn test_save_to_json_roundtrip_entries() {
5948 let log = NetworkLog::new();
5949 let mut req = make_request("1", "POST", "https://api.example.com");
5950 req.headers = vec![CapturedHeader {
5951 name: "Content-Type".into(),
5952 value: "application/json".into(),
5953 }];
5954 req.post_data = Some(r#"{"key":"value"}"#.into());
5955 push_entry(
5956 &log,
5957 req,
5958 Some(make_response("1", 201, "https://api.example.com")),
5959 None,
5960 )
5961 .await;
5962 let path = std::path::Path::new("/tmp/foxdriver_json_roundtrip.json");
5963 log.save_to_json(path).await.unwrap();
5964 let content = std::fs::read_to_string(path).unwrap();
5965 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5966 assert_eq!(parsed.len(), 1);
5967 assert_eq!(parsed[0].request.id, "1");
5968 assert_eq!(parsed[0].request.method, "POST");
5969 assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
5970 }
5971
5972 #[tokio::test]
5973 async fn test_har_with_multiple_entries() {
5974 let log = NetworkLog::new();
5975 push_entry(
5976 &log,
5977 make_request("1", "GET", "https://a.com"),
5978 Some(make_response("1", 200, "https://a.com")),
5979 None,
5980 )
5981 .await;
5982 push_entry(
5983 &log,
5984 make_request("2", "POST", "https://b.com"),
5985 Some(make_response("2", 201, "https://b.com")),
5986 None,
5987 )
5988 .await;
5989 let path = std::path::Path::new("/tmp/foxdriver_har_multi.json");
5990 log.save_as_har(path, Some("test page")).await.unwrap();
5991 let content = std::fs::read_to_string(path).unwrap();
5992 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
5993 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 2);
5994 assert_eq!(parsed["log"]["pages"][0]["title"], "test page");
5995 }
5996
5997 #[tokio::test]
5998 async fn test_har_error_entry_has_no_response() {
5999 let log = NetworkLog::new();
6000 push_entry(
6001 &log,
6002 make_request("1", "GET", "https://a.com"),
6003 None,
6004 Some(make_error("1", "https://a.com", "timeout")),
6005 )
6006 .await;
6007 let path = std::path::Path::new("/tmp/foxdriver_har_error.json");
6008 log.save_as_har(path, None).await.unwrap();
6009 let content = std::fs::read_to_string(path).unwrap();
6010 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
6011 let entry = &parsed["log"]["entries"][0];
6012 assert!(entry["response"].is_null());
6013 }
6014
6015 #[tokio::test]
6018 async fn test_metrics_requests_received_manual_increment() {
6019 let log = NetworkLog::new();
6020 let m0 = log.metrics().await;
6021 {
6022 let mut inner = log.inner.write().await;
6023 inner.metrics.requests_received += 5;
6024 }
6025 let m1 = log.metrics().await;
6026 assert_eq!(m1.requests_received, m0.requests_received + 5);
6027 }
6028
6029 #[tokio::test]
6030 async fn test_metrics_responses_received_manual_increment() {
6031 let log = NetworkLog::new();
6032 let m0 = log.metrics().await;
6033 {
6034 let mut inner = log.inner.write().await;
6035 inner.metrics.responses_received += 3;
6036 }
6037 let m1 = log.metrics().await;
6038 assert_eq!(m1.responses_received, m0.responses_received + 3);
6039 }
6040
6041 #[tokio::test]
6042 async fn test_metrics_errors_received_manual_increment() {
6043 let log = NetworkLog::new();
6044 let m0 = log.metrics().await;
6045 {
6046 let mut inner = log.inner.write().await;
6047 inner.metrics.errors_received += 2;
6048 }
6049 let m1 = log.metrics().await;
6050 assert_eq!(m1.errors_received, m0.errors_received + 2);
6051 }
6052
6053 #[tokio::test]
6054 async fn test_deduplication_does_not_affect_metrics() {
6055 let log = NetworkLog::new();
6056 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6057 let m1 = log.metrics().await;
6058 push_request(&log, make_request("1", "GET", "https://b.com")).await; let m2 = log.metrics().await;
6060 assert_eq!(m2.requests_received, m1.requests_received); assert_eq!(log.len().await, 1); }
6063
6064 #[tokio::test]
6067 async fn test_filter_status_range_boundary_inclusive() {
6068 let log = NetworkLog::new();
6069 push_entry(
6070 &log,
6071 make_request("1", "GET", "https://a.com"),
6072 Some(make_response("1", 200, "https://a.com")),
6073 None,
6074 )
6075 .await;
6076 push_entry(
6077 &log,
6078 make_request("2", "GET", "https://a.com"),
6079 Some(make_response("2", 299, "https://a.com")),
6080 None,
6081 )
6082 .await;
6083 push_entry(
6084 &log,
6085 make_request("3", "GET", "https://a.com"),
6086 Some(make_response("3", 300, "https://a.com")),
6087 None,
6088 )
6089 .await;
6090 assert_eq!(
6091 log.filter(Filter::new().status_range(200..=299))
6092 .await
6093 .len(),
6094 2
6095 );
6096 }
6097
6098 #[tokio::test]
6099 async fn test_filter_destination_deep() {
6100 let log = NetworkLog::new();
6101 let mut req1 = make_request("1", "GET", "https://a.com");
6102 req1.destination = "document".into();
6103 let mut req2 = make_request("2", "GET", "https://b.com");
6104 req2.destination = "image".into();
6105 push_request(&log, req1).await;
6106 push_request(&log, req2).await;
6107 assert_eq!(
6108 log.filter(Filter::new().destination("document"))
6109 .await
6110 .len(),
6111 1
6112 );
6113 assert_eq!(
6114 log.filter(Filter::new().destination("image")).await.len(),
6115 1
6116 );
6117 assert_eq!(
6118 log.filter(Filter::new().destination("script")).await.len(),
6119 0
6120 );
6121 }
6122
6123 #[tokio::test]
6124 async fn test_filter_combined_with_response_and_without_response() {
6125 let log = NetworkLog::new();
6126 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6127 push_entry(
6128 &log,
6129 make_request("2", "GET", "https://b.com"),
6130 Some(make_response("2", 200, "https://b.com")),
6131 None,
6132 )
6133 .await;
6134 push_entry(
6135 &log,
6136 make_request("3", "GET", "https://c.com"),
6137 None,
6138 Some(make_error("3", "https://c.com", "err")),
6139 )
6140 .await;
6141 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
6142 assert_eq!(log.filter(Filter::new().without_response()).await.len(), 2);
6143 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
6144 }
6145
6146 #[tokio::test]
6147 async fn test_filter_url_regex_complex_pattern() {
6148 let log = NetworkLog::new();
6149 push_request(
6150 &log,
6151 make_request("1", "GET", "https://api.example.com/v1/users/123"),
6152 )
6153 .await;
6154 push_request(
6155 &log,
6156 make_request("2", "GET", "https://api.example.com/v2/items/456"),
6157 )
6158 .await;
6159 push_request(
6160 &log,
6161 make_request("3", "GET", "https://other.com/v1/users/789"),
6162 )
6163 .await;
6164 let matches = log
6165 .filter(
6166 Filter::new()
6167 .url_regex(r"api\.example\.com/v\d+/(users|items)/\d+")
6168 .unwrap(),
6169 )
6170 .await;
6171 assert_eq!(matches.len(), 2);
6172 }
6173
6174 #[tokio::test]
6177 async fn test_captured_request_serde_roundtrip() {
6178 let req = make_request("1", "POST", "https://a.com");
6179 let json = serde_json::to_string(&req).unwrap();
6180 let de: CapturedRequest = serde_json::from_str(&json).unwrap();
6181 assert_eq!(de.id, req.id);
6182 assert_eq!(de.method, req.method);
6183 assert_eq!(de.url, req.url);
6184 }
6185
6186 #[tokio::test]
6187 async fn test_captured_response_serde_roundtrip() {
6188 let resp = make_response("1", 404, "https://a.com");
6189 let json = serde_json::to_string(&resp).unwrap();
6190 let de: CapturedResponse = serde_json::from_str(&json).unwrap();
6191 assert_eq!(de.status, 404);
6192 assert_eq!(de.url, "https://a.com");
6193 }
6194
6195 #[tokio::test]
6196 async fn test_captured_error_serde_roundtrip() {
6197 let err = make_error("1", "https://a.com", "timeout");
6198 let json = serde_json::to_string(&err).unwrap();
6199 let de: CapturedError = serde_json::from_str(&json).unwrap();
6200 assert_eq!(de.error_text, "timeout");
6201 }
6202
6203 #[tokio::test]
6204 async fn test_network_entry_serde_roundtrip() {
6205 let entry = NetworkEntry {
6206 request: make_request("1", "GET", "https://a.com"),
6207 response: Some(make_response("1", 200, "https://a.com")),
6208 error: None,
6209 };
6210 let json = serde_json::to_string(&entry).unwrap();
6211 let de: NetworkEntry = serde_json::from_str(&json).unwrap();
6212 assert_eq!(de.request.id, "1");
6213 assert!(de.response.is_some());
6214 assert!(de.error.is_none());
6215 }
6216
6217 #[tokio::test]
6220 async fn test_concurrent_remove_and_query() {
6221 let log = NetworkLog::new();
6222 for i in 0..100 {
6223 push_request(
6224 &log,
6225 make_request(&format!("{}", i), "GET", "https://x.com"),
6226 )
6227 .await;
6228 }
6229 let mut handles = Vec::new();
6230 for _ in 0..10 {
6231 let log = log.clone();
6232 handles.push(tokio::spawn(async move {
6233 for _ in 0..10 {
6234 let _ = log.remove_by_id("50").await;
6235 let _ = log.len().await;
6236 let _ = log.contains_id("50").await;
6237 }
6238 }));
6239 }
6240 for h in handles {
6241 h.await.unwrap();
6242 }
6243 }
6244
6245 #[tokio::test]
6246 async fn test_concurrent_retain_and_read() {
6247 let log = NetworkLog::new();
6248 for i in 0..100 {
6249 push_request(
6250 &log,
6251 make_request(&format!("{}", i), "GET", "https://x.com"),
6252 )
6253 .await;
6254 }
6255 let mut handles = Vec::new();
6256 for _ in 0..5 {
6257 let log = log.clone();
6258 handles.push(tokio::spawn(async move {
6259 log.retain(|e| e.request.id != "50").await;
6260 }));
6261 }
6262 for _ in 0..5 {
6263 let log = log.clone();
6264 handles.push(tokio::spawn(async move {
6265 for _ in 0..20 {
6266 let _ = log.len().await;
6267 tokio::task::yield_now().await;
6268 }
6269 }));
6270 }
6271 for h in handles {
6272 h.await.unwrap();
6273 }
6274 }
6275
6276 #[tokio::test]
6277 async fn test_concurrent_drain_pending_and_ingest() {
6278 let log = NetworkLog::new();
6279 let mut handles = Vec::new();
6280 for _ in 0..5 {
6281 let log = log.clone();
6282 handles.push(tokio::spawn(async move {
6283 for i in 0..20 {
6284 push_request(
6285 &log,
6286 make_request(&format!("{}", i), "GET", "https://x.com"),
6287 )
6288 .await;
6289 }
6290 }));
6291 }
6292 for _ in 0..5 {
6293 let log = log.clone();
6294 handles.push(tokio::spawn(async move {
6295 for _ in 0..20 {
6296 let _ = log.drain_pending().await;
6297 tokio::task::yield_now().await;
6298 }
6299 }));
6300 }
6301 for h in handles {
6302 h.await.unwrap();
6303 }
6304 }
6305
6306 #[tokio::test]
6309 async fn test_wait_for_url_receives_new_entry() {
6310 let log = NetworkLog::new();
6311 let log2 = log.clone();
6312 let waiter = tokio::spawn(async move {
6313 log2.wait_for_url("target", std::time::Duration::from_secs(1))
6314 .await
6315 });
6316 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6317 push_request(&log, make_request("1", "GET", "https://target.com")).await;
6318 let result = waiter.await.unwrap();
6319 assert!(result.is_some());
6320 assert_eq!(result.unwrap().request.url, "https://target.com");
6321 }
6322
6323 #[tokio::test]
6324 async fn test_wait_for_response_receives_update() {
6325 let log = NetworkLog::new();
6326 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6327 let log2 = log.clone();
6328 let waiter = tokio::spawn(async move {
6329 log2.wait_for_response("1", std::time::Duration::from_secs(1))
6330 .await
6331 });
6332 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6333 {
6334 let mut inner = log.inner.write().await;
6335 let mut new_entry = (*inner.entries[0]).clone();
6336 new_entry.response = Some(make_response("1", 200, "https://a.com"));
6337 inner.entries[0] = Arc::new(new_entry);
6338 let _ = inner.tx.send(inner.entries[0].clone());
6339 }
6340 let result = waiter.await.unwrap();
6341 assert!(result.is_some());
6342 assert!(result.unwrap().has_response());
6343 }
6344
6345 #[tokio::test]
6346 async fn test_filter_combined_method_status_url() {
6347 let log = NetworkLog::new();
6348 push_request(
6349 &log,
6350 make_request("1", "POST", "https://api.example.com/v1"),
6351 )
6352 .await;
6353 push_request(&log, make_request("2", "GET", "https://api.example.com/v2")).await;
6354 {
6355 let mut inner = log.inner.write().await;
6356 let mut e = (*inner.entries[0]).clone();
6357 e.response = Some(make_response("1", 201, "https://api.example.com/v1"));
6358 inner.entries[0] = Arc::new(e);
6359 }
6360 let f = Filter::new()
6361 .method("POST")
6362 .status_range(200..=299)
6363 .url_contains("v1");
6364 let hits = log.filter(f).await;
6365 assert_eq!(hits.len(), 1);
6366 assert_eq!(hits[0].request.id, "1");
6367 }
6368
6369 #[tokio::test]
6370 async fn test_filter_has_error_true() {
6371 let log = NetworkLog::new();
6372 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6373 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6374 {
6375 let mut inner = log.inner.write().await;
6376 let mut e = (*inner.entries[1]).clone();
6377 e.error = Some(CapturedError {
6378 id: "2".into(),
6379 url: "https://b.com".into(),
6380 error_text: "net::ERR_FAILED".into(),
6381 });
6382 inner.entries[1] = Arc::new(e);
6383 }
6384 let hits = log.filter(Filter::new().with_error()).await;
6385 assert_eq!(hits.len(), 1);
6386 assert_eq!(hits[0].request.id, "2");
6387 }
6388
6389 #[tokio::test]
6390 async fn test_find_by_url_exact_vs_substring() {
6391 let log = NetworkLog::new();
6392 push_request(&log, make_request("1", "GET", "https://example.com/path")).await;
6393 let found = log.find_by_url("/path").await;
6394 assert!(found.is_some());
6395 let not_found = log.find_by_url("/other").await;
6396 assert!(not_found.is_none());
6397 }
6398
6399 #[tokio::test]
6400 async fn test_endpoints_domain_only_url() {
6401 let log = NetworkLog::new();
6402 push_request(&log, make_request("1", "GET", "https://example.com")).await;
6403 let eps = log.endpoints().await;
6404 assert_eq!(eps, vec!["https://example.com"]);
6405 }
6406
6407 #[tokio::test]
6408 async fn test_hostnames_idn_url() {
6409 let log = NetworkLog::new();
6410 push_request(&log, make_request("1", "GET", "https://münchen.de/shop")).await;
6411 let hosts = log.hostnames().await;
6412 assert_eq!(hosts, vec!["xn--mnchen-3ya.de"]);
6414 }
6415
6416 #[tokio::test]
6417 async fn test_total_bytes_in_missing_body_size() {
6418 let log = NetworkLog::new();
6419 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6420 {
6421 let mut inner = log.inner.write().await;
6422 let mut e = (*inner.entries[0]).clone();
6423 e.response = Some(CapturedResponse {
6424 id: "1".into(),
6425 url: "https://a.com".into(),
6426 protocol: "h2".into(),
6427 status: 200,
6428 status_text: "OK".into(),
6429 headers: vec![],
6430 mime_type: "text/html".into(),
6431 body_size: None,
6432 from_cache: false,
6433 });
6434 inner.entries[0] = Arc::new(e);
6435 }
6436 assert_eq!(log.total_bytes_in().await, 0);
6437 }
6438
6439 #[tokio::test]
6440 async fn test_total_bytes_out_empty_post_data() {
6441 let log = NetworkLog::new();
6442 let mut req = make_request("1", "POST", "https://a.com");
6443 req.post_data = Some("".into());
6444 push_request(&log, req).await;
6445 assert_eq!(log.total_bytes_out().await, 0);
6446 }
6447
6448 #[test]
6449 fn test_to_curl_post_json() {
6450 let entry = NetworkEntry {
6451 request: CapturedRequest {
6452 id: "1".into(),
6453 context: None,
6454 url: "https://api.com".into(),
6455 method: "POST".into(),
6456 headers: vec![CapturedHeader {
6457 name: "Content-Type".into(),
6458 value: "application/json".into(),
6459 }],
6460 cookies: vec![],
6461 post_data: Some(r#"{"key":"val"}"#.into()),
6462 timestamp: 0,
6463 destination: "document".into(),
6464 initiator_type: None,
6465 timing: CapturedTiming::default(),
6466 },
6467 response: None,
6468 error: None,
6469 };
6470 let curl = entry.to_curl();
6471 assert!(curl.contains("-X 'POST'"));
6472 assert!(curl.contains("-H 'Content-Type: application/json'"));
6473 assert!(curl.contains(r#"-d '{"key":"val"}'"#));
6474 }
6475
6476 #[test]
6477 fn test_to_curl_header_with_quote() {
6478 let entry = NetworkEntry {
6479 request: CapturedRequest {
6480 id: "1".into(),
6481 context: None,
6482 url: "https://a.com".into(),
6483 method: "GET".into(),
6484 headers: vec![CapturedHeader {
6485 name: "X-Token".into(),
6486 value: "it's ok".into(),
6487 }],
6488 cookies: vec![],
6489 post_data: None,
6490 timestamp: 0,
6491 destination: "document".into(),
6492 initiator_type: None,
6493 timing: CapturedTiming::default(),
6494 },
6495 response: None,
6496 error: None,
6497 };
6498 let curl = entry.to_curl();
6499 assert!(curl.contains("X-Token: it'\\''s ok"));
6500 }
6501
6502 #[tokio::test]
6503 async fn test_retain_keeps_some_removes_others() {
6504 let log = NetworkLog::new();
6505 push_request(&log, make_request("1", "GET", "https://keep.com")).await;
6506 push_request(&log, make_request("2", "GET", "https://drop.com")).await;
6507 push_request(&log, make_request("3", "GET", "https://keep.com/page")).await;
6508 log.retain(|e| e.request.url.contains("keep")).await;
6509 let ids = log.request_ids().await;
6510 assert_eq!(ids.len(), 2);
6511 assert!(ids.contains(&"1".into()));
6512 assert!(ids.contains(&"3".into()));
6513 }
6514
6515 #[tokio::test]
6516 async fn test_request_ids_ordering_after_eviction() {
6517 let log = NetworkLog::with_limits(4, 100);
6518 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6519 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6520 push_request(&log, make_request("3", "GET", "https://c.com")).await;
6521 push_request(&log, make_request("4", "GET", "https://d.com")).await;
6522 push_request(&log, make_request("5", "GET", "https://e.com")).await; let ids = log.request_ids().await;
6524 assert_eq!(ids.len(), 4);
6526 assert_eq!(
6527 ids,
6528 vec![
6529 "2".to_string(),
6530 "3".to_string(),
6531 "4".to_string(),
6532 "5".to_string()
6533 ]
6534 );
6535 }
6536
6537 #[tokio::test]
6538 async fn test_has_response_false_for_nonexistent() {
6539 let log = NetworkLog::new();
6540 assert!(!log.has_response("nope").await);
6541 }
6542
6543 #[tokio::test]
6544 async fn test_has_error_false_for_nonexistent() {
6545 let log = NetworkLog::new();
6546 assert!(!log.has_error("nope").await);
6547 }
6548
6549 #[tokio::test]
6550 async fn test_memory_estimate_increases_with_entries() {
6551 let log = NetworkLog::new();
6552 let empty = log.memory_estimate().await;
6553 push_request(
6554 &log,
6555 make_request("1", "GET", "https://example.com/some/long/path/here"),
6556 )
6557 .await;
6558 let with_one = log.memory_estimate().await;
6559 assert!(with_one > empty);
6560 }
6561
6562 #[tokio::test]
6563 async fn test_save_to_json_empty_log_roundtrip() {
6564 let log = NetworkLog::new();
6565 let tmp =
6566 std::env::temp_dir().join(format!("foxdriver_empty_json_{}.json", std::process::id()));
6567 log.save_to_json(&tmp).await.unwrap();
6568 let content = std::fs::read_to_string(&tmp).unwrap();
6569 assert!(content.contains("[]"));
6570 let _ = std::fs::remove_file(&tmp);
6571 }
6572
6573 #[tokio::test]
6574 async fn test_save_as_har_empty_log() {
6575 let log = NetworkLog::new();
6576 let tmp =
6577 std::env::temp_dir().join(format!("foxdriver_empty_har_{}.har", std::process::id()));
6578 log.save_as_har(&tmp, Some("empty")).await.unwrap();
6579 let content = std::fs::read_to_string(&tmp).unwrap();
6580 assert!(content.contains("\"entries\""));
6581 assert!(content.contains("\"pages\""));
6582 let _ = std::fs::remove_file(&tmp);
6583 }
6584
6585 #[tokio::test]
6586 async fn test_filter_destination_matching() {
6587 let log = NetworkLog::new();
6588 let mut req = make_request("1", "GET", "https://a.com");
6589 req.destination = "image".into();
6590 push_request(&log, req).await;
6591 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6592 let hits = log.filter(Filter::new().destination("image")).await;
6593 assert_eq!(hits.len(), 1);
6594 assert_eq!(hits[0].request.id, "1");
6595 }
6596
6597 #[tokio::test]
6598 async fn test_distinct_methods_empty_log() {
6599 let log = NetworkLog::new();
6600 assert!(log.distinct_methods().await.is_empty());
6601 }
6602
6603 #[tokio::test]
6604 async fn test_distinct_statuses_no_responses() {
6605 let log = NetworkLog::new();
6606 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6607 assert!(log.distinct_statuses().await.is_empty());
6608 }
6609
6610 #[tokio::test]
6611 async fn test_total_bytes_in_all_none() {
6612 let log = NetworkLog::new();
6613 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6614 {
6615 let mut inner = log.inner.write().await;
6616 let mut e = (*inner.entries[0]).clone();
6617 e.response = Some(CapturedResponse {
6618 id: "1".into(),
6619 url: "https://a.com".into(),
6620 protocol: "h2".into(),
6621 status: 200,
6622 status_text: "OK".into(),
6623 headers: vec![],
6624 mime_type: "text/html".into(),
6625 body_size: None,
6626 from_cache: false,
6627 });
6628 inner.entries[0] = Arc::new(e);
6629 }
6630 assert_eq!(log.total_bytes_in().await, 0);
6631 }
6632
6633 #[tokio::test]
6634 async fn test_total_bytes_out_no_post_data() {
6635 let log = NetworkLog::new();
6636 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6637 assert_eq!(log.total_bytes_out().await, 0);
6638 }
6639
6640 #[tokio::test]
6641 async fn test_save_to_json_roundtrip_with_response() {
6642 let log = NetworkLog::new();
6643 push_request(&log, make_request("1", "POST", "https://api.com")).await;
6644 {
6645 let mut inner = log.inner.write().await;
6646 let mut e = (*inner.entries[0]).clone();
6647 e.response = Some(make_response("1", 201, "https://api.com"));
6648 inner.entries[0] = Arc::new(e);
6649 }
6650 let tmp =
6651 std::env::temp_dir().join(format!("foxdriver_json_rt_{}.json", std::process::id()));
6652 log.save_to_json(&tmp).await.unwrap();
6653 let content = std::fs::read_to_string(&tmp).unwrap();
6654 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
6655 assert_eq!(parsed.len(), 1);
6656 assert_eq!(parsed[0].request.id, "1");
6657 assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
6658 let _ = std::fs::remove_file(&tmp);
6659 }
6660
6661 #[tokio::test]
6662 async fn test_save_as_har_with_response_entry() {
6663 let log = NetworkLog::new();
6664 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6665 {
6666 let mut inner = log.inner.write().await;
6667 let mut e = (*inner.entries[0]).clone();
6668 e.response = Some(make_response("1", 200, "https://a.com"));
6669 inner.entries[0] = Arc::new(e);
6670 }
6671 let tmp =
6672 std::env::temp_dir().join(format!("foxdriver_har_resp_{}.har", std::process::id()));
6673 log.save_as_har(&tmp, Some("testpage")).await.unwrap();
6674 let content = std::fs::read_to_string(&tmp).unwrap();
6675 assert!(content.contains("\"testpage\""));
6676 assert!(content.contains("200"));
6677 let _ = std::fs::remove_file(&tmp);
6678 }
6679
6680 #[tokio::test]
6681 async fn test_has_response_true_when_present() {
6682 let log = NetworkLog::new();
6683 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6684 assert!(!log.has_response("1").await);
6685 {
6686 let mut inner = log.inner.write().await;
6687 let mut e = (*inner.entries[0]).clone();
6688 e.response = Some(make_response("1", 200, "https://a.com"));
6689 inner.entries[0] = Arc::new(e);
6690 }
6691 assert!(log.has_response("1").await);
6692 }
6693
6694 #[tokio::test]
6695 async fn test_has_error_true_when_present() {
6696 let log = NetworkLog::new();
6697 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6698 assert!(!log.has_error("1").await);
6699 {
6700 let mut inner = log.inner.write().await;
6701 let mut e = (*inner.entries[0]).clone();
6702 e.error = Some(CapturedError {
6703 id: "1".into(),
6704 url: "https://a.com".into(),
6705 error_text: "fail".into(),
6706 });
6707 inner.entries[0] = Arc::new(e);
6708 }
6709 assert!(log.has_error("1").await);
6710 }
6711
6712 #[tokio::test]
6713 async fn test_memory_estimate_decreases_after_clear() {
6714 let log = NetworkLog::new();
6715 push_request(
6716 &log,
6717 make_request(
6718 "1",
6719 "GET",
6720 "https://example.com/very/long/path/here/for/bytes",
6721 ),
6722 )
6723 .await;
6724 let before = log.memory_estimate().await;
6725 log.clear().await;
6726 let after = log.memory_estimate().await;
6727 assert!(after < before);
6728 }
6729
6730 #[tokio::test]
6731 async fn test_memory_estimate_decreases_after_retain() {
6732 let log = NetworkLog::new();
6733 push_request(&log, make_request("1", "GET", "https://a.com/loooooong")).await;
6734 push_request(&log, make_request("2", "GET", "https://b.com/loooooong")).await;
6735 let before = log.memory_estimate().await;
6736 log.retain(|e| e.request.id == "1").await;
6737 let after = log.memory_estimate().await;
6738 assert!(after < before);
6739 }
6740
6741 #[tokio::test]
6742 async fn test_completed_excludes_in_flight() {
6743 let log = NetworkLog::new();
6744 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6745 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6746 {
6747 let mut inner = log.inner.write().await;
6748 let mut e = (*inner.entries[1]).clone();
6749 e.response = Some(make_response("2", 200, "https://b.com"));
6750 inner.entries[1] = Arc::new(e);
6751 }
6752 let completed = log.completed().await;
6753 assert_eq!(completed.len(), 1);
6754 assert_eq!(completed[0].request.id, "2");
6755 }
6756
6757 #[tokio::test]
6758 async fn test_completed_includes_errors() {
6759 let log = NetworkLog::new();
6760 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6761 {
6762 let mut inner = log.inner.write().await;
6763 let mut e = (*inner.entries[0]).clone();
6764 e.error = Some(CapturedError {
6765 id: "1".into(),
6766 url: "https://a.com".into(),
6767 error_text: "net::ERR_ABORTED".into(),
6768 });
6769 inner.entries[0] = Arc::new(e);
6770 }
6771 let completed = log.completed().await;
6772 assert_eq!(completed.len(), 1);
6773 assert!(completed[0].is_error());
6774 }
6775
6776 #[tokio::test]
6777 async fn test_find_by_url_regex_complex_pattern() {
6778 let log = NetworkLog::new();
6779 push_request(
6780 &log,
6781 make_request("1", "GET", "https://api.example.com/v1/users"),
6782 )
6783 .await;
6784 push_request(
6785 &log,
6786 make_request("2", "GET", "https://api.example.com/v2/items"),
6787 )
6788 .await;
6789 let re = regex::Regex::new(r"/v\d+/users").unwrap();
6790 let found = log.find_by_url_regex(&re).await;
6791 assert_eq!(found.unwrap().request.id, "1");
6792 }
6793
6794 #[tokio::test]
6795 async fn test_nth_out_of_bounds_returns_none() {
6796 let log = NetworkLog::new();
6797 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6798 assert!(log.nth(5).await.is_none());
6799 }
6800
6801 #[tokio::test]
6802 async fn test_first_last_both_none_on_empty() {
6803 let log = NetworkLog::new();
6804 assert!(log.first().await.is_none());
6805 assert!(log.last().await.is_none());
6806 }
6807
6808 #[tokio::test]
6809 async fn test_remove_by_id_missing_returns_none() {
6810 let log = NetworkLog::new();
6811 assert!(log.remove_by_id("nope").await.is_none());
6812 }
6813
6814 #[tokio::test]
6815 async fn test_contains_id_true_and_false() {
6816 let log = NetworkLog::new();
6817 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6818 assert!(log.contains_id("1").await);
6819 assert!(!log.contains_id("2").await);
6820 }
6821
6822 #[tokio::test]
6823 async fn test_endpoints_dedupes_by_full_url() {
6824 let log = NetworkLog::new();
6826 push_request(&log, make_request("1", "GET", "https://a.com/page?x=1")).await;
6827 push_request(&log, make_request("2", "GET", "https://a.com/page?x=2")).await;
6828 let eps = log.endpoints().await;
6829 assert_eq!(eps.len(), 2);
6830 }
6831
6832 #[tokio::test]
6833 async fn test_hostnames_dedupes() {
6834 let log = NetworkLog::new();
6835 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
6836 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
6837 let hosts = log.hostnames().await;
6838 assert_eq!(hosts.len(), 1);
6839 assert_eq!(hosts[0], "a.com");
6840 }
6841
6842 #[tokio::test]
6843 async fn test_drain_pending_empties_maps() {
6844 let log = NetworkLog::new();
6845 {
6847 let mut inner = log.inner.write().await;
6848 inner
6849 .pending_responses
6850 .insert("1".into(), make_response("1", 200, "https://a.com"));
6851 inner.pending_errors.insert(
6852 "2".into(),
6853 CapturedError {
6854 id: "2".into(),
6855 url: "https://b.com".into(),
6856 error_text: "err".into(),
6857 },
6858 );
6859 }
6860 assert_eq!(log.pending_count().await, 2);
6861 let (resps, errs) = log.drain_pending().await;
6862 assert_eq!(resps.len(), 1);
6863 assert_eq!(errs.len(), 1);
6864 assert_eq!(log.pending_count().await, 0);
6865 }
6866
6867 #[tokio::test]
6868 async fn test_metrics_zero_on_fresh_log() {
6869 let log = NetworkLog::new();
6870 let m = log.metrics().await;
6871 assert_eq!(m.requests_received, 0);
6872 assert_eq!(m.responses_received, 0);
6873 assert_eq!(m.errors_received, 0);
6874 assert_eq!(m.entries_evicted, 0);
6875 assert_eq!(m.broadcast_drops, 0);
6876 assert_eq!(m.duplicate_responses, 0);
6877 assert_eq!(m.duplicate_errors, 0);
6878 }
6879
6880 #[tokio::test]
6881 async fn test_metrics_track_entries() {
6882 let log = NetworkLog::new();
6883 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6884 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6885 let m = log.metrics().await;
6888 assert_eq!(m.max_entries, 50_000);
6889 assert_eq!(m.max_entries, log.inner.read().await.max_entries);
6890 }
6891
6892 #[tokio::test]
6893 async fn test_eviction_evicts_oldest_incrementally() {
6894 let log = NetworkLog::with_limits(3, 5);
6895 push_request(&log, make_request("1", "GET", "https://a.com")).await;
6896 push_request(&log, make_request("2", "GET", "https://b.com")).await;
6897 push_request(&log, make_request("3", "GET", "https://c.com")).await;
6898 push_request(&log, make_request("4", "GET", "https://d.com")).await;
6899 push_request(&log, make_request("5", "GET", "https://e.com")).await;
6900
6901 let entries = log.entries().await;
6902 assert_eq!(entries.len(), 3);
6903 assert_eq!(entries[0].request.id, "3");
6904 assert_eq!(entries[1].request.id, "4");
6905 assert_eq!(entries[2].request.id, "5");
6906
6907 let m = log.metrics().await;
6908 assert_eq!(m.entries_evicted, 2);
6909
6910 push_request(&log, make_request("6", "GET", "https://f.com")).await;
6912 let entries = log.entries().await;
6913 assert_eq!(entries.len(), 3);
6914 assert_eq!(entries[0].request.id, "4");
6915 let m = log.metrics().await;
6916 assert_eq!(m.entries_evicted, 3);
6917 }
6918
6919 #[tokio::test]
6920 async fn test_filter_status_range_ignores_error_without_status() {
6921 let log = NetworkLog::new();
6925 push_entry(
6926 &log,
6927 make_request("1", "GET", "https://a.com"),
6928 None,
6929 Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
6930 )
6931 .await;
6932 assert!(log
6935 .filter(Filter::new().status_range(0..=0))
6936 .await
6937 .is_empty());
6938 assert!(log.filter(Filter::new().with_error()).await.len() == 1);
6939 }
6940}