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