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