1use anyhow::Result;
7use rustenium_bidi_definitions::network::events::{
8 BeforeRequestSent, FetchError, ResponseCompleted,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::{broadcast, RwLock};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct CapturedHeader {
21 pub name: String,
22 pub value: String,
23}
24
25impl From<&rustenium_bidi_definitions::network::types::Header> for CapturedHeader {
26 fn from(h: &rustenium_bidi_definitions::network::types::Header) -> Self {
27 let value = match &h.value {
28 rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => s.value.clone(),
29 rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => b.value.clone(),
30 };
31 Self {
32 name: h.name.clone(),
33 value,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
39pub struct CapturedTiming {
40 pub dns_start_ms: Option<f64>,
41 pub dns_end_ms: Option<f64>,
42 pub connect_start_ms: Option<f64>,
43 pub connect_end_ms: Option<f64>,
44 pub tls_start_ms: Option<f64>,
45 pub response_start_ms: Option<f64>,
46 pub response_end_ms: Option<f64>,
47}
48
49impl From<&rustenium_bidi_definitions::network::types::FetchTimingInfo> for CapturedTiming {
50 fn from(t: &rustenium_bidi_definitions::network::types::FetchTimingInfo) -> Self {
51 let origin = t.request_time;
52 Self {
53 dns_start_ms: non_neg(t.dns_start - origin),
54 dns_end_ms: non_neg(t.dns_end - origin),
55 connect_start_ms: non_neg(t.connect_start - origin),
56 connect_end_ms: non_neg(t.connect_end - origin),
57 tls_start_ms: non_neg(t.tls_start - origin),
58 response_start_ms: non_neg(t.response_start - origin),
59 response_end_ms: non_neg(t.response_end - origin),
60 }
61 }
62}
63
64fn non_neg(v: f64) -> Option<f64> {
65 if v >= 0.0 { Some(v) } else { None }
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct CapturedCookie {
70 pub name: String,
71 pub value: String,
72 pub domain: String,
73 pub path: String,
74 pub size: u64,
75 pub http_only: bool,
76 pub secure: bool,
77 pub same_site: String,
78}
79
80impl From<&rustenium_bidi_definitions::network::types::Cookie> for CapturedCookie {
81 fn from(c: &rustenium_bidi_definitions::network::types::Cookie) -> Self {
82 let value = match &c.value {
83 rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => s.value.clone(),
84 rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => b.value.clone(),
85 };
86 Self {
87 name: c.name.clone(),
88 value,
89 domain: c.domain.clone(),
90 path: c.path.clone(),
91 size: c.size,
92 http_only: c.http_only,
93 secure: c.secure,
94 same_site: format!("{:?}", c.same_site).to_lowercase(),
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct CapturedRequest {
101 pub id: String,
102 pub context: Option<String>,
103 pub method: String,
104 pub url: String,
105 pub headers: Vec<CapturedHeader>,
106 pub post_data: Option<String>,
107 pub timestamp: u64,
108 pub destination: String,
109 pub initiator_type: Option<String>,
110 pub timing: CapturedTiming,
111 pub cookies: Vec<CapturedCookie>,
112}
113
114impl CapturedRequest {
115 pub fn json_body(&self) -> Option<serde_json::Value> {
117 self.post_data.as_ref().and_then(|s| serde_json::from_str(s).ok())
118 }
119
120 pub fn query_params(&self) -> Vec<(String, String)> {
122 match url::Url::parse(&self.url) {
123 Ok(u) => u.query_pairs().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
124 Err(_) => Vec::new(),
125 }
126 }
127
128 pub fn request_header(&self, name: &str) -> Option<&str> {
130 let name_lower = name.to_lowercase();
131 self.headers.iter().find(|h| h.name.to_lowercase() == name_lower).map(|h| h.value.as_str())
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct CapturedResponse {
137 pub id: String,
138 pub url: String,
139 pub protocol: String,
140 pub status: u16,
141 pub status_text: String,
142 pub headers: Vec<CapturedHeader>,
143 pub mime_type: String,
144 pub body_size: Option<u64>,
145 pub from_cache: bool,
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct CapturedError {
150 pub id: String,
151 pub url: String,
152 pub error_text: String,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct NetworkEntry {
157 pub request: CapturedRequest,
158 pub response: Option<CapturedResponse>,
159 pub error: Option<CapturedError>,
160}
161
162impl NetworkEntry {
163 pub fn has_response(&self) -> bool {
164 self.response.is_some()
165 }
166
167 pub fn is_error(&self) -> bool {
168 self.error.is_some()
169 }
170
171 pub fn final_url(&self) -> &str {
172 self.response.as_ref().map(|r| r.url.as_str()).unwrap_or(&self.request.url)
173 }
174
175 pub fn status(&self) -> Option<u16> {
176 self.response.as_ref().map(|r| r.status)
177 }
178
179 pub fn response_header(&self, name: &str) -> Option<&str> {
180 let name_lower = name.to_lowercase();
181 self.response.as_ref().and_then(|r| {
182 r.headers.iter().find(|h| h.name.to_lowercase() == name_lower).map(|h| h.value.as_str())
183 })
184 }
185
186 pub fn request_header(&self, name: &str) -> Option<&str> {
187 self.request.request_header(name)
188 }
189
190 pub fn to_curl(&self) -> String {
192 fn shell_quote(s: &str) -> String {
193 s.replace('\'', "'\\''")
195 }
196 let req = &self.request;
197 let mut parts = vec![format!("curl -X {}", req.method)];
198 for h in &req.headers {
199 if h.name.eq_ignore_ascii_case("host")
200 || h.name.eq_ignore_ascii_case("connection")
201 || h.name.eq_ignore_ascii_case("accept-encoding")
202 {
203 continue;
204 }
205 parts.push(format!("-H '{}: {}'", shell_quote(&h.name), shell_quote(&h.value)));
206 }
207 if let Some(ref body) = req.post_data {
208 parts.push(format!("-d '{}'", shell_quote(body)));
209 }
210 parts.push(format!("'{}'", shell_quote(&req.url)));
211 parts.join(" ")
212 }
213}
214
215#[derive(Debug, Clone, Default)]
220pub struct Filter {
221 method: Option<String>,
222 status_range: Option<std::ops::RangeInclusive<u16>>,
223 url_substring: Option<String>,
224 url_regex: Option<regex::Regex>,
225 header_name: Option<String>,
226 header_value_substring: Option<String>,
227 destination: Option<String>,
228 has_response: Option<bool>,
229 has_error: Option<bool>,
230}
231
232impl Filter {
233 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn method(mut self, m: impl Into<String>) -> Self {
238 self.method = Some(m.into().to_uppercase());
239 self
240 }
241
242 pub fn status_range(mut self, r: std::ops::RangeInclusive<u16>) -> Self {
243 self.status_range = Some(r);
244 self
245 }
246
247 pub fn url_contains(mut self, needle: impl Into<String>) -> Self {
248 self.url_substring = Some(needle.into().to_lowercase());
249 self
250 }
251
252 pub fn url_regex(mut self, pattern: &str) -> Result<Self> {
253 self.url_regex = Some(regex::Regex::new(pattern)?);
254 Ok(self)
255 }
256
257 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
258 self.header_name = Some(name.into().to_lowercase());
259 self.header_value_substring = Some(value.into().to_lowercase());
260 self
261 }
262
263 pub fn destination(mut self, d: impl Into<String>) -> Self {
264 self.destination = Some(d.into().to_lowercase());
265 self
266 }
267
268 pub fn with_response(mut self) -> Self {
269 self.has_response = Some(true);
270 self
271 }
272
273 pub fn without_response(mut self) -> Self {
274 self.has_response = Some(false);
275 self
276 }
277
278 pub fn with_error(mut self) -> Self {
279 self.has_error = Some(true);
280 self
281 }
282
283 fn matches(&self, e: &NetworkEntry) -> bool {
284 if let Some(ref m) = self.method {
285 if e.request.method.to_uppercase() != *m {
286 return false;
287 }
288 }
289 if let Some(ref r) = self.status_range {
290 let st = e.status().unwrap_or(0);
291 if !r.contains(&st) {
292 return false;
293 }
294 }
295 if let Some(ref needle) = self.url_substring {
296 if !e.request.url.to_lowercase().contains(needle) {
297 return false;
298 }
299 }
300 if let Some(ref re) = self.url_regex {
301 if !re.is_match(&e.request.url) {
302 return false;
303 }
304 }
305 if let (Some(ref name), Some(ref value)) = (&self.header_name, &self.header_value_substring) {
306 let found = e
307 .request
308 .headers
309 .iter()
310 .chain(e.response.as_ref().map(|r| r.headers.as_slice()).unwrap_or(&[]))
311 .any(|h| h.name.to_lowercase().contains(name) && h.value.to_lowercase().contains(value));
312 if !found {
313 return false;
314 }
315 }
316 if let Some(ref d) = self.destination {
317 if e.request.destination.to_lowercase() != *d {
318 return false;
319 }
320 }
321 if let Some(want) = self.has_response {
322 if e.has_response() != want {
323 return false;
324 }
325 }
326 if let Some(want) = self.has_error {
327 if e.is_error() != want {
328 return false;
329 }
330 }
331 true
332 }
333}
334
335#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
340pub struct NetworkMetrics {
341 pub requests_received: u64,
342 pub responses_received: u64,
343 pub errors_received: u64,
344 pub entries_evicted: u64,
345 pub pending_responses_dropped: u64,
346 pub pending_errors_dropped: u64,
347 pub broadcast_drops: u64,
348 pub duplicate_responses: u64,
349 pub duplicate_errors: u64,
350 pub max_entries: usize,
351}
352
353#[derive(Debug)]
358struct Inner {
359 entries: Vec<Arc<NetworkEntry>>,
360 by_id: HashMap<String, usize>,
361 pending_responses: HashMap<String, CapturedResponse>,
362 pending_errors: HashMap<String, CapturedError>,
363 max_entries: usize,
364 max_pending: usize,
365 tx: broadcast::Sender<Arc<NetworkEntry>>,
366 metrics: NetworkMetrics,
367}
368
369impl Inner {
370 fn new(max_entries: usize, max_pending: usize, tx: broadcast::Sender<Arc<NetworkEntry>>) -> Self {
371 let mut metrics = NetworkMetrics::default();
372 metrics.max_entries = max_entries;
373 Self {
374 entries: if max_entries > 0 { Vec::with_capacity(max_entries) } else { Vec::new() },
375 by_id: HashMap::new(),
376 pending_responses: HashMap::new(),
377 pending_errors: HashMap::new(),
378 max_entries,
379 max_pending,
380 tx,
381 metrics,
382 }
383 }
384
385 fn push_entry(&mut self, entry: Arc<NetworkEntry>) {
386 if self.by_id.contains_key(&entry.request.id) {
388 return;
389 }
390
391 if self.max_entries > 0 && self.entries.len() >= self.max_entries {
393 let remove = self.entries.len() / 2;
394 self.entries.drain(..remove);
395 self.metrics.entries_evicted += remove as u64;
396 self.by_id.clear();
397 for (i, e) in self.entries.iter().enumerate() {
398 self.by_id.insert(e.request.id.clone(), i);
399 }
400 }
401
402 let idx = self.entries.len();
403 self.by_id.insert(entry.request.id.clone(), idx);
404 self.entries.push(entry.clone());
405
406 if self.tx.send(entry).is_err() {
408 self.metrics.broadcast_drops += 1;
409 }
410 }
411}
412
413#[derive(Debug, Clone)]
418pub struct NetworkLog {
419 inner: Arc<RwLock<Inner>>,
420}
421
422
423
424
425
426
427
428
429
430
431
432
433
434impl NetworkLog {
435 pub fn new() -> Self {
436 Self::with_limits(50_000, 10_000)
437 }
438
439 pub fn with_limits(max_entries: usize, max_pending: usize) -> Self {
440 let (tx, _) = broadcast::channel(1024);
441 Self {
442 inner: Arc::new(RwLock::new(Inner::new(max_entries, max_pending, tx))),
443 }
444 }
445
446 pub async fn subscribe(&self) -> broadcast::Receiver<Arc<NetworkEntry>> {
447 self.inner.read().await.tx.subscribe()
448 }
449
450 pub async fn entries(&self) -> Vec<Arc<NetworkEntry>> {
451 let inner = self.inner.read().await;
452 inner.entries.clone()
453 }
454
455 pub async fn completed(&self) -> Vec<Arc<NetworkEntry>> {
456 let inner = self.inner.read().await;
457 inner.entries.iter().filter(|e| e.has_response() || e.is_error()).cloned().collect()
458 }
459
460 pub async fn count(&self, filter: Filter) -> usize {
461 let inner = self.inner.read().await;
462 inner.entries.iter().filter(|e| filter.matches(e)).count()
463 }
464
465 pub async fn filter(&self, f: Filter) -> Vec<Arc<NetworkEntry>> {
466 let inner = self.inner.read().await;
467 inner.entries.iter().filter(|e| f.matches(e)).cloned().collect()
468 }
469
470 pub async fn first(&self) -> Option<Arc<NetworkEntry>> {
471 let inner = self.inner.read().await;
472 inner.entries.first().cloned()
473 }
474
475 pub async fn last(&self) -> Option<Arc<NetworkEntry>> {
476 let inner = self.inner.read().await;
477 inner.entries.last().cloned()
478 }
479
480 pub async fn nth(&self, n: usize) -> Option<Arc<NetworkEntry>> {
481 let inner = self.inner.read().await;
482 inner.entries.get(n).cloned()
483 }
484
485 pub async fn find_by_url(&self, substring: &str) -> Option<Arc<NetworkEntry>> {
486 let inner = self.inner.read().await;
487 inner.entries.iter().find(|e| e.request.url.contains(substring)).cloned()
488 }
489
490 pub async fn find_by_url_regex(&self, re: ®ex::Regex) -> Option<Arc<NetworkEntry>> {
491 let inner = self.inner.read().await;
492 inner.entries.iter().find(|e| re.is_match(&e.request.url)).cloned()
493 }
494
495 pub async fn endpoints(&self) -> Vec<String> {
496 let inner = self.inner.read().await;
497 let mut seen = std::collections::HashSet::new();
498 inner.entries.iter().filter(|e| seen.insert(e.request.url.clone())).map(|e| e.request.url.clone()).collect()
499 }
500
501 pub async fn hostnames(&self) -> Vec<String> {
502 let inner = self.inner.read().await;
503 let mut seen = std::collections::HashSet::new();
504 inner
505 .entries
506 .iter()
507 .filter_map(|e| {
508 url::Url::parse(&e.request.url).ok().and_then(|u| {
509 let host = u.host_str()?.to_string();
510 seen.insert(host.clone()).then_some(host)
511 })
512 })
513 .collect()
514 }
515
516 pub async fn distinct_methods(&self) -> Vec<String> {
517 let inner = self.inner.read().await;
518 let mut seen = std::collections::HashSet::new();
519 let mut out: Vec<String> = inner
520 .entries
521 .iter()
522 .filter_map(|e| seen.insert(e.request.method.clone()).then_some(e.request.method.clone()))
523 .collect();
524 out.sort_unstable();
525 out.dedup();
526 out
527 }
528
529 pub async fn distinct_statuses(&self) -> Vec<u16> {
530 let inner = self.inner.read().await;
531 let mut seen = std::collections::HashSet::new();
532 let mut out: Vec<u16> = inner
533 .entries
534 .iter()
535 .filter_map(|e| {
536 let status = e.status()?;
537 seen.insert(status).then_some(status)
538 })
539 .collect();
540 out.sort_unstable();
541 out
542 }
543
544 pub async fn total_bytes_in(&self) -> u64 {
545 let inner = self.inner.read().await;
546 inner.entries.iter().filter_map(|e| e.response.as_ref().and_then(|r| r.body_size)).sum()
547 }
548
549 pub async fn total_bytes_out(&self) -> u64 {
550 let inner = self.inner.read().await;
551 inner.entries.iter().map(|e| e.request.post_data.as_ref().map(|b| b.len() as u64).unwrap_or(0)).sum()
552 }
553
554 pub async fn len(&self) -> usize {
555 self.inner.read().await.entries.len()
556 }
557
558 pub async fn is_empty(&self) -> bool {
559 self.len().await == 0
560 }
561
562 pub async fn clear(&self) {
563 let mut inner = self.inner.write().await;
564 inner.entries.clear();
565 inner.by_id.clear();
566 }
567
568 pub async fn metrics(&self) -> NetworkMetrics {
569 self.inner.read().await.metrics.clone()
570 }
571
572 pub async fn contains_id(&self, id: &str) -> bool {
573 self.inner.read().await.by_id.contains_key(id)
574 }
575
576 pub async fn remove_by_id(&self, id: &str) -> Option<Arc<NetworkEntry>> {
577 let mut inner = self.inner.write().await;
578 let idx = inner.by_id.remove(id)?;
579 let entry = inner.entries.remove(idx);
580 let updates: Vec<(String, usize)> = inner
582 .entries
583 .iter()
584 .enumerate()
585 .skip(idx)
586 .map(|(i, e)| (e.request.id.clone(), i))
587 .collect();
588 for (id, i) in updates {
589 inner.by_id.insert(id, i);
590 }
591 Some(entry)
592 }
593
594 pub async fn request_ids(&self) -> Vec<String> {
596 let inner = self.inner.read().await;
597 inner.entries.iter().map(|e| e.request.id.clone()).collect()
598 }
599
600 pub async fn memory_estimate(&self) -> usize {
603 let inner = self.inner.read().await;
604 let mut estimate = 0;
605 estimate += inner.entries.capacity() * std::mem::size_of::<Arc<NetworkEntry>>();
606 estimate += inner.by_id.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<usize>());
607 estimate += inner.pending_responses.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedResponse>());
608 estimate += inner.pending_errors.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedError>());
609 for e in &inner.entries {
610 estimate += e.request.url.len();
611 estimate += e.request.method.len();
612 estimate += e.request.headers.iter().map(|h| h.name.len() + h.value.len()).sum::<usize>();
613 if let Some(ref body) = e.request.post_data {
614 estimate += body.len();
615 }
616 if let Some(ref r) = e.response {
617 estimate += r.url.len() + r.status_text.len() + r.mime_type.len() + r.protocol.len();
618 estimate += r.headers.iter().map(|h| h.name.len() + h.value.len()).sum::<usize>();
619 }
620 if let Some(ref err) = e.error {
621 estimate += err.url.len() + err.error_text.len();
622 }
623 }
624 estimate
625 }
626
627 pub async fn has_response(&self, id: &str) -> bool {
629 let inner = self.inner.read().await;
630 inner.by_id.get(id).map(|&idx| inner.entries[idx].response.is_some()).unwrap_or(false)
631 }
632
633 pub async fn has_error(&self, id: &str) -> bool {
635 let inner = self.inner.read().await;
636 inner.by_id.get(id).map(|&idx| inner.entries[idx].error.is_some()).unwrap_or(false)
637 }
638
639 pub async fn pending_count(&self) -> usize {
642 let inner = self.inner.read().await;
643 inner.pending_responses.len() + inner.pending_errors.len()
644 }
645
646 pub async fn drain_pending(&self) -> (Vec<CapturedResponse>, Vec<CapturedError>) {
650 let mut inner = self.inner.write().await;
651 let responses: Vec<CapturedResponse> = inner.pending_responses.drain().map(|(_, v)| v).collect();
652 let errors: Vec<CapturedError> = inner.pending_errors.drain().map(|(_, v)| v).collect();
653 (responses, errors)
654 }
655
656 pub async fn retain<F>(&self, mut f: F)
659 where
660 F: FnMut(&NetworkEntry) -> bool,
661 {
662 let mut inner = self.inner.write().await;
663 let mut new_entries = Vec::new();
664 let mut new_by_id = HashMap::new();
665 for entry in inner.entries.drain(..) {
666 if f(&entry) {
667 let idx = new_entries.len();
668 new_by_id.insert(entry.request.id.clone(), idx);
669 new_entries.push(entry);
670 }
671 }
672 inner.entries = new_entries;
673 inner.by_id = new_by_id;
674 }
675
676 pub async fn wait_for_url(
678 &self,
679 substring: &str,
680 timeout: std::time::Duration,
681 ) -> Option<Arc<NetworkEntry>> {
682 let mut rx = self.subscribe().await;
683 let deadline = tokio::time::Instant::now() + timeout;
684
685 if let Some(e) = self.find_by_url(substring).await {
687 return Some(e);
688 }
689
690 loop {
691 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
692 match tokio::time::timeout(remaining, rx.recv()).await {
693 Ok(Ok(entry)) => {
694 if entry.request.url.contains(substring) {
695 return Some(entry);
696 }
697 }
698 _ => return None,
699 }
700 }
701 }
702
703 pub async fn wait_for_response(
705 &self,
706 id: &str,
707 timeout: std::time::Duration,
708 ) -> Option<Arc<NetworkEntry>> {
709 let mut rx = self.subscribe().await;
710 let deadline = tokio::time::Instant::now() + timeout;
711
712 {
714 let inner = self.inner.read().await;
715 if let Some(idx) = inner.by_id.get(id) {
716 if inner.entries[*idx].has_response() || inner.entries[*idx].is_error() {
717 return Some(inner.entries[*idx].clone());
718 }
719 }
720 }
721
722 loop {
723 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
724 match tokio::time::timeout(remaining, rx.recv()).await {
725 Ok(Ok(entry)) => {
726 if entry.request.id == id && (entry.has_response() || entry.is_error()) {
727 return Some(entry);
728 }
729 }
730 _ => return None,
731 }
732 }
733 }
734
735 pub async fn find_by_status(&self, status: u16) -> Vec<Arc<NetworkEntry>> {
741 let inner = self.inner.read().await;
742 inner
743 .entries
744 .iter()
745 .filter(|e| e.status() == Some(status))
746 .cloned()
747 .collect()
748 }
749
750 pub async fn entries_since(&self, timestamp: u64) -> Vec<Arc<NetworkEntry>> {
752 let inner = self.inner.read().await;
753 inner
754 .entries
755 .iter()
756 .filter(|e| e.request.timestamp >= timestamp)
757 .cloned()
758 .collect()
759 }
760
761 pub async fn last_n(&self, n: usize) -> Vec<Arc<NetworkEntry>> {
763 let inner = self.inner.read().await;
764 inner.entries.iter().rev().take(n).cloned().collect::<Vec<_>>().into_iter().rev().collect()
765 }
766
767 pub async fn unique_urls(&self) -> Vec<String> {
769 let inner = self.inner.read().await;
770 let mut seen = std::collections::HashSet::new();
771 inner
772 .entries
773 .iter()
774 .filter_map(|e| {
775 if seen.insert(e.request.url.clone()) {
776 Some(e.request.url.clone())
777 } else {
778 None
779 }
780 })
781 .collect()
782 }
783
784 pub async fn save_to_json(&self, path: &std::path::Path) -> Result<()> {
789 let entries: Vec<NetworkEntry> = self.entries().await.iter().map(|e| (**e).clone()).collect();
790 let json = serde_json::to_string_pretty(&entries)?;
791 tokio::fs::write(path, json).await?;
792 Ok(())
793 }
794
795 pub async fn save_as_har(&self, path: &std::path::Path, page_title: Option<&str>) -> Result<()> {
797 use chrono::Utc;
798
799 let entries = self.entries().await;
800 let started = if let Some(e) = entries.first() {
801 chrono::DateTime::from_timestamp_millis(e.request.timestamp as i64).unwrap_or_else(|| Utc::now())
802 } else {
803 Utc::now()
804 };
805
806 let mut har_entries = Vec::new();
807 for e in &entries {
808 let req = &e.request;
809 let resp = e.response.as_ref();
810 let timing = &req.timing;
811
812 let query_string: Vec<_> = req
813 .query_params()
814 .into_iter()
815 .map(|(name, value)| serde_json::json!({"name": name, "value": value}))
816 .collect();
817
818 let request_json = serde_json::json!({
819 "method": req.method,
820 "url": req.url,
821 "httpVersion": "HTTP/1.1",
822 "headers": req.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
823 "cookies": req.cookies.iter().map(|c| serde_json::json!({"name": c.name, "value": c.value, "domain": c.domain, "path": c.path})).collect::<Vec<_>>(),
824 "queryString": query_string,
825 "headersSize": -1,
826 "bodySize": req.post_data.as_ref().map(|b| b.len() as i64).unwrap_or(-1),
827 "postData": req.post_data.as_ref().map(|b| {
828 let mime = req.request_header("content-type").unwrap_or("application/octet-stream");
829 serde_json::json!({"mimeType": mime, "text": b})
830 }),
831 });
832
833 let response_json = resp.map(|r| serde_json::json!({
834 "status": r.status,
835 "statusText": r.status_text,
836 "httpVersion": "HTTP/1.1",
837 "headers": r.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
838 "cookies": [],
839 "content": {
840 "size": r.body_size.unwrap_or(0),
841 "mimeType": r.mime_type,
842 },
843 "redirectURL": "",
844 "headersSize": -1,
845 "bodySize": r.body_size.map(|b| b as i64).unwrap_or(-1),
846 }));
847
848 let timings_json = serde_json::json!({
849 "blocked": -1,
850 "dns": option_f64_ms(timing.dns_end_ms, timing.dns_start_ms),
851 "connect": option_f64_ms(timing.connect_end_ms, timing.connect_start_ms),
852 "ssl": option_f64_ms(timing.connect_end_ms, timing.tls_start_ms),
853 "send": -1,
854 "wait": option_f64_ms(timing.response_start_ms, timing.connect_end_ms),
855 "receive": option_f64_ms(timing.response_end_ms, timing.response_start_ms),
856 });
857
858 har_entries.push(serde_json::json!({
859 "startedDateTime": format!("{}", chrono::DateTime::from_timestamp_millis(req.timestamp as i64).unwrap_or_else(|| Utc::now())),
860 "time": 0,
861 "request": request_json,
862 "response": response_json,
863 "cache": {},
864 "timings": timings_json,
865 }));
866 }
867
868 let har = serde_json::json!({
869 "log": {
870 "version": "1.2",
871 "creator": {
872 "name": "runtime_foxdriver",
873 "version": env!("CARGO_PKG_VERSION"),
874 },
875 "pages": [{
876 "startedDateTime": format!("{}", started),
877 "id": "page_1",
878 "title": page_title.unwrap_or("unknown"),
879 "pageTimings": { "onContentLoad": -1, "onLoad": -1 },
880 }],
881 "entries": har_entries,
882 }
883 });
884
885 tokio::fs::write(path, serde_json::to_string_pretty(&har)?).await?;
886 Ok(())
887 }
888
889 pub(crate) async fn ingest_before_request_sent(&self, evt: &BeforeRequestSent) {
894 let mut inner = self.inner.write().await;
895 inner.metrics.requests_received += 1;
896
897 let req = build_request(evt);
898 let resp = inner.pending_responses.remove(&req.id);
899 let err = inner.pending_errors.remove(&req.id);
900
901 let entry = Arc::new(NetworkEntry { request: req, response: resp, error: err });
902 inner.push_entry(entry);
903 }
904
905 pub(crate) async fn ingest_response_completed(&self, evt: &ResponseCompleted) {
906 let mut inner = self.inner.write().await;
907 inner.metrics.responses_received += 1;
908
909 let id = evt.params.base_parameters.request.request.inner().clone();
910 let resp = build_response(evt);
911
912 if let Some(idx) = inner.by_id.get(&id).copied() {
913 let mut new_entry = (*inner.entries[idx]).clone();
914 if new_entry.response.is_some() {
915 inner.metrics.duplicate_responses += 1;
916 }
917 new_entry.response = Some(resp);
918 let new_arc = Arc::new(new_entry);
919 inner.entries[idx] = new_arc.clone();
920 let _ = inner.tx.send(new_arc);
921 } else {
922 while inner.max_pending > 0 && inner.pending_responses.len() >= inner.max_pending {
923 if let Some(k) = inner.pending_responses.keys().next().cloned() {
924 inner.pending_responses.remove(&k);
925 inner.metrics.pending_responses_dropped += 1;
926 }
927 }
928 inner.pending_responses.insert(id, resp);
929 }
930 }
931
932 pub(crate) async fn ingest_fetch_error(&self, evt: &FetchError) {
933 let mut inner = self.inner.write().await;
934 inner.metrics.errors_received += 1;
935
936 let id = evt.params.base_parameters.request.request.inner().clone();
937 let err = CapturedError {
938 id: id.clone(),
939 url: evt
940 .params
941 .base_parameters
942 .navigation
943 .as_ref()
944 .map(|n| n.inner().to_string())
945 .unwrap_or_default(),
946 error_text: evt.params.error_text.clone(),
947 };
948
949 if let Some(idx) = inner.by_id.get(&id).copied() {
950 let mut new_entry = (*inner.entries[idx]).clone();
951 if new_entry.error.is_some() {
952 inner.metrics.duplicate_errors += 1;
953 }
954 new_entry.error = Some(err);
955 let new_arc = Arc::new(new_entry);
956 inner.entries[idx] = new_arc.clone();
957 let _ = inner.tx.send(new_arc);
958 } else {
959 while inner.max_pending > 0 && inner.pending_errors.len() >= inner.max_pending {
960 if let Some(k) = inner.pending_errors.keys().next().cloned() {
961 inner.pending_errors.remove(&k);
962 inner.metrics.pending_errors_dropped += 1;
963 }
964 }
965 inner.pending_errors.insert(id, err);
966 }
967 }
968}
969
970impl Default for NetworkLog {
971 fn default() -> Self {
972 Self::new()
973 }
974}
975
976fn option_f64_ms(end: Option<f64>, start: Option<f64>) -> f64 {
977 match (end, start) {
978 (Some(e), Some(s)) => (e - s).max(0.0),
979 _ => -1.0,
980 }
981}
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998fn build_request(evt: &BeforeRequestSent) -> CapturedRequest {
1003 let bp = &evt.params.base_parameters;
1004 let id = bp.request.request.inner().clone();
1005 let url = bp.request.url.clone();
1006 let headers = bp.request.headers.iter().map(CapturedHeader::from).collect();
1007 let post_data = None; let timestamp = bp.timestamp;
1009 let destination = bp.request.destination.clone();
1010 let initiator_type = evt.params.initiator.as_ref().and_then(|i| {
1011 i.r#type.as_ref().map(|t| format!("{:?}", t).to_lowercase())
1012 });
1013 let timing = CapturedTiming::from(&bp.request.timings);
1014 let cookies = bp.request.cookies.iter().map(CapturedCookie::from).collect();
1015
1016 CapturedRequest {
1017 id,
1018 context: bp.context.as_ref().map(|c| c.inner().to_string()),
1019 method: bp.request.method.clone(),
1020 url,
1021 headers,
1022 post_data,
1023 timestamp,
1024 destination,
1025 initiator_type,
1026 timing,
1027 cookies,
1028 }
1029}
1030
1031fn build_response(evt: &ResponseCompleted) -> CapturedResponse {
1032 let bp = &evt.params.base_parameters;
1033 let resp = &evt.params.response;
1034 let id = bp.request.request.inner().clone();
1035 let url = resp.url.clone();
1036 let headers = resp.headers.iter().map(CapturedHeader::from).collect();
1037 let protocol = resp.protocol.clone();
1038 let status = resp.status as u16;
1039 let status_text = resp.status_text.clone();
1040 let mime_type = resp.mime_type.clone();
1041 let body_size = resp.body_size;
1042 let from_cache = resp.from_cache;
1043
1044 CapturedResponse {
1045 id,
1046 url,
1047 protocol,
1048 status,
1049 status_text,
1050 headers,
1051 mime_type,
1052 body_size,
1053 from_cache,
1054 }
1055}
1056
1057
1058pub fn make_network_handler(
1063 log: NetworkLog,
1064) -> impl FnMut(rustenium_bidi_definitions::Event) -> std::pin::Pin<
1065 Box<dyn std::future::Future<Output = ()> + Send>,
1066> {
1067 use rustenium_bidi_definitions::network::events::NetworkEvent;
1068 move |evt| {
1069 let log = log.clone();
1070 Box::pin(async move {
1071 match evt {
1072 rustenium_bidi_definitions::Event::Network(nev) => match nev {
1073 NetworkEvent::BeforeRequestSent(evt) => log.ingest_before_request_sent(&evt).await,
1074 NetworkEvent::ResponseCompleted(evt) => log.ingest_response_completed(&evt).await,
1075 NetworkEvent::FetchError(evt) => log.ingest_fetch_error(&evt).await,
1076 _ => {}
1077 },
1078 _ => {}
1079 }
1080 })
1081 }
1082}
1083
1084
1085#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 fn make_request(id: &str, method: &str, url: &str) -> CapturedRequest {
1096 CapturedRequest {
1097 id: id.into(),
1098 context: None,
1099 method: method.into(),
1100 url: url.into(),
1101 headers: vec![],
1102 post_data: None,
1103 timestamp: 0,
1104 destination: "document".into(),
1105 initiator_type: None,
1106 timing: CapturedTiming::default(),
1107 cookies: vec![],
1108 }
1109 }
1110
1111 fn make_request_with_headers(id: &str, method: &str, url: &str, headers: Vec<CapturedHeader>) -> CapturedRequest {
1112 CapturedRequest {
1113 id: id.into(),
1114 context: None,
1115 method: method.into(),
1116 url: url.into(),
1117 headers,
1118 post_data: None,
1119 timestamp: 0,
1120 destination: "document".into(),
1121 initiator_type: None,
1122 timing: CapturedTiming::default(),
1123 cookies: vec![],
1124 }
1125 }
1126
1127 fn make_response(id: &str, status: u16, url: &str) -> CapturedResponse {
1128 CapturedResponse {
1129 id: id.into(),
1130 url: url.into(),
1131 protocol: "h2".into(),
1132 status,
1133 status_text: "OK".into(),
1134 headers: vec![],
1135 mime_type: "application/json".into(),
1136 body_size: Some(100),
1137 from_cache: false,
1138 }
1139 }
1140
1141 fn make_error(id: &str, url: &str, text: &str) -> CapturedError {
1142 CapturedError {
1143 id: id.into(),
1144 url: url.into(),
1145 error_text: text.into(),
1146 }
1147 }
1148
1149 async fn push_request(log: &NetworkLog, req: CapturedRequest) {
1150 let entry = Arc::new(NetworkEntry { request: req, response: None, error: None });
1151 let mut inner = log.inner.write().await;
1152 inner.push_entry(entry);
1153 }
1154
1155 async fn push_entry(log: &NetworkLog, req: CapturedRequest, resp: Option<CapturedResponse>, err: Option<CapturedError>) {
1156 let entry = Arc::new(NetworkEntry { request: req, response: resp, error: err });
1157 let mut inner = log.inner.write().await;
1158 inner.push_entry(entry);
1159 }
1160
1161 #[tokio::test]
1164 async fn test_new_log_is_empty() {
1165 let log = NetworkLog::new();
1166 assert!(log.is_empty().await);
1167 assert_eq!(log.len().await, 0);
1168 }
1169
1170 #[tokio::test]
1171 async fn test_with_limits() {
1172 let log = NetworkLog::with_limits(10, 5);
1173 let m = log.metrics().await;
1174 assert_eq!(m.max_entries, 10);
1175 }
1176
1177 #[tokio::test]
1178 async fn test_clear() {
1179 let log = NetworkLog::new();
1180 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1181 assert_eq!(log.len().await, 1);
1182 log.clear().await;
1183 assert!(log.is_empty().await);
1184 }
1185
1186 #[tokio::test]
1187 async fn test_entries_returns_all() {
1188 let log = NetworkLog::new();
1189 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1190 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1191 let entries = log.entries().await;
1192 assert_eq!(entries.len(), 2);
1193 assert_eq!(entries[0].request.id, "1");
1194 assert_eq!(entries[1].request.id, "2");
1195 }
1196
1197 #[tokio::test]
1198 async fn test_first_last_nth() {
1199 let log = NetworkLog::new();
1200 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1201 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1202 push_request(&log, make_request("3", "PUT", "https://c.com")).await;
1203
1204 assert_eq!(log.first().await.unwrap().request.id, "1");
1205 assert_eq!(log.last().await.unwrap().request.id, "3");
1206 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
1207 assert_eq!(log.nth(1).await.unwrap().request.id, "2");
1208 assert_eq!(log.nth(2).await.unwrap().request.id, "3");
1209 assert!(log.nth(99).await.is_none());
1210 }
1211
1212 #[tokio::test]
1215 async fn test_filter_method() {
1216 let log = NetworkLog::new();
1217 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1218 push_request(&log, make_request("2", "POST", "https://example.com")).await;
1219 let get = log.filter(Filter::new().method("GET")).await;
1220 assert_eq!(get.len(), 1);
1221 assert_eq!(get[0].request.method, "GET");
1222 }
1223
1224 #[tokio::test]
1225 async fn test_filter_status_range() {
1226 let log = NetworkLog::new();
1227 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1228 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 404, "https://b.com")), None).await;
1229 push_entry(&log, make_request("3", "GET", "https://c.com"), None, None).await;
1230 let f = log.filter(Filter::new().status_range(200..=299)).await;
1231 assert_eq!(f.len(), 1);
1232 assert_eq!(f[0].request.id, "1");
1233 }
1234
1235 #[tokio::test]
1236 async fn test_filter_url_contains() {
1237 let log = NetworkLog::new();
1238 push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1239 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1240 let f = log.filter(Filter::new().url_contains("api")).await;
1241 assert_eq!(f.len(), 1);
1242 assert_eq!(f[0].request.id, "1");
1243 }
1244
1245 #[tokio::test]
1246 async fn test_filter_url_regex() {
1247 let log = NetworkLog::new();
1248 push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1249 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1250 let f = log.filter(Filter::new().url_regex(r"api\.\w+\.com").unwrap()).await;
1251 assert_eq!(f.len(), 1);
1252 }
1253
1254 #[tokio::test]
1255 async fn test_filter_header() {
1256 let log = NetworkLog::new();
1257 let h1 = vec![CapturedHeader { name: "Authorization".into(), value: "Bearer abc".into() }];
1258 let h2 = vec![CapturedHeader { name: "Content-Type".into(), value: "application/json".into() }];
1259 push_request(&log, make_request_with_headers("1", "GET", "https://a.com", h1)).await;
1260 push_request(&log, make_request_with_headers("2", "GET", "https://b.com", h2)).await;
1261 let f = log.filter(Filter::new().header("authorization", "bearer")).await;
1262 assert_eq!(f.len(), 1);
1263 assert_eq!(f[0].request.id, "1");
1264 }
1265
1266 #[tokio::test]
1267 async fn test_filter_destination() {
1268 let log = NetworkLog::new();
1269 let mut r1 = make_request("1", "GET", "https://a.com");
1270 r1.destination = "image".into();
1271 let mut r2 = make_request("2", "GET", "https://b.com");
1272 r2.destination = "document".into();
1273 push_request(&log, r1).await;
1274 push_request(&log, r2).await;
1275 let f = log.filter(Filter::new().destination("image")).await;
1276 assert_eq!(f.len(), 1);
1277 assert_eq!(f[0].request.id, "1");
1278 }
1279
1280 #[tokio::test]
1281 async fn test_filter_with_response() {
1282 let log = NetworkLog::new();
1283 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1284 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1285 let f = log.filter(Filter::new().with_response()).await;
1286 assert_eq!(f.len(), 1);
1287 assert_eq!(f[0].request.id, "1");
1288 }
1289
1290 #[tokio::test]
1291 async fn test_filter_without_response() {
1292 let log = NetworkLog::new();
1293 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1294 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1295 let f = log.filter(Filter::new().without_response()).await;
1296 assert_eq!(f.len(), 1);
1297 assert_eq!(f[0].request.id, "2");
1298 }
1299
1300 #[tokio::test]
1301 async fn test_filter_with_error() {
1302 let log = NetworkLog::new();
1303 push_entry(&log, make_request("1", "GET", "https://a.com"), None, Some(make_error("1", "https://a.com", "net::ERR_FAILED"))).await;
1304 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1305 let f = log.filter(Filter::new().with_error()).await;
1306 assert_eq!(f.len(), 1);
1307 assert_eq!(f[0].request.id, "1");
1308 }
1309
1310 #[tokio::test]
1311 async fn test_filter_composition() {
1312 let log = NetworkLog::new();
1313 push_entry(&log, make_request("1", "POST", "https://api.example.com/login"), Some(make_response("1", 200, "https://api.example.com/login")), None).await;
1314 push_request(&log, make_request("2", "GET", "https://api.example.com/login")).await;
1315 let f = log.filter(Filter::new().method("POST").url_contains("login").with_response()).await;
1316 assert_eq!(f.len(), 1);
1317 assert_eq!(f[0].request.id, "1");
1318 }
1319
1320 #[tokio::test]
1321 async fn test_count() {
1322 let log = NetworkLog::new();
1323 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1324 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1325 assert_eq!(log.count(Filter::new().method("GET")).await, 1);
1326 assert_eq!(log.count(Filter::new()).await, 2);
1327 }
1328
1329 #[tokio::test]
1332 async fn test_completed() {
1333 let log = NetworkLog::new();
1334 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1335 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1336 push_entry(&log, make_request("3", "GET", "https://c.com"), None, Some(make_error("3", "https://c.com", "err"))).await;
1337 let c = log.completed().await;
1338 assert_eq!(c.len(), 2);
1339 }
1340
1341 #[tokio::test]
1344 async fn test_find_by_url() {
1345 let log = NetworkLog::new();
1346 push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1347 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1348 assert_eq!(log.find_by_url("api").await.unwrap().request.id, "1");
1349 assert!(log.find_by_url("notfound").await.is_none());
1350 }
1351
1352 #[tokio::test]
1353 async fn test_find_by_url_regex() {
1354 let log = NetworkLog::new();
1355 push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1356 push_request(&log, make_request("2", "GET", "https://other.com")).await;
1357 let re = regex::Regex::new(r"api\.\w+\.com").unwrap();
1358 assert_eq!(log.find_by_url_regex(&re).await.unwrap().request.id, "1");
1359 }
1360
1361 #[tokio::test]
1364 async fn test_endpoints() {
1365 let log = NetworkLog::new();
1366 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1367 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1368 push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1369 let ep = log.endpoints().await;
1370 assert_eq!(ep.len(), 3);
1371 }
1372
1373 #[tokio::test]
1374 async fn test_hostnames() {
1375 let log = NetworkLog::new();
1376 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1377 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1378 push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1379 let h = log.hostnames().await;
1380 assert_eq!(h.len(), 2);
1381 assert!(h.contains(&"a.com".into()));
1382 assert!(h.contains(&"b.com".into()));
1383 }
1384
1385 #[tokio::test]
1386 async fn test_distinct_methods() {
1387 let log = NetworkLog::new();
1388 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1389 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1390 push_request(&log, make_request("3", "GET", "https://c.com")).await;
1391 let m = log.distinct_methods().await;
1392 assert_eq!(m, vec!["GET", "POST"]);
1393 }
1394
1395 #[tokio::test]
1396 async fn test_distinct_statuses() {
1397 let log = NetworkLog::new();
1398 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1399 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 404, "https://b.com")), None).await;
1400 push_entry(&log, make_request("3", "GET", "https://c.com"), Some(make_response("3", 200, "https://c.com")), None).await;
1401 let s = log.distinct_statuses().await;
1402 assert_eq!(s, vec![200, 404]);
1403 }
1404
1405 #[tokio::test]
1406 async fn test_total_bytes() {
1407 let log = NetworkLog::new();
1408 let mut r1 = make_request("1", "POST", "https://a.com");
1409 r1.post_data = Some("hello".into());
1410 push_entry(&log, r1, Some(make_response("1", 200, "https://a.com")), None).await;
1411 let mut r2 = make_request("2", "POST", "https://b.com");
1412 r2.post_data = Some("world!!".into());
1413 push_entry(&log, r2, Some(make_response("2", 200, "https://b.com")), None).await;
1414 assert_eq!(log.total_bytes_in().await, 200);
1415 assert_eq!(log.total_bytes_out().await, 12); }
1417
1418 #[tokio::test]
1421 async fn test_entry_status_and_final_url() {
1422 let entry = NetworkEntry {
1423 request: make_request("1", "GET", "https://a.com"),
1424 response: Some(make_response("1", 301, "https://b.com")),
1425 error: None,
1426 };
1427 assert_eq!(entry.status(), Some(301));
1428 assert_eq!(entry.final_url(), "https://b.com");
1429 assert!(entry.has_response());
1430 assert!(!entry.is_error());
1431 }
1432
1433 #[tokio::test]
1434 async fn test_entry_request_header() {
1435 let req = make_request_with_headers("1", "GET", "https://a.com", vec![
1436 CapturedHeader { name: "X-Custom".into(), value: "123".into() },
1437 ]);
1438 let entry = NetworkEntry { request: req, response: None, error: None };
1439 assert_eq!(entry.request_header("x-custom"), Some("123"));
1440 assert_eq!(entry.request_header("missing"), None);
1441 }
1442
1443 #[tokio::test]
1444 async fn test_entry_response_header() {
1445 let req = make_request("1", "GET", "https://a.com");
1446 let resp = CapturedResponse {
1447 id: "1".into(),
1448 url: "https://a.com".into(),
1449 protocol: "h2".into(),
1450 status: 200,
1451 status_text: "OK".into(),
1452 headers: vec![CapturedHeader { name: "Content-Type".into(), value: "application/json".into() }],
1453 mime_type: "application/json".into(),
1454 body_size: Some(10),
1455 from_cache: false,
1456 };
1457 let entry = NetworkEntry { request: req, response: Some(resp), error: None };
1458 assert_eq!(entry.response_header("content-type"), Some("application/json"));
1459 assert_eq!(entry.response_header("missing"), None);
1460 }
1461
1462 #[tokio::test]
1463 async fn test_entry_to_curl() {
1464 let req = CapturedRequest {
1465 id: "1".into(),
1466 context: None,
1467 method: "POST".into(),
1468 url: "https://api.example.com/login".into(),
1469 headers: vec![
1470 CapturedHeader { name: "Content-Type".into(), value: "application/json".into() },
1471 CapturedHeader { name: "Host".into(), value: "api.example.com".into() },
1472 ],
1473 post_data: Some(r#"{"user":"admin"}"#.into()),
1474 timestamp: 0,
1475 destination: "document".into(),
1476 initiator_type: None,
1477 timing: CapturedTiming::default(),
1478 cookies: vec![],
1479 };
1480 let entry = NetworkEntry { request: req, response: None, error: None };
1481 let curl = entry.to_curl();
1482 assert!(curl.starts_with("curl -X POST"));
1483 assert!(curl.contains("-H 'Content-Type: application/json'"));
1484 assert!(!curl.contains("Host")); assert!(curl.contains(r#"-d '{"user":"admin"}'"#));
1486 assert!(curl.contains("'https://api.example.com/login'"));
1487 }
1488
1489 #[tokio::test]
1492 async fn test_request_json_body() {
1493 let mut req = make_request("1", "POST", "https://a.com");
1494 req.post_data = Some(r#"{"key":"value"}"#.into());
1495 assert_eq!(req.json_body(), Some(serde_json::json!({"key": "value"})));
1496 }
1497
1498 #[tokio::test]
1499 async fn test_request_json_body_invalid() {
1500 let mut req = make_request("1", "POST", "https://a.com");
1501 req.post_data = Some("not json".into());
1502 assert!(req.json_body().is_none());
1503 }
1504
1505 #[tokio::test]
1506 async fn test_request_query_params() {
1507 let req = make_request("1", "GET", "https://a.com?foo=bar&baz=qux");
1508 let params = req.query_params();
1509 assert_eq!(params.len(), 2);
1510 assert!(params.contains(&("foo".into(), "bar".into())));
1511 assert!(params.contains(&("baz".into(), "qux".into())));
1512 }
1513
1514 #[tokio::test]
1530 async fn test_max_entries_eviction() {
1531 let log = NetworkLog::with_limits(4, 10);
1532 for i in 0..6 {
1533 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
1534 }
1535 let entries = log.entries().await;
1540 let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
1541 assert_eq!(ids, vec!["2", "3", "4", "5"]);
1542
1543 let m = log.metrics().await;
1544 assert_eq!(m.entries_evicted, 2);
1545 }
1546
1547 #[tokio::test]
1550 async fn test_broadcast_receives_entries() {
1551 let log = NetworkLog::new();
1552 let mut rx = log.subscribe().await;
1553 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1554 let received = rx.recv().await.unwrap();
1555 assert_eq!(received.request.id, "1");
1556 }
1557
1558 #[tokio::test]
1559 async fn test_broadcast_multiple_receivers() {
1560 let log = NetworkLog::new();
1561 let mut rx1 = log.subscribe().await;
1562 let mut rx2 = log.subscribe().await;
1563 push_request(&log, make_request("1", "GET", "https://example.com")).await;
1564 assert_eq!(rx1.recv().await.unwrap().request.id, "1");
1565 assert_eq!(rx2.recv().await.unwrap().request.id, "1");
1566 }
1567
1568 #[tokio::test]
1571 async fn test_metrics_requests_received() {
1572 let log = NetworkLog::new();
1573 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1574 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1575 let m = log.metrics().await;
1576 assert_eq!(m.requests_received, 0); assert_eq!(m.max_entries, 50_000);
1578 }
1579
1580 #[tokio::test]
1583 async fn test_network_entry_serialize_roundtrip() {
1584 let entry = NetworkEntry {
1585 request: make_request("1", "GET", "https://example.com"),
1586 response: Some(make_response("1", 200, "https://example.com")),
1587 error: None,
1588 };
1589 let json = serde_json::to_string(&entry).unwrap();
1590 let de: NetworkEntry = serde_json::from_str(&json).unwrap();
1591 assert_eq!(de.request.id, entry.request.id);
1592 assert_eq!(de.status(), Some(200));
1593 }
1594
1595 #[tokio::test]
1598 async fn test_log_clone_shares_state() {
1599 let log1 = NetworkLog::new();
1600 let log2 = log1.clone();
1601 push_request(&log1, make_request("1", "GET", "https://example.com")).await;
1602 assert_eq!(log2.len().await, 1);
1603 }
1604
1605 #[tokio::test]
1608 async fn test_non_neg() {
1609 assert_eq!(non_neg(5.0), Some(5.0));
1610 assert_eq!(non_neg(-1.0), None);
1611 assert_eq!(non_neg(0.0), Some(0.0));
1612 }
1613
1614 #[tokio::test]
1615 async fn test_option_f64_ms() {
1616 assert_eq!(option_f64_ms(Some(10.0), Some(3.0)), 7.0);
1617 assert_eq!(option_f64_ms(Some(3.0), Some(10.0)), 0.0); assert_eq!(option_f64_ms(None, Some(3.0)), -1.0);
1619 }
1620
1621 #[tokio::test]
1624 async fn test_empty_filter_matches_all() {
1625 let log = NetworkLog::new();
1626 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1627 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1628 assert_eq!(log.filter(Filter::new()).await.len(), 2);
1629 assert_eq!(log.count(Filter::new()).await, 2);
1630 }
1631
1632 #[tokio::test]
1633 async fn test_eviction_rebuilds_by_id() {
1634 let log = NetworkLog::with_limits(4, 10);
1635 for i in 0..6 {
1636 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
1637 }
1638 let entries = log.entries().await;
1640 assert_eq!(entries.len(), 4);
1641 for e in &entries {
1642 assert!(log.find_by_url(&e.request.url).await.is_some());
1643 }
1644 }
1645
1646 #[tokio::test]
1647 async fn test_broadcast_dropped_receiver_does_not_panic() {
1648 let log = NetworkLog::new();
1649 {
1650 let _rx = log.subscribe().await;
1651 } push_request(&log, make_request("1", "GET", "https://example.com")).await;
1653 }
1655
1656 #[tokio::test]
1657 async fn test_save_to_json_empty() {
1658 let log = NetworkLog::new();
1659 let path = std::path::Path::new("/tmp/foxdriver_network_empty.json");
1660 log.save_to_json(path).await.unwrap();
1661 let content = std::fs::read_to_string(path).unwrap();
1662 assert_eq!(content.trim(), "[]");
1663 }
1664
1665 #[tokio::test]
1666 async fn test_save_as_har_empty() {
1667 let log = NetworkLog::new();
1668 let path = std::path::Path::new("/tmp/foxdriver_network_empty.har");
1669 log.save_as_har(path, Some("test")).await.unwrap();
1670 let content = std::fs::read_to_string(path).unwrap();
1671 assert!(content.contains("1.2"));
1672 assert!(content.contains("runtime_foxdriver"));
1673 }
1674
1675 #[tokio::test]
1676 async fn test_captured_timing_default() {
1677 let t = CapturedTiming::default();
1678 assert!(t.dns_start_ms.is_none());
1679 assert!(t.response_end_ms.is_none());
1680 }
1681
1682 #[tokio::test]
1683 async fn test_network_entry_no_response_final_url() {
1684 let entry = NetworkEntry {
1685 request: make_request("1", "GET", "https://a.com"),
1686 response: None,
1687 error: None,
1688 };
1689 assert_eq!(entry.final_url(), "https://a.com");
1690 assert_eq!(entry.status(), None);
1691 assert!(!entry.has_response());
1692 assert!(!entry.is_error());
1693 }
1694
1695 #[tokio::test]
1696 async fn test_distinct_methods_empty() {
1697 let log = NetworkLog::new();
1698 assert!(log.distinct_methods().await.is_empty());
1699 }
1700
1701 #[tokio::test]
1702 async fn test_distinct_statuses_empty() {
1703 let log = NetworkLog::new();
1704 assert!(log.distinct_statuses().await.is_empty());
1705 }
1706
1707 #[tokio::test]
1708 async fn test_hostnames_malformed_url() {
1709 let log = NetworkLog::new();
1710 push_request(&log, make_request("1", "GET", "not-a-url")).await;
1711 assert!(log.hostnames().await.is_empty());
1712 }
1713
1714 #[tokio::test]
1715 async fn test_total_bytes_no_response() {
1716 let log = NetworkLog::new();
1717 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1718 assert_eq!(log.total_bytes_in().await, 0);
1719 }
1720
1721 #[tokio::test]
1724 async fn test_contains_id() {
1725 let log = NetworkLog::new();
1726 push_request(&log, make_request("abc", "GET", "https://a.com")).await;
1727 assert!(log.contains_id("abc").await);
1728 assert!(!log.contains_id("xyz").await);
1729 }
1730
1731 #[tokio::test]
1732 async fn test_remove_by_id() {
1733 let log = NetworkLog::new();
1734 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1735 push_request(&log, make_request("2", "GET", "https://b.com")).await;
1736 push_request(&log, make_request("3", "GET", "https://c.com")).await;
1737
1738 let removed = log.remove_by_id("2").await;
1739 assert_eq!(removed.unwrap().request.id, "2");
1740 assert_eq!(log.len().await, 2);
1741 assert!(!log.contains_id("2").await);
1742
1743 assert!(log.contains_id("1").await);
1745 assert!(log.contains_id("3").await);
1746 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
1747 assert_eq!(log.nth(1).await.unwrap().request.id, "3");
1748 }
1749
1750 #[tokio::test]
1751 async fn test_remove_by_id_unknown() {
1752 let log = NetworkLog::new();
1753 assert!(log.remove_by_id("nope").await.is_none());
1754 }
1755
1756 #[tokio::test]
1759 async fn test_wait_for_url_existing() {
1760 let log = NetworkLog::new();
1761 push_request(&log, make_request("1", "GET", "https://target.com/page")).await;
1762 let found = log.wait_for_url("target.com", std::time::Duration::from_secs(1)).await;
1763 assert_eq!(found.unwrap().request.id, "1");
1764 }
1765
1766 #[tokio::test]
1767 async fn test_wait_for_url_future() {
1768 let log = NetworkLog::new();
1769 let log2 = log.clone();
1770 tokio::spawn(async move {
1771 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1772 push_request(&log2, make_request("1", "GET", "https://target.com/page")).await;
1773 });
1774 let found = log.wait_for_url("target.com", std::time::Duration::from_secs(1)).await;
1775 assert_eq!(found.unwrap().request.id, "1");
1776 }
1777
1778 #[tokio::test]
1779 async fn test_wait_for_url_timeout() {
1780 let log = NetworkLog::new();
1781 let found = log.wait_for_url("never", std::time::Duration::from_millis(50)).await;
1782 assert!(found.is_none());
1783 }
1784
1785 #[tokio::test]
1786 async fn test_wait_for_response_existing() {
1787 let log = NetworkLog::new();
1788 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1789 let found = log.wait_for_response("1", std::time::Duration::from_secs(1)).await;
1790 assert_eq!(found.unwrap().request.id, "1");
1791 }
1792
1793 #[tokio::test]
1794 async fn test_wait_for_response_future() {
1795 let log = NetworkLog::new();
1796 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1797 let log2 = log.clone();
1798 tokio::spawn(async move {
1799 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1800 let mut inner = log2.inner.write().await;
1802 if let Some(idx) = inner.by_id.get("1").copied() {
1803 let mut new_entry = (*inner.entries[idx]).clone();
1804 new_entry.response = Some(make_response("1", 200, "https://a.com"));
1805 let new_arc = Arc::new(new_entry);
1806 inner.entries[idx] = new_arc.clone();
1807 let _ = inner.tx.send(new_arc);
1808 }
1809 });
1810 let found = log.wait_for_response("1", std::time::Duration::from_secs(1)).await;
1811 assert_eq!(found.unwrap().request.id, "1");
1812 }
1813
1814 #[tokio::test]
1815 async fn test_wait_for_response_timeout() {
1816 let log = NetworkLog::new();
1817 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1818 let found = log.wait_for_response("1", std::time::Duration::from_millis(50)).await;
1819 assert!(found.is_none());
1820 }
1821
1822 #[tokio::test]
1825 async fn test_concurrent_pushes() {
1826 let log = NetworkLog::new();
1827 let mut handles = Vec::new();
1828 for t in 0..10 {
1829 let log = log.clone();
1830 handles.push(tokio::spawn(async move {
1831 for i in 0..100 {
1832 push_request(&log, make_request(&format!("{}-{}", t, i), "GET", "https://example.com")).await;
1833 }
1834 }));
1835 }
1836 for h in handles {
1837 h.await.unwrap();
1838 }
1839 assert_eq!(log.len().await, 1000);
1840 }
1841
1842 #[tokio::test]
1843 async fn test_concurrent_read_while_write() {
1844 let log = NetworkLog::new();
1845 let log2 = log.clone();
1846
1847 let writer = tokio::spawn(async move {
1848 for i in 0..500 {
1849 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
1850 if i % 50 == 0 {
1851 tokio::task::yield_now().await;
1852 }
1853 }
1854 });
1855
1856 let reader = tokio::spawn(async move {
1857 let mut last_len = 0;
1858 for _ in 0..50 {
1859 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1860 let len = log2.len().await;
1861 assert!(len >= last_len); last_len = len;
1863 }
1864 });
1865
1866 let (r1, r2) = tokio::join!(writer, reader);
1867 r1.unwrap();
1868 r2.unwrap();
1869 }
1870
1871 #[tokio::test]
1874 async fn test_save_to_json_roundtrip() {
1875 let log = NetworkLog::new();
1876 push_entry(&log, make_request("1", "GET", "https://example.com"), Some(make_response("1", 200, "https://example.com")), None).await;
1877 let path = std::path::Path::new("/tmp/foxdriver_network_real.json");
1878 log.save_to_json(path).await.unwrap();
1879 let content = std::fs::read_to_string(path).unwrap();
1880 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
1881 assert_eq!(parsed.len(), 1);
1882 assert_eq!(parsed[0].request.id, "1");
1883 assert_eq!(parsed[0].status(), Some(200));
1884 }
1885
1886 #[tokio::test]
1887 async fn test_save_as_har_with_data() {
1888 let log = NetworkLog::new();
1889 let mut req = make_request("1", "POST", "https://api.example.com/data?key=val");
1890 req.post_data = Some(r#"{"hello":"world"}"#.into());
1891 req.headers = vec![
1892 CapturedHeader { name: "Content-Type".into(), value: "application/json".into() },
1893 ];
1894 push_entry(&log, req, Some(make_response("1", 201, "https://api.example.com/data?key=val")), None).await;
1895
1896 let path = std::path::Path::new("/tmp/foxdriver_network_real.har");
1897 log.save_as_har(path, Some("test page")).await.unwrap();
1898 let content = std::fs::read_to_string(path).unwrap();
1899 assert!(content.contains("1.2"));
1900 assert!(content.contains("POST"));
1901 assert!(content.contains("201"));
1902 assert!(content.contains("key=val"));
1903 assert!(content.contains("hello"));
1904 assert!(content.contains("test page"));
1905 }
1906
1907 #[tokio::test]
1910 async fn test_to_curl_no_headers_no_body() {
1911 let req = make_request("1", "GET", "https://example.com");
1912 let entry = NetworkEntry { request: req, response: None, error: None };
1913 let curl = entry.to_curl();
1914 assert_eq!(curl, "curl -X GET 'https://example.com'");
1915 }
1916
1917 #[tokio::test]
1918 async fn test_to_curl_special_chars() {
1919 let req = CapturedRequest {
1920 id: "1".into(),
1921 context: None,
1922 method: "POST".into(),
1923 url: "https://example.com?a=1&b=2".into(),
1924 headers: vec![CapturedHeader { name: "X-Special".into(), value: "val'ue\".txt".into() }],
1925 post_data: Some("data='quoted'".into()),
1926 timestamp: 0,
1927 destination: "document".into(),
1928 initiator_type: None,
1929 timing: CapturedTiming::default(),
1930 cookies: vec![],
1931 };
1932 let entry = NetworkEntry { request: req, response: None, error: None };
1933 let curl = entry.to_curl();
1934 assert!(curl.contains("-H 'X-Special: val'\\''ue\".txt'"));
1935 assert!(curl.contains("-d 'data='\\''quoted'\\'''"));
1936 }
1937
1938 #[tokio::test]
1945 async fn test_filter_empty_method_matches_all() {
1946 let log = NetworkLog::new();
1947 push_request(&log, make_request("1", "GET", "https://a.com")).await;
1948 push_request(&log, make_request("2", "POST", "https://b.com")).await;
1949 assert_eq!(log.filter(Filter::new()).await.len(), 2);
1951 }
1952
1953 #[tokio::test]
1954 async fn test_filter_status_range_no_matches() {
1955 let log = NetworkLog::new();
1956 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
1957 let f = log.filter(Filter::new().status_range(500..=599)).await;
1958 assert!(f.is_empty());
1959 }
1960
1961 #[tokio::test]
1962 async fn test_filter_url_contains_case_insensitive() {
1963 let log = NetworkLog::new();
1964 push_request(&log, make_request("1", "GET", "https://API.example.com")).await;
1965 let f = log.filter(Filter::new().url_contains("api")).await;
1966 assert_eq!(f.len(), 1);
1967 }
1968
1969 #[tokio::test]
1970 async fn test_filter_destination_case_insensitive() {
1971 let log = NetworkLog::new();
1972 let mut r = make_request("1", "GET", "https://a.com");
1973 r.destination = "IMAGE".into();
1974 push_request(&log, r).await;
1975 let f = log.filter(Filter::new().destination("image")).await;
1976 assert_eq!(f.len(), 1);
1977 }
1978
1979 #[tokio::test]
1982 async fn test_query_params_empty() {
1983 let req = make_request("1", "GET", "https://a.com");
1984 assert!(req.query_params().is_empty());
1985 }
1986
1987 #[tokio::test]
1988 async fn test_query_params_url_encoded() {
1989 let req = make_request("1", "GET", "https://a.com?foo=%20bar&baz=qux");
1990 let params = req.query_params();
1991 assert!(params.contains(&("foo".into(), " bar".into())));
1992 assert!(params.contains(&("baz".into(), "qux".into())));
1993 }
1994
1995 #[tokio::test]
1998 async fn test_multiple_evictions() {
1999 let log = NetworkLog::with_limits(4, 10);
2000 for i in 0..20 {
2001 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
2002 }
2003 let entries = log.entries().await;
2004 assert_eq!(entries.len(), 4);
2005 let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
2006 assert_eq!(ids, vec!["16", "17", "18", "19"]);
2007
2008 let m = log.metrics().await;
2009 assert_eq!(m.entries_evicted, 16);
2010 }
2011
2012 #[tokio::test]
2015 async fn test_request_header_multiple_same_name() {
2016 let req = make_request_with_headers("1", "GET", "https://a.com", vec![
2017 CapturedHeader { name: "X-Dup".into(), value: "first".into() },
2018 CapturedHeader { name: "X-Dup".into(), value: "second".into() },
2019 ]);
2020 assert_eq!(req.request_header("x-dup"), Some("first")); }
2022
2023 #[tokio::test]
2024 async fn test_response_header_no_response() {
2025 let entry = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: None, error: None };
2026 assert_eq!(entry.response_header("anything"), None);
2027 }
2028
2029 #[tokio::test]
2032 async fn test_option_f64_ms_both_none() {
2033 assert_eq!(option_f64_ms(None, None), -1.0);
2034 }
2035
2036 #[tokio::test]
2037 async fn test_option_f64_ms_end_none() {
2038 assert_eq!(option_f64_ms(None, Some(5.0)), -1.0);
2039 }
2040
2041 fn make_bidi_request_data(id: &str, url: &str, method: &str) -> rustenium_bidi_definitions::network::types::RequestData {
2044 use rustenium_bidi_definitions::network::types::{FetchTimingInfo, Request, RequestData};
2045 RequestData {
2046 request: Request::new(id),
2047 url: url.into(),
2048 method: method.into(),
2049 headers: vec![],
2050 cookies: vec![],
2051 headers_size: 0,
2052 body_size: None,
2053 destination: "document".into(),
2054 initiator_type: None,
2055 timings: FetchTimingInfo {
2056 time_origin: 0.0,
2057 request_time: 0.0,
2058 redirect_start: 0.0,
2059 redirect_end: 0.0,
2060 fetch_start: 0.0,
2061 dns_start: 0.0,
2062 dns_end: 0.0,
2063 connect_start: 0.0,
2064 connect_end: 0.0,
2065 tls_start: 0.0,
2066 request_start: 0.0,
2067 response_start: 0.0,
2068 response_end: 0.0,
2069 },
2070 extensible: std::collections::HashMap::new(),
2071 }
2072 }
2073
2074 fn make_before_request_sent(id: &str, url: &str, method: &str) -> BeforeRequestSent {
2075 use rustenium_bidi_definitions::network::events::BeforeRequestSentParams;
2076 use rustenium_bidi_definitions::network::types::BaseParameters;
2077 BeforeRequestSent {
2078 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
2079 params: BeforeRequestSentParams {
2080 base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, method), 0u64),
2081 initiator: None,
2082 },
2083 }
2084 }
2085
2086 fn make_response_completed(id: &str, url: &str, status: u64) -> ResponseCompleted {
2087 use rustenium_bidi_definitions::network::events::ResponseCompletedParams;
2088 use rustenium_bidi_definitions::network::types::{BaseParameters, ResponseData, ResponseContent};
2089 ResponseCompleted {
2090 method: rustenium_bidi_definitions::network::events::ResponseCompletedMethod::ResponseCompleted,
2091 params: ResponseCompletedParams {
2092 base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, "GET"), 0u64),
2093 response: ResponseData {
2094 url: url.into(),
2095 protocol: "h2".into(),
2096 status,
2097 status_text: "OK".into(),
2098 from_cache: false,
2099 headers: vec![],
2100 mime_type: "application/json".into(),
2101 bytes_received: 100,
2102 headers_size: None,
2103 body_size: Some(100),
2104 content: ResponseContent::new(100u64),
2105 auth_challenges: None,
2106 },
2107 },
2108 }
2109 }
2110
2111 fn make_fetch_error(id: &str, url: &str, error_text: &str) -> FetchError {
2112 use rustenium_bidi_definitions::network::events::FetchErrorParams;
2113 use rustenium_bidi_definitions::network::types::BaseParameters;
2114 FetchError {
2115 method: rustenium_bidi_definitions::network::events::FetchErrorMethod::FetchError,
2116 params: FetchErrorParams {
2117 base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, "GET"), 0u64),
2118 error_text: error_text.into(),
2119 },
2120 }
2121 }
2122
2123 #[tokio::test]
2124 async fn test_ingest_before_request_sent_creates_entry() {
2125 let log = NetworkLog::new();
2126 let evt = make_before_request_sent("req-1", "https://example.com", "GET");
2127 log.ingest_before_request_sent(&evt).await;
2128 assert_eq!(log.len().await, 1);
2129 assert!(log.contains_id("req-1").await);
2130 let m = log.metrics().await;
2131 assert_eq!(m.requests_received, 1);
2132 }
2133
2134 #[tokio::test]
2135 async fn test_ingest_response_completed_attaches_to_existing() {
2136 let log = NetworkLog::new();
2137 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://example.com", "GET")).await;
2138 log.ingest_response_completed(&make_response_completed("req-1", "https://example.com", 200)).await;
2139
2140 let entry = log.first().await.unwrap();
2141 assert_eq!(entry.status(), Some(200));
2142 assert!(entry.has_response());
2143 let m = log.metrics().await;
2144 assert_eq!(m.responses_received, 1);
2145 }
2146
2147 #[tokio::test]
2148 async fn test_ingest_fetch_error_attaches_to_existing() {
2149 let log = NetworkLog::new();
2150 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://example.com", "GET")).await;
2151 log.ingest_fetch_error(&make_fetch_error("req-1", "https://example.com", "net::ERR_FAILED")).await;
2152
2153 let entry = log.first().await.unwrap();
2154 assert!(entry.is_error());
2155 assert_eq!(entry.error.as_ref().unwrap().error_text, "net::ERR_FAILED");
2156 let m = log.metrics().await;
2157 assert_eq!(m.errors_received, 1);
2158 }
2159
2160 #[tokio::test]
2161 async fn test_out_of_order_response_reconciliation() {
2162 let log = NetworkLog::new();
2163 log.ingest_response_completed(&make_response_completed("req-1", "https://example.com", 200)).await;
2165 assert_eq!(log.len().await, 0); assert_eq!(log.metrics().await.responses_received, 1);
2167
2168 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://example.com", "GET")).await;
2170 assert_eq!(log.len().await, 1);
2171 let entry = log.first().await.unwrap();
2172 assert_eq!(entry.status(), Some(200));
2173 }
2174
2175 #[tokio::test]
2176 async fn test_out_of_order_error_reconciliation() {
2177 let log = NetworkLog::new();
2178 log.ingest_fetch_error(&make_fetch_error("req-1", "https://example.com", "net::ERR_ABORTED")).await;
2179 assert_eq!(log.len().await, 0);
2180
2181 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://example.com", "GET")).await;
2182 let entry = log.first().await.unwrap();
2183 assert!(entry.is_error());
2184 }
2185
2186 #[tokio::test]
2187 async fn test_response_update_rebroadcasts() {
2188 let log = NetworkLog::new();
2189 let mut rx = log.subscribe().await;
2190
2191 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://example.com", "GET")).await;
2192 let first = rx.recv().await.unwrap();
2193 assert!(!first.has_response());
2194
2195 log.ingest_response_completed(&make_response_completed("req-1", "https://example.com", 200)).await;
2196 let updated = rx.recv().await.unwrap();
2197 assert!(updated.has_response());
2198 assert_eq!(updated.status(), Some(200));
2199 }
2200
2201 #[tokio::test]
2202 async fn test_pending_response_overflow() {
2203 let log = NetworkLog::with_limits(100, 3);
2204 for i in 0..5 {
2206 log.ingest_response_completed(&make_response_completed(&format!("req-{}", i), "https://example.com", 200)).await;
2207 }
2208 let m = log.metrics().await;
2209 assert_eq!(m.pending_responses_dropped, 2); assert_eq!(m.responses_received, 5);
2211 }
2212
2213 #[tokio::test]
2214 async fn test_pending_error_overflow() {
2215 let log = NetworkLog::with_limits(100, 2);
2216 for i in 0..4 {
2217 log.ingest_fetch_error(&make_fetch_error(&format!("req-{}", i), "https://example.com", "err")).await;
2218 }
2219 let m = log.metrics().await;
2220 assert_eq!(m.pending_errors_dropped, 2); assert_eq!(m.errors_received, 4);
2222 }
2223
2224 #[tokio::test]
2225 async fn test_clear_then_ingest() {
2226 let log = NetworkLog::new();
2227 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
2228 log.clear().await;
2229 log.ingest_before_request_sent(&make_before_request_sent("req-2", "https://b.com", "POST")).await;
2230 assert_eq!(log.len().await, 1);
2231 assert!(!log.contains_id("req-1").await);
2232 assert!(log.contains_id("req-2").await);
2233 }
2234
2235 #[tokio::test]
2236 async fn test_entry_with_both_response_and_error() {
2237 let log = NetworkLog::new();
2238 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
2239 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200)).await;
2240 log.ingest_fetch_error(&make_fetch_error("req-1", "https://a.com", "net::ERR_FAILED")).await;
2241
2242 let entry = log.first().await.unwrap();
2243 assert!(entry.has_response());
2244 assert!(entry.is_error());
2245 assert_eq!(entry.status(), Some(200));
2246 }
2247
2248 #[tokio::test]
2251 async fn test_captured_cookie_roundtrip() {
2252 let cookie = CapturedCookie {
2253 name: "session".into(),
2254 value: "abc123".into(),
2255 domain: ".example.com".into(),
2256 path: "/".into(),
2257 size: 42,
2258 http_only: true,
2259 secure: true,
2260 same_site: "strict".into(),
2261 };
2262 let json = serde_json::to_string(&cookie).unwrap();
2263 let de: CapturedCookie = serde_json::from_str(&json).unwrap();
2264 assert_eq!(de, cookie);
2265 }
2266
2267 #[tokio::test]
2268 async fn test_captured_header_roundtrip() {
2269 let h = CapturedHeader { name: "X-Test".into(), value: "value".into() };
2270 let json = serde_json::to_string(&h).unwrap();
2271 let de: CapturedHeader = serde_json::from_str(&json).unwrap();
2272 assert_eq!(de, h);
2273 }
2274
2275 #[tokio::test]
2276 async fn test_filter_url_regex_invalid_pattern() {
2277 let result = Filter::new().url_regex("[");
2278 assert!(result.is_err());
2279 }
2280
2281 #[tokio::test]
2282 async fn test_large_url_does_not_panic() {
2283 let log = NetworkLog::new();
2284 let long_url = format!("https://example.com/{}", "a".repeat(10000));
2285 push_request(&log, make_request("1", "GET", &long_url)).await;
2286 assert_eq!(log.len().await, 1);
2287 assert!(log.find_by_url("example.com").await.is_some());
2288 }
2289
2290 #[tokio::test]
2291 async fn test_eviction_with_subscriber() {
2292 let log = NetworkLog::with_limits(2, 10);
2293 let mut rx = log.subscribe().await;
2294 for i in 0..5 {
2295 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
2296 }
2297 let mut count = 0;
2299 while let Ok(_) = rx.try_recv() {
2300 count += 1;
2301 }
2302 assert_eq!(count, 5);
2303 }
2304
2305 #[tokio::test]
2308 async fn test_filter_method_case_insensitive() {
2309 let log = NetworkLog::new();
2310 push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
2311 assert_eq!(log.filter(Filter::new().method("delete")).await.len(), 1);
2312 assert_eq!(log.filter(Filter::new().method("DELETE")).await.len(), 1);
2313 }
2314
2315 #[tokio::test]
2316 async fn test_filter_header_in_response() {
2317 let log = NetworkLog::new();
2318 let req = make_request("1", "GET", "https://a.com");
2319 let mut resp = make_response("1", 200, "https://a.com");
2320 resp.headers = vec![CapturedHeader { name: "X-Resp".into(), value: "secret-val".into() }];
2321 push_entry(&log, req, Some(resp), None).await;
2322 let f = log.filter(Filter::new().header("x-resp", "secret")).await;
2323 assert_eq!(f.len(), 1);
2324 }
2325
2326 #[tokio::test]
2327 async fn test_filter_composition_conflicting() {
2328 let log = NetworkLog::new();
2329 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2330 let f = log.filter(Filter::new().method("GET").method("POST")).await;
2332 assert!(f.is_empty());
2333 }
2334
2335 #[tokio::test]
2336 async fn test_count_zero() {
2337 let log = NetworkLog::new();
2338 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2339 assert_eq!(log.count(Filter::new().method("POST")).await, 0);
2340 }
2341
2342 #[tokio::test]
2343 async fn test_is_empty_false() {
2344 let log = NetworkLog::new();
2345 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2346 assert!(!log.is_empty().await);
2347 }
2348
2349 #[tokio::test]
2350 async fn test_endpoints_dedupes() {
2351 let log = NetworkLog::new();
2352 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2353 push_request(&log, make_request("2", "GET", "https://a.com/x")).await;
2354 push_request(&log, make_request("3", "GET", "https://a.com/y")).await;
2355 let ep = log.endpoints().await;
2356 assert_eq!(ep.len(), 2);
2357 }
2358
2359 #[tokio::test]
2360 async fn test_hostnames_with_port() {
2361 let log = NetworkLog::new();
2362 push_request(&log, make_request("1", "GET", "https://example.com:8443/path")).await;
2363 let h = log.hostnames().await;
2364 assert_eq!(h.len(), 1);
2365 assert!(h.contains(&"example.com".into()));
2366 }
2367
2368 #[tokio::test]
2369 async fn test_total_bytes_out_none() {
2370 let log = NetworkLog::new();
2371 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2372 assert_eq!(log.total_bytes_out().await, 0);
2373 }
2374
2375 #[tokio::test]
2376 async fn test_completed_mixed_states() {
2377 let log = NetworkLog::new();
2378 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
2379 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2380 push_entry(&log, make_request("3", "GET", "https://c.com"), None, Some(make_error("3", "https://c.com", "err"))).await;
2381 push_entry(&log, make_request("4", "GET", "https://d.com"), Some(make_response("4", 500, "https://d.com")), Some(make_error("4", "https://d.com", "err"))).await;
2382 let c = log.completed().await;
2383 assert_eq!(c.len(), 3);
2384 }
2385
2386 #[tokio::test]
2387 async fn test_distinct_statuses_with_no_response() {
2388 let log = NetworkLog::new();
2389 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
2390 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2391 push_entry(&log, make_request("3", "GET", "https://c.com"), Some(make_response("3", 404, "https://c.com")), None).await;
2392 let s = log.distinct_statuses().await;
2393 assert_eq!(s, vec![200, 404]);
2394 }
2395
2396 #[tokio::test]
2397 async fn test_distinct_methods_dedupes() {
2398 let log = NetworkLog::new();
2399 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2400 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2401 push_request(&log, make_request("3", "POST", "https://c.com")).await;
2402 push_request(&log, make_request("4", "POST", "https://d.com")).await;
2403 let m = log.distinct_methods().await;
2404 assert_eq!(m, vec!["GET", "POST"]);
2405 }
2406
2407 #[tokio::test]
2408 async fn test_find_by_url_empty_substring() {
2409 let log = NetworkLog::new();
2410 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2411 assert!(log.find_by_url("").await.is_some());
2413 }
2414
2415 #[tokio::test]
2416 async fn test_find_by_url_regex_no_match() {
2417 let log = NetworkLog::new();
2418 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2419 let re = regex::Regex::new(r"zzz").unwrap();
2420 assert!(log.find_by_url_regex(&re).await.is_none());
2421 }
2422
2423 #[tokio::test]
2424 async fn test_filter_url_contains_empty() {
2425 let log = NetworkLog::new();
2426 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2427 let f = log.filter(Filter::new().url_contains("")).await;
2428 assert_eq!(f.len(), 1);
2429 }
2430
2431 #[tokio::test]
2432 async fn test_to_curl_binary_body() {
2433 let req = CapturedRequest {
2434 id: "1".into(), context: None, method: "POST".into(),
2435 url: "https://example.com".into(), headers: vec![],
2436 post_data: Some("\x00\x01\x02".into()),
2437 timestamp: 0, destination: "document".into(),
2438 initiator_type: None, timing: CapturedTiming::default(), cookies: vec![],
2439 };
2440 let entry = NetworkEntry { request: req, response: None, error: None };
2441 let curl = entry.to_curl();
2442 assert!(curl.contains("-d '"));
2443 }
2444
2445 #[tokio::test]
2446 async fn test_json_body_empty_string() {
2447 let mut req = make_request("1", "POST", "https://a.com");
2448 req.post_data = Some("".into());
2449 assert!(req.json_body().is_none());
2450 }
2451
2452 #[tokio::test]
2453 async fn test_query_params_duplicate_keys() {
2454 let req = make_request("1", "GET", "https://a.com?foo=1&foo=2");
2455 let params = req.query_params();
2456 assert_eq!(params.len(), 2);
2457 assert!(params.contains(&("foo".into(), "1".into())));
2458 assert!(params.contains(&("foo".into(), "2".into())));
2459 }
2460
2461 #[tokio::test]
2462 async fn test_request_header_empty_name() {
2463 let req = make_request_with_headers("1", "GET", "https://a.com", vec![]);
2464 assert_eq!(req.request_header(""), None);
2465 }
2466
2467 #[tokio::test]
2468 async fn test_response_header_empty_name() {
2469 let entry = NetworkEntry {
2470 request: make_request("1", "GET", "https://a.com"),
2471 response: Some(make_response("1", 200, "https://a.com")),
2472 error: None,
2473 };
2474 assert_eq!(entry.response_header(""), None);
2475 }
2476
2477 #[tokio::test]
2478 async fn test_remove_by_id_first() {
2479 let log = NetworkLog::new();
2480 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2481 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2482 log.remove_by_id("1").await;
2483 assert_eq!(log.first().await.unwrap().request.id, "2");
2484 }
2485
2486 #[tokio::test]
2487 async fn test_remove_by_id_last() {
2488 let log = NetworkLog::new();
2489 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2490 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2491 log.remove_by_id("2").await;
2492 assert_eq!(log.last().await.unwrap().request.id, "1");
2493 }
2494
2495 #[tokio::test]
2496 async fn test_contains_id_after_eviction() {
2497 let log = NetworkLog::with_limits(4, 10);
2498 for i in 0..10 {
2499 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
2500 }
2501 for i in 0..4 {
2503 assert!(!log.contains_id(&format!("{}", i)).await);
2504 }
2505 for i in 6..10 {
2506 assert!(log.contains_id(&format!("{}", i)).await);
2507 }
2508 }
2509
2510 #[tokio::test]
2511 async fn test_wait_for_response_error_instead_of_response() {
2512 let log = NetworkLog::new();
2513 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2514 let log2 = log.clone();
2515 tokio::spawn(async move {
2516 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2517 let mut inner = log2.inner.write().await;
2518 if let Some(idx) = inner.by_id.get("1").copied() {
2519 let mut new_entry = (*inner.entries[idx]).clone();
2520 new_entry.error = Some(make_error("1", "https://a.com", "net::ERR_ABORTED"));
2521 let new_arc = Arc::new(new_entry);
2522 inner.entries[idx] = new_arc.clone();
2523 let _ = inner.tx.send(new_arc);
2524 }
2525 });
2526 let found = log.wait_for_response("1", std::time::Duration::from_secs(1)).await;
2527 assert!(found.unwrap().is_error());
2528 }
2529
2530 #[tokio::test]
2531 async fn test_captured_timing_from_bidi() {
2532 use rustenium_bidi_definitions::network::types::FetchTimingInfo;
2533 let info = FetchTimingInfo {
2534 time_origin: 0.0,
2535 request_time: 10.0,
2536 redirect_start: 0.0,
2537 redirect_end: 0.0,
2538 fetch_start: 10.0,
2539 dns_start: 11.0,
2540 dns_end: 12.0,
2541 connect_start: 12.0,
2542 connect_end: 14.0,
2543 tls_start: 13.0,
2544 request_start: 14.0,
2545 response_start: 15.0,
2546 response_end: 16.0,
2547 };
2548 let t = CapturedTiming::from(&info);
2549 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)); }
2557
2558 #[tokio::test]
2559 async fn test_captured_timing_negative_clamped() {
2560 use rustenium_bidi_definitions::network::types::FetchTimingInfo;
2561 let info = FetchTimingInfo {
2562 time_origin: 0.0,
2563 request_time: 10.0,
2564 redirect_start: 0.0,
2565 redirect_end: 0.0,
2566 fetch_start: 10.0,
2567 dns_start: 9.0, dns_end: 8.0, connect_start: 0.0,
2570 connect_end: 0.0,
2571 tls_start: 0.0,
2572 request_start: 0.0,
2573 response_start: 0.0,
2574 response_end: 0.0,
2575 };
2576 let t = CapturedTiming::from(&info);
2577 assert_eq!(t.dns_start_ms, None); assert_eq!(t.dns_end_ms, None);
2579 }
2580
2581
2582 #[tokio::test]
2583 async fn test_har_timings_correct() {
2584 let log = NetworkLog::new();
2585 let mut req = make_request("1", "GET", "https://api.example.com");
2586 req.timing = CapturedTiming {
2587 dns_start_ms: Some(1.0),
2588 dns_end_ms: Some(3.0),
2589 connect_start_ms: Some(3.0),
2590 connect_end_ms: Some(7.0),
2591 tls_start_ms: Some(5.0),
2592 response_start_ms: Some(8.0),
2593 response_end_ms: Some(10.0),
2594 };
2595 push_entry(&log, req, Some(make_response("1", 200, "https://api.example.com")), None).await;
2596
2597 let path = std::path::Path::new("/tmp/foxdriver_har_timings.har");
2598 log.save_as_har(path, None).await.unwrap();
2599 let content = std::fs::read_to_string(path).unwrap();
2600 assert!(content.contains("\"dns\": 2.0") || content.contains("\"dns\": 2"));
2602 assert!(content.contains("\"connect\": 4.0") || content.contains("\"connect\": 4"));
2604 assert!(content.contains("\"ssl\": 2.0") || content.contains("\"ssl\": 2"));
2606 }
2607
2608 #[tokio::test]
2609 async fn test_concurrent_ingestion_stress() {
2610 let log = NetworkLog::new();
2611 let mut handles = Vec::new();
2612 for t in 0..20 {
2613 let log = log.clone();
2614 handles.push(tokio::spawn(async move {
2615 for i in 0..50 {
2616 let id = format!("{}-{}", t, i);
2617 log.ingest_before_request_sent(&make_before_request_sent(&id, "https://example.com", "GET")).await;
2618 if i % 2 == 0 {
2619 log.ingest_response_completed(&make_response_completed(&id, "https://example.com", 200)).await;
2620 } else {
2621 log.ingest_fetch_error(&make_fetch_error(&id, "https://example.com", "err")).await;
2622 }
2623 }
2624 }));
2625 }
2626 for h in handles {
2627 h.await.unwrap();
2628 }
2629 assert_eq!(log.len().await, 1000);
2630 let completed = log.completed().await;
2631 assert_eq!(completed.len(), 1000); let m = log.metrics().await;
2633 assert_eq!(m.requests_received, 1000);
2634 assert_eq!(m.responses_received, 500);
2635 assert_eq!(m.errors_received, 500);
2636 }
2637
2638 #[tokio::test]
2639 async fn test_property_filter_then_count_matches() {
2640 let log = NetworkLog::new();
2641 for i in 0..100 {
2642 let method = if i % 2 == 0 { "GET" } else { "POST" };
2643 push_request(&log, make_request(&format!("{}", i), method, "https://example.com")).await;
2644 }
2645 let f = Filter::new().method("GET");
2646 let filtered = log.filter(f.clone()).await;
2647 let counted = log.count(f).await;
2648 assert_eq!(filtered.len(), counted);
2649 assert_eq!(counted, 50);
2650 }
2651
2652 #[tokio::test]
2653 async fn test_property_first_last_consistency() {
2654 let log = NetworkLog::new();
2655 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2656 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2657 push_request(&log, make_request("3", "GET", "https://c.com")).await;
2658
2659 assert_eq!(log.first().await.unwrap().request.id, log.nth(0).await.unwrap().request.id);
2660 assert_eq!(log.last().await.unwrap().request.id, log.nth(2).await.unwrap().request.id);
2661 }
2662
2663 #[tokio::test]
2664 async fn test_property_endpoints_is_subset_of_entries() {
2665 let log = NetworkLog::new();
2666 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2667 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
2668 let entries = log.entries().await;
2669 let endpoints = log.endpoints().await;
2670 assert!(endpoints.len() <= entries.len());
2671 for ep in &endpoints {
2672 assert!(entries.iter().any(|e| e.request.url == *ep));
2673 }
2674 }
2675
2676 #[tokio::test]
2677 async fn test_property_hostnames_is_subset_of_endpoints() {
2678 let log = NetworkLog::new();
2679 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2680 push_request(&log, make_request("2", "GET", "https://b.com/y")).await;
2681 let hostnames = log.hostnames().await;
2682 let endpoints = log.endpoints().await;
2683 for h in &hostnames {
2684 assert!(endpoints.iter().any(|e| e.contains(h)));
2685 }
2686 }
2687
2688 #[tokio::test]
2689 async fn test_stress_many_entries() {
2690 let log = NetworkLog::with_limits(100_000, 10_000);
2691 for i in 0..10_000 {
2692 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://example.com/{}", i))).await;
2693 }
2694 assert_eq!(log.len().await, 10_000);
2695 assert!(log.find_by_url("example.com").await.is_some());
2696 assert_eq!(log.distinct_methods().await, vec!["GET"]);
2697 }
2698
2699 #[tokio::test]
2702 async fn test_handler_ignores_non_network_events() {
2703 let log = NetworkLog::new();
2704 let mut handler = make_network_handler(log.clone());
2705 let evt = rustenium_bidi_definitions::Event::Log(
2707 rustenium_bidi_definitions::log::events::LogEvent::EntryAdded(
2708 rustenium_bidi_definitions::log::events::EntryAdded {
2709 method: rustenium_bidi_definitions::log::events::EntryAddedMethod::EntryAdded,
2710 params: rustenium_bidi_definitions::log::events::EntryAddedParams {},
2711 }
2712 )
2713 );
2714 handler(evt).await;
2715 assert!(log.is_empty().await);
2716 }
2717
2718 #[tokio::test]
2719 async fn test_handler_processes_before_request_sent() {
2720 let log = NetworkLog::new();
2721 let mut handler = make_network_handler(log.clone());
2722 let evt = rustenium_bidi_definitions::Event::Network(
2723 rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
2724 make_before_request_sent("req-1", "https://example.com", "GET")
2725 )
2726 );
2727 handler(evt).await;
2728 assert_eq!(log.len().await, 1);
2729 assert!(log.contains_id("req-1").await);
2730 }
2731
2732 #[tokio::test]
2733 async fn test_handler_processes_response_completed() {
2734 let log = NetworkLog::new();
2735 let mut handler = make_network_handler(log.clone());
2736 handler(rustenium_bidi_definitions::Event::Network(
2737 rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
2738 make_before_request_sent("req-1", "https://example.com", "GET")
2739 )
2740 )).await;
2741 handler(rustenium_bidi_definitions::Event::Network(
2742 rustenium_bidi_definitions::network::events::NetworkEvent::ResponseCompleted(
2743 make_response_completed("req-1", "https://example.com", 200)
2744 )
2745 )).await;
2746 let entry = log.first().await.unwrap();
2747 assert_eq!(entry.status(), Some(200));
2748 }
2749
2750 #[tokio::test]
2751 async fn test_handler_ignores_unknown_network_events() {
2752 let log = NetworkLog::new();
2753 let mut handler = make_network_handler(log.clone());
2754 handler(rustenium_bidi_definitions::Event::Network(
2756 rustenium_bidi_definitions::network::events::NetworkEvent::ResponseStarted(
2757 rustenium_bidi_definitions::network::events::ResponseStarted {
2758 method: rustenium_bidi_definitions::network::events::ResponseStartedMethod::ResponseStarted,
2759 params: rustenium_bidi_definitions::network::events::ResponseStartedParams {
2760 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(
2761 false, 0u64, make_bidi_request_data("req-1", "https://example.com", "GET"), 0u64
2762 ),
2763 response: rustenium_bidi_definitions::network::types::ResponseData {
2764 url: "https://example.com".into(),
2765 protocol: "h2".into(),
2766 status: 200,
2767 status_text: "OK".into(),
2768 from_cache: false,
2769 headers: vec![],
2770 mime_type: "application/json".into(),
2771 bytes_received: 100,
2772 headers_size: None,
2773 body_size: Some(100),
2774 content: rustenium_bidi_definitions::network::types::ResponseContent::new(100u64),
2775 auth_challenges: None,
2776 },
2777 },
2778 }
2779 )
2780 )).await;
2781 assert!(log.is_empty().await);
2782 }
2783
2784 #[tokio::test]
2787 async fn test_har_with_null_response() {
2788 let log = NetworkLog::new();
2789 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2790 let path = std::path::Path::new("/tmp/foxdriver_har_null_resp.har");
2791 log.save_as_har(path, None).await.unwrap();
2792 let content = std::fs::read_to_string(path).unwrap();
2793 assert!(content.contains("\"response\": null"));
2794 }
2795
2796 #[tokio::test]
2797 async fn test_har_with_error_entry() {
2798 let log = NetworkLog::new();
2799 push_entry(&log, make_request("1", "GET", "https://a.com"), None, Some(make_error("1", "https://a.com", "net::ERR_FAILED"))).await;
2800 let path = std::path::Path::new("/tmp/foxdriver_har_error.har");
2801 log.save_as_har(path, None).await.unwrap();
2802 let content = std::fs::read_to_string(path).unwrap();
2803 assert!(content.contains("\"response\": null"));
2804 }
2805
2806 #[tokio::test]
2807 async fn test_default_impl() {
2808 let log: NetworkLog = Default::default();
2809 assert!(log.is_empty().await);
2810 let m = log.metrics().await;
2811 assert_eq!(m.max_entries, 50_000);
2812 }
2813
2814 #[tokio::test]
2815 async fn test_clear_does_not_break_broadcast() {
2816 let log = NetworkLog::new();
2817 let mut rx = log.subscribe().await;
2818 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2819 log.clear().await;
2820 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2821 let first = rx.recv().await.unwrap();
2823 assert_eq!(first.request.id, "1");
2824 let second = rx.recv().await.unwrap();
2825 assert_eq!(second.request.id, "2");
2826 }
2827
2828 #[tokio::test]
2829 async fn test_entries_returns_arc_clones() {
2830 let log = NetworkLog::new();
2831 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2832 let e1 = log.entries().await;
2833 let e2 = log.entries().await;
2834 assert!(Arc::ptr_eq(&e1[0], &e2[0]));
2836 }
2837
2838 #[tokio::test]
2839 async fn test_filter_header_only_response() {
2840 let log = NetworkLog::new();
2841 let req = make_request("1", "GET", "https://a.com");
2842 let mut resp = make_response("1", 200, "https://a.com");
2843 resp.headers = vec![CapturedHeader { name: "X-Resp-Only".into(), value: "found-me".into() }];
2844 push_entry(&log, req, Some(resp), None).await;
2845 let f = log.filter(Filter::new().header("x-resp-only", "found")).await;
2847 assert_eq!(f.len(), 1);
2848 }
2849
2850 #[tokio::test]
2851 async fn test_find_by_url_after_remove() {
2852 let log = NetworkLog::new();
2853 push_request(&log, make_request("1", "GET", "https://a.com/page")).await;
2854 push_request(&log, make_request("2", "GET", "https://b.com/page")).await;
2855 log.remove_by_id("1").await;
2856 assert!(log.find_by_url("a.com").await.is_none());
2857 assert!(log.find_by_url("b.com").await.is_some());
2858 }
2859
2860 #[tokio::test]
2861 async fn test_concurrent_wait_for_url() {
2862 let log = NetworkLog::new();
2863 let log2 = log.clone();
2864 let log3 = log.clone();
2865 let h1 = tokio::spawn(async move {
2866 log2.wait_for_url("target", std::time::Duration::from_secs(1)).await
2867 });
2868 let h2 = tokio::spawn(async move {
2869 log3.wait_for_url("target", std::time::Duration::from_secs(1)).await
2870 });
2871 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2872 push_request(&log, make_request("1", "GET", "https://target.com")).await;
2873 let r1 = h1.await.unwrap();
2874 let r2 = h2.await.unwrap();
2875 assert!(r1.is_some());
2876 assert!(r2.is_some());
2877 }
2878
2879 #[tokio::test]
2880 async fn test_build_response_from_cache() {
2881 let log = NetworkLog::new();
2882 let mut resp = make_response("1", 200, "https://a.com");
2883 resp.from_cache = true;
2884 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(resp), None).await;
2885 let entry = log.first().await.unwrap();
2886 assert!(entry.response.as_ref().unwrap().from_cache);
2887 }
2888
2889 #[tokio::test]
2890 async fn test_build_response_no_body_size() {
2891 let log = NetworkLog::new();
2892 let mut resp = make_response("1", 204, "https://a.com");
2893 resp.body_size = None;
2894 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(resp), None).await;
2895 let entry = log.first().await.unwrap();
2896 assert_eq!(entry.response.as_ref().unwrap().body_size, None);
2897 assert_eq!(log.total_bytes_in().await, 0);
2898 }
2899
2900 #[tokio::test]
2901 async fn test_filter_has_response_and_has_error_same_entry() {
2902 let log = NetworkLog::new();
2903 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 500, "https://a.com")), Some(make_error("1", "https://a.com", "err"))).await;
2904 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
2906 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
2908 }
2909
2910 #[tokio::test]
2911 async fn test_nth_after_remove() {
2912 let log = NetworkLog::new();
2913 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2914 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2915 push_request(&log, make_request("3", "GET", "https://c.com")).await;
2916 log.remove_by_id("2").await;
2917 assert_eq!(log.nth(0).await.unwrap().request.id, "1");
2918 assert_eq!(log.nth(1).await.unwrap().request.id, "3");
2919 assert!(log.nth(2).await.is_none());
2920 }
2921
2922 #[tokio::test]
2923 async fn test_pending_response_then_error_then_request() {
2924 let log = NetworkLog::new();
2925 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200)).await;
2927 log.ingest_fetch_error(&make_fetch_error("req-1", "https://a.com", "net::ERR_ABORTED")).await;
2929 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
2931 let entry = log.first().await.unwrap();
2932 assert!(entry.has_response());
2933 assert!(entry.is_error());
2934 }
2935
2936 #[tokio::test]
2937 async fn test_metrics_persist_after_eviction() {
2938 let log = NetworkLog::with_limits(4, 10);
2939 for i in 0..10 {
2940 log.ingest_before_request_sent(&make_before_request_sent(&format!("{}", i), "https://a.com", "GET")).await;
2941 }
2942 let m = log.metrics().await;
2943 assert_eq!(m.requests_received, 10);
2944 assert_eq!(m.entries_evicted, 6); }
2946
2947 #[tokio::test]
2948 async fn test_curl_strips_accept_encoding() {
2949 let req = CapturedRequest {
2950 id: "1".into(), context: None, method: "GET".into(),
2951 url: "https://example.com".into(),
2952 headers: vec![
2953 CapturedHeader { name: "Accept-Encoding".into(), value: "gzip".into() },
2954 ],
2955 post_data: None, timestamp: 0, destination: "document".into(),
2956 initiator_type: None, timing: CapturedTiming::default(), cookies: vec![],
2957 };
2958 let entry = NetworkEntry { request: req, response: None, error: None };
2959 let curl = entry.to_curl();
2960 assert!(!curl.contains("Accept-Encoding"));
2961 }
2962
2963 #[tokio::test]
2966 async fn test_max_entries_zero_does_not_panic() {
2967 let log = NetworkLog::with_limits(0, 10);
2968 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2969 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2970 assert_eq!(log.len().await, 2);
2972 }
2973
2974 #[tokio::test]
2975 async fn test_max_pending_zero_does_not_hang() {
2976 let log = NetworkLog::with_limits(100, 0);
2977 log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200)).await;
2979 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
2980 assert_eq!(log.len().await, 1);
2981 assert!(log.first().await.unwrap().has_response());
2982 }
2983
2984 #[tokio::test]
2985 async fn test_max_entries_one() {
2986 let log = NetworkLog::with_limits(1, 10);
2987 push_request(&log, make_request("1", "GET", "https://a.com")).await;
2988 push_request(&log, make_request("2", "GET", "https://b.com")).await;
2989 assert_eq!(log.len().await, 2);
2995 }
2996
2997 #[tokio::test]
3000 async fn test_ingest_request_with_headers() {
3001 let log = NetworkLog::new();
3002 let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3003 req_data.headers = vec![
3004 rustenium_bidi_definitions::network::types::Header {
3005 name: "X-Test".into(),
3006 value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3007 rustenium_bidi_definitions::network::types::StringValue::new(
3008 rustenium_bidi_definitions::network::types::StringValueType::String,
3009 "hello"
3010 )
3011 ),
3012 }
3013 ];
3014 let evt = BeforeRequestSent {
3015 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3016 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3017 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3018 initiator: None,
3019 },
3020 };
3021 log.ingest_before_request_sent(&evt).await;
3022 let entry = log.first().await.unwrap();
3023 assert_eq!(entry.request.headers.len(), 1);
3024 assert_eq!(entry.request.headers[0].name, "X-Test");
3025 assert_eq!(entry.request.headers[0].value, "hello");
3026 }
3027
3028 #[tokio::test]
3029 async fn test_ingest_request_with_base64_header() {
3030 let log = NetworkLog::new();
3031 let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3032 req_data.headers = vec![
3033 rustenium_bidi_definitions::network::types::Header {
3034 name: "X-Binary".into(),
3035 value: rustenium_bidi_definitions::network::types::BytesValue::Base64Value(
3036 rustenium_bidi_definitions::network::types::Base64Value::new(
3037 rustenium_bidi_definitions::network::types::Base64ValueType::Base64,
3038 "SGVsbG8="
3039 )
3040 ),
3041 }
3042 ];
3043 let evt = BeforeRequestSent {
3044 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3045 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3046 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3047 initiator: None,
3048 },
3049 };
3050 log.ingest_before_request_sent(&evt).await;
3051 let entry = log.first().await.unwrap();
3052 assert_eq!(entry.request.headers[0].value, "SGVsbG8=");
3053 }
3054
3055 #[tokio::test]
3056 async fn test_ingest_request_with_context() {
3057 let log = NetworkLog::new();
3058 let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3059 let mut base = rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64);
3060 base.context = Some(rustenium_bidi_definitions::browsing_context::types::BrowsingContext::new("ctx-1"));
3061 let evt = BeforeRequestSent {
3062 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3063 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3064 base_parameters: base,
3065 initiator: None,
3066 },
3067 };
3068 log.ingest_before_request_sent(&evt).await;
3069 let entry = log.first().await.unwrap();
3070 assert_eq!(entry.request.context, Some("ctx-1".into()));
3071 }
3072
3073 #[tokio::test]
3074 async fn test_ingest_request_with_initiator() {
3075 let log = NetworkLog::new();
3076 let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3077 let evt = BeforeRequestSent {
3078 method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3079 params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3080 base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3081 initiator: Some(rustenium_bidi_definitions::network::types::Initiator {
3082 column_number: None,
3083 line_number: None,
3084 request: None,
3085 stack_trace: None,
3086 r#type: Some(rustenium_bidi_definitions::network::types::InitiatorType::Script),
3087 }),
3088 },
3089 };
3090 log.ingest_before_request_sent(&evt).await;
3091 let entry = log.first().await.unwrap();
3092 assert_eq!(entry.request.initiator_type, Some("script".into()));
3093 }
3094
3095 #[tokio::test]
3096 async fn test_ingest_response_with_headers() {
3097 let log = NetworkLog::new();
3098 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
3099
3100 let mut resp_data = make_response_completed("req-1", "https://a.com", 200);
3101 resp_data.params.response.headers = vec![
3102 rustenium_bidi_definitions::network::types::Header {
3103 name: "X-Response".into(),
3104 value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3105 rustenium_bidi_definitions::network::types::StringValue::new(
3106 rustenium_bidi_definitions::network::types::StringValueType::String,
3107 "world"
3108 )
3109 ),
3110 }
3111 ];
3112 log.ingest_response_completed(&resp_data).await;
3113
3114 let entry = log.first().await.unwrap();
3115 assert_eq!(entry.response.as_ref().unwrap().headers.len(), 1);
3116 assert_eq!(entry.response.as_ref().unwrap().headers[0].name, "X-Response");
3117 assert_eq!(entry.response.as_ref().unwrap().headers[0].value, "world");
3118 }
3119
3120 #[tokio::test]
3121 async fn test_ingest_response_from_cache() {
3122 let log = NetworkLog::new();
3123 log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET")).await;
3124 let mut resp = make_response_completed("req-1", "https://a.com", 200);
3125 resp.params.response.from_cache = true;
3126 log.ingest_response_completed(&resp).await;
3127 let entry = log.first().await.unwrap();
3128 assert!(entry.response.as_ref().unwrap().from_cache);
3129 }
3130
3131 #[tokio::test]
3134 async fn test_first_on_empty_log() {
3135 let log = NetworkLog::new();
3136 assert!(log.first().await.is_none());
3137 }
3138
3139 #[tokio::test]
3140 async fn test_last_on_empty_log() {
3141 let log = NetworkLog::new();
3142 assert!(log.last().await.is_none());
3143 }
3144
3145 #[tokio::test]
3146 async fn test_nth_on_empty_log() {
3147 let log = NetworkLog::new();
3148 assert!(log.nth(0).await.is_none());
3149 }
3150
3151 #[tokio::test]
3152 async fn test_find_by_url_on_empty_log() {
3153 let log = NetworkLog::new();
3154 assert!(log.find_by_url("anything").await.is_none());
3155 }
3156
3157 #[tokio::test]
3158 async fn test_filter_on_empty_log() {
3159 let log = NetworkLog::new();
3160 assert!(log.filter(Filter::new()).await.is_empty());
3161 }
3162
3163 #[tokio::test]
3164 async fn test_count_on_empty_log() {
3165 let log = NetworkLog::new();
3166 assert_eq!(log.count(Filter::new()).await, 0);
3167 }
3168
3169 #[tokio::test]
3170 async fn test_completed_on_empty_log() {
3171 let log = NetworkLog::new();
3172 assert!(log.completed().await.is_empty());
3173 }
3174
3175 #[tokio::test]
3176 async fn test_endpoints_on_empty_log() {
3177 let log = NetworkLog::new();
3178 assert!(log.endpoints().await.is_empty());
3179 }
3180
3181 #[tokio::test]
3182 async fn test_hostnames_on_empty_log() {
3183 let log = NetworkLog::new();
3184 assert!(log.hostnames().await.is_empty());
3185 }
3186
3187 #[tokio::test]
3188 async fn test_distinct_methods_on_empty_log() {
3189 let log = NetworkLog::new();
3190 assert!(log.distinct_methods().await.is_empty());
3191 }
3192
3193 #[tokio::test]
3194 async fn test_distinct_statuses_on_empty_log() {
3195 let log = NetworkLog::new();
3196 assert!(log.distinct_statuses().await.is_empty());
3197 }
3198
3199 #[tokio::test]
3200 async fn test_total_bytes_on_empty_log() {
3201 let log = NetworkLog::new();
3202 assert_eq!(log.total_bytes_in().await, 0);
3203 assert_eq!(log.total_bytes_out().await, 0);
3204 }
3205
3206 #[tokio::test]
3207 async fn test_contains_id_on_empty_log() {
3208 let log = NetworkLog::new();
3209 assert!(!log.contains_id("anything").await);
3210 }
3211
3212 #[tokio::test]
3213 async fn test_remove_by_id_on_empty_log() {
3214 let log = NetworkLog::new();
3215 assert!(log.remove_by_id("anything").await.is_none());
3216 }
3217
3218 #[tokio::test]
3219 async fn test_clear_on_empty_log() {
3220 let log = NetworkLog::new();
3221 log.clear().await;
3222 assert!(log.is_empty().await);
3223 }
3224
3225 #[tokio::test]
3226 async fn test_subscribe_on_empty_log() {
3227 let log = NetworkLog::new();
3228 let mut rx = log.subscribe().await;
3229 assert!(rx.try_recv().is_err());
3230 }
3231
3232 #[tokio::test]
3235 async fn test_remove_by_id_twice() {
3236 let log = NetworkLog::new();
3237 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3238 assert!(log.remove_by_id("1").await.is_some());
3239 assert!(log.remove_by_id("1").await.is_none());
3240 }
3241
3242 #[tokio::test]
3243 async fn test_remove_by_id_only_entry() {
3244 let log = NetworkLog::new();
3245 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3246 log.remove_by_id("1").await;
3247 assert!(log.is_empty().await);
3248 assert!(!log.contains_id("1").await);
3249 }
3250
3251 #[tokio::test]
3252 async fn test_concurrent_remove_by_id() {
3253 let log = NetworkLog::new();
3254 for i in 0..100 {
3255 push_request(&log, make_request(&format!("{}", i), "GET", "https://a.com")).await;
3256 }
3257 let mut handles = Vec::new();
3258 for i in 0..100 {
3259 let log = log.clone();
3260 handles.push(tokio::spawn(async move {
3261 log.remove_by_id(&format!("{}", i)).await
3262 }));
3263 }
3264 for h in handles {
3265 h.await.unwrap();
3266 }
3267 assert!(log.is_empty().await);
3268 }
3269
3270 #[tokio::test]
3273 async fn test_wait_for_url_empty_substring() {
3274 let log = NetworkLog::new();
3275 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3276 let found = log.wait_for_url("", std::time::Duration::from_secs(1)).await;
3277 assert!(found.is_some());
3278 }
3279
3280 #[tokio::test]
3281 async fn test_wait_for_response_nonexistent_id() {
3282 let log = NetworkLog::new();
3283 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3284 let found = log.wait_for_response("does-not-exist", std::time::Duration::from_millis(50)).await;
3285 assert!(found.is_none());
3286 }
3287
3288 #[tokio::test]
3291 async fn test_json_body_array() {
3292 let mut req = make_request("1", "POST", "https://a.com");
3293 req.post_data = Some("[1, 2, 3]".into());
3294 assert_eq!(req.json_body(), Some(serde_json::json!([1, 2, 3])));
3295 }
3296
3297 #[tokio::test]
3298 async fn test_json_body_nested() {
3299 let mut req = make_request("1", "POST", "https://a.com");
3300 req.post_data = Some(r#"{"outer": {"inner": [true, false]}}"#.into());
3301 assert_eq!(req.json_body(), Some(serde_json::json!({"outer": {"inner": [true, false]}})));
3302 }
3303
3304 #[tokio::test]
3307 async fn test_to_curl_delete_method() {
3308 let req = make_request("1", "DELETE", "https://api.example.com/resource/1");
3309 let entry = NetworkEntry { request: req, response: None, error: None };
3310 assert!(entry.to_curl().starts_with("curl -X DELETE"));
3311 }
3312
3313 #[tokio::test]
3314 async fn test_to_curl_url_with_single_quotes() {
3315 let req = make_request("1", "GET", "https://example.com?foo='bar'");
3316 let entry = NetworkEntry { request: req, response: None, error: None };
3317 let curl = entry.to_curl();
3318 assert!(curl.contains("'https://example.com?foo='\\''bar'\\'''"));
3319 }
3320
3321 #[tokio::test]
3322 async fn test_to_curl_header_with_single_quotes() {
3323 let req = make_request_with_headers("1", "GET", "https://example.com", vec![
3324 CapturedHeader { name: "X-Quote".into(), value: "it's working".into() },
3325 ]);
3326 let entry = NetworkEntry { request: req, response: None, error: None };
3327 let curl = entry.to_curl();
3328 assert!(curl.contains("-H 'X-Quote: it'\\''s working'"));
3329 }
3330
3331 #[tokio::test]
3334 async fn test_network_metrics_roundtrip() {
3335 let m = NetworkMetrics {
3336 requests_received: 10,
3337 responses_received: 8,
3338 errors_received: 2,
3339 entries_evicted: 5,
3340 pending_responses_dropped: 1,
3341 pending_errors_dropped: 0,
3342 broadcast_drops: 3,
3343 duplicate_responses: 1,
3344 duplicate_errors: 0,
3345 max_entries: 100,
3346 };
3347 let json = serde_json::to_string(&m).unwrap();
3348 let de: NetworkMetrics = serde_json::from_str(&json).unwrap();
3349 assert_eq!(de, m);
3350 }
3351
3352 #[tokio::test]
3355 async fn test_find_by_url_unicode() {
3356 let log = NetworkLog::new();
3357 push_request(&log, make_request("1", "GET", "https://例子.com/path")).await;
3358 assert!(log.find_by_url("例子").await.is_some());
3359 }
3360
3361 #[tokio::test]
3362 async fn test_hostnames_idn() {
3363 let log = NetworkLog::new();
3364 push_request(&log, make_request("1", "GET", "https://xn--fsq092h.com/path")).await;
3365 let h = log.hostnames().await;
3366 assert_eq!(h.len(), 1);
3367 assert!(h.contains(&"xn--fsq092h.com".into()));
3368 }
3369
3370 #[tokio::test]
3373 async fn test_total_bytes_out_multibyte() {
3374 let log = NetworkLog::new();
3375 let mut req = make_request("1", "POST", "https://a.com");
3376 req.post_data = Some("日本語".into()); push_entry(&log, req, None, None).await;
3378 assert_eq!(log.total_bytes_out().await, 9);
3379 }
3380
3381 #[tokio::test]
3384 async fn test_completed_only_errors() {
3385 let log = NetworkLog::new();
3386 push_entry(&log, make_request("1", "GET", "https://a.com"), None, Some(make_error("1", "https://a.com", "err"))).await;
3387 push_entry(&log, make_request("2", "GET", "https://b.com"), None, Some(make_error("2", "https://b.com", "err"))).await;
3388 let c = log.completed().await;
3389 assert_eq!(c.len(), 2);
3390 }
3391
3392 #[tokio::test]
3395 async fn test_distinct_statuses_all_same() {
3396 let log = NetworkLog::new();
3397 for i in 0..5 {
3398 push_entry(&log, make_request(&format!("{}", i), "GET", "https://a.com"), Some(make_response(&format!("{}", i), 200, "https://a.com")), None).await;
3399 }
3400 assert_eq!(log.distinct_statuses().await, vec![200]);
3401 }
3402
3403 #[tokio::test]
3404 async fn test_distinct_methods_all_same() {
3405 let log = NetworkLog::new();
3406 for i in 0..5 {
3407 push_request(&log, make_request(&format!("{}", i), "POST", "https://a.com")).await;
3408 }
3409 assert_eq!(log.distinct_methods().await, vec!["POST"]);
3410 }
3411
3412 #[tokio::test]
3415 async fn test_clear_subscribe_push() {
3416 let log = NetworkLog::new();
3417 log.clear().await;
3418 let mut rx = log.subscribe().await;
3419 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3420 assert_eq!(rx.recv().await.unwrap().request.id, "1");
3421 }
3422
3423 #[tokio::test]
3426 async fn test_total_bytes_in_from_cache() {
3427 let log = NetworkLog::new();
3428 let mut resp = make_response("1", 200, "https://a.com");
3429 resp.from_cache = true;
3430 resp.body_size = Some(500);
3431 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(resp), None).await;
3432 assert_eq!(log.total_bytes_in().await, 500);
3433 }
3434
3435 #[tokio::test]
3438 async fn test_to_curl_produces_valid_shell_with_quotes() {
3439 let req = CapturedRequest {
3440 id: "1".into(), context: None, method: "POST".into(),
3441 url: "https://example.com".into(),
3442 headers: vec![CapturedHeader { name: "X-Quote".into(), value: "it's".into() }],
3443 post_data: Some("data='val'".into()),
3444 timestamp: 0, destination: "document".into(),
3445 initiator_type: None, timing: CapturedTiming::default(), cookies: vec![],
3446 };
3447 let entry = NetworkEntry { request: req, response: None, error: None };
3448 let curl = entry.to_curl();
3449 assert!(curl.contains("it'\\''s"));
3453 assert!(curl.contains("data='\\''val'\\'''"));
3454 }
3455
3456
3457
3458 #[tokio::test]
3459 async fn test_save_as_har_postdata_uses_content_type_header() {
3460 let log = NetworkLog::new();
3461 let mut req = make_request("1", "POST", "https://api.example.com");
3462 req.post_data = Some(r#"{"key":"value"}"#.into());
3463 req.headers = vec![CapturedHeader { name: "Content-Type".into(), value: "application/vnd.api+json".into() }];
3464 push_entry(&log, req, Some(make_response("1", 201, "https://api.example.com")), None).await;
3465
3466 let path = std::path::Path::new("/tmp/foxdriver_har_mime.har");
3467 log.save_as_har(path, None).await.unwrap();
3468 let content = std::fs::read_to_string(path).unwrap();
3469 assert!(content.contains("\"mimeType\": \"application/vnd.api+json\""));
3471 }
3472
3473 #[tokio::test]
3474 async fn test_save_as_har_postdata_defaults_octet_stream() {
3475 let log = NetworkLog::new();
3476 let mut req = make_request("1", "POST", "https://api.example.com");
3477 req.post_data = Some("raw bytes".into());
3478 push_entry(&log, req, Some(make_response("1", 201, "https://api.example.com")), None).await;
3480
3481 let path = std::path::Path::new("/tmp/foxdriver_har_default_mime.har");
3482 log.save_as_har(path, None).await.unwrap();
3483 let content = std::fs::read_to_string(path).unwrap();
3484 assert!(content.contains("\"mimeType\": \"application/octet-stream\""));
3485 }
3486
3487 #[tokio::test]
3488 async fn test_concurrent_reads_with_rwlock() {
3489 let log = NetworkLog::new();
3490 for i in 0..100 {
3491 push_request(&log, make_request(&format!("{}", i), "GET", "https://example.com")).await;
3492 }
3493 let mut handles = Vec::new();
3494 for _ in 0..20 {
3495 let log = log.clone();
3496 handles.push(tokio::spawn(async move {
3497 for _ in 0..50 {
3498 let _ = log.len().await;
3499 let _ = log.entries().await;
3500 let _ = log.distinct_methods().await;
3501 let _ = log.total_bytes_in().await;
3502 }
3503 }));
3504 }
3505 for h in handles {
3506 h.await.unwrap();
3507 }
3508 assert_eq!(log.len().await, 100);
3510 }
3511
3512 #[tokio::test]
3519 async fn test_find_by_status() {
3520 let log = NetworkLog::new();
3521 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
3522 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 404, "https://b.com")), None).await;
3523 push_entry(&log, make_request("3", "GET", "https://c.com"), None, Some(make_error("3", "https://c.com", "err"))).await;
3524 assert_eq!(log.find_by_status(200).await.len(), 1);
3525 assert_eq!(log.find_by_status(404).await.len(), 1);
3526 assert_eq!(log.find_by_status(500).await.len(), 0);
3527 }
3528
3529 #[tokio::test]
3530 async fn test_entries_since() {
3531 let log = NetworkLog::new();
3532 let mut req1 = make_request("1", "GET", "https://a.com");
3533 req1.timestamp = 1000;
3534 let mut req2 = make_request("2", "GET", "https://b.com");
3535 req2.timestamp = 2000;
3536 let mut req3 = make_request("3", "GET", "https://c.com");
3537 req3.timestamp = 3000;
3538 push_request(&log, req1).await;
3539 push_request(&log, req2).await;
3540 push_request(&log, req3).await;
3541 assert_eq!(log.entries_since(0).await.len(), 3);
3542 assert_eq!(log.entries_since(2000).await.len(), 2);
3543 assert_eq!(log.entries_since(3001).await.len(), 0);
3544 }
3545
3546 #[tokio::test]
3547 async fn test_last_n() {
3548 let log = NetworkLog::new();
3549 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3550 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3551 push_request(&log, make_request("3", "GET", "https://c.com")).await;
3552 let last = log.last_n(2).await;
3553 assert_eq!(last.len(), 2);
3554 assert_eq!(last[0].request.id, "2");
3555 assert_eq!(last[1].request.id, "3");
3556 assert_eq!(log.last_n(10).await.len(), 3);
3557 assert!(log.last_n(0).await.is_empty());
3558 }
3559
3560
3561
3562 #[tokio::test]
3589 async fn test_unique_urls() {
3590 let log = NetworkLog::new();
3591 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3592 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3593 push_request(&log, make_request("3", "GET", "https://a.com")).await;
3594 let urls = log.unique_urls().await;
3595 assert_eq!(urls.len(), 2);
3596 assert!(urls.contains(&"https://a.com".to_string()));
3597 assert!(urls.contains(&"https://b.com".to_string()));
3598 }
3599
3600 #[tokio::test]
3601 async fn test_unique_urls_empty() {
3602 let log = NetworkLog::new();
3603 assert!(log.unique_urls().await.is_empty());
3604 }
3605
3606 #[tokio::test]
3609 async fn test_broadcast_drops_tracked_when_no_receivers() {
3610 let log = NetworkLog::new();
3611 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3613 let m = log.metrics().await;
3614 assert_eq!(m.broadcast_drops, 1);
3615 }
3616
3617 #[tokio::test]
3618 async fn test_broadcast_no_drop_when_receiver_active() {
3619 let log = NetworkLog::new();
3620 let _rx = log.subscribe().await;
3621 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3622 let m = log.metrics().await;
3623 assert_eq!(m.broadcast_drops, 0);
3624 }
3625
3626 #[tokio::test]
3627 async fn test_pending_count_and_drain() {
3628 let log = NetworkLog::new();
3629 {
3631 let mut inner = log.inner.write().await;
3632 inner.pending_responses.insert("orphan-1".into(), make_response("orphan-1", 200, "https://a.com"));
3633 inner.pending_errors.insert("orphan-2".into(), make_error("orphan-2", "https://b.com", "err"));
3634 }
3635 assert_eq!(log.pending_count().await, 2);
3636
3637 let (responses, errors) = log.drain_pending().await;
3638 assert_eq!(responses.len(), 1);
3639 assert_eq!(errors.len(), 1);
3640 assert_eq!(log.pending_count().await, 0);
3641 }
3642
3643 #[tokio::test]
3644 async fn test_pending_count_empty() {
3645 let log = NetworkLog::new();
3646 assert_eq!(log.pending_count().await, 0);
3647 let (r, e) = log.drain_pending().await;
3648 assert!(r.is_empty() && e.is_empty());
3649 }
3650
3651 #[tokio::test]
3652 async fn test_retain_filters_entries() {
3653 let log = NetworkLog::new();
3654 push_request(&log, make_request("1", "GET", "https://keep.com")).await;
3655 push_request(&log, make_request("2", "GET", "https://drop.com")).await;
3656 push_request(&log, make_request("3", "GET", "https://keep.com/path")).await;
3657 log.retain(|e| e.request.url.contains("keep")).await;
3658 assert_eq!(log.len().await, 2);
3659 assert!(log.contains_id("1").await);
3660 assert!(!log.contains_id("2").await);
3661 assert!(log.contains_id("3").await);
3662 }
3663
3664 #[tokio::test]
3665 async fn test_retain_rebuilds_indices() {
3666 let log = NetworkLog::new();
3667 for i in 0..5 {
3668 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i))).await;
3669 }
3670 log.retain(|e| e.request.id.parse::<i32>().unwrap() % 2 == 0).await;
3671 assert_eq!(log.len().await, 3);
3672 assert!(log.contains_id("0").await);
3673 assert!(log.contains_id("2").await);
3674 assert!(log.contains_id("4").await);
3675 }
3676
3677 #[tokio::test]
3678 async fn test_deduplication_guard_ignores_duplicate_request_id() {
3679 let log = NetworkLog::new();
3680 push_request(&log, make_request("1", "GET", "https://first.com")).await;
3681 push_request(&log, make_request("1", "GET", "https://second.com")).await;
3682 assert_eq!(log.len().await, 1);
3683 assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
3684 }
3685
3686 #[tokio::test]
3687 async fn test_duplicate_response_tracked_in_metrics() {
3688 let log = NetworkLog::new();
3689 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
3690 {
3692 let mut inner = log.inner.write().await;
3693 if let Some(idx) = inner.by_id.get("1").copied() {
3694 let mut new_entry = (*inner.entries[idx]).clone();
3695 new_entry.response = Some(make_response("1", 201, "https://a.com"));
3696 inner.entries[idx] = Arc::new(new_entry);
3697 }
3698 }
3699 let m = log.metrics().await;
3702 assert_eq!(m.duplicate_responses, 0); }
3704
3705 #[tokio::test]
3706 async fn test_request_ids_returns_all_ids() {
3707 let log = NetworkLog::new();
3708 push_request(&log, make_request("a", "GET", "https://a.com")).await;
3709 push_request(&log, make_request("b", "GET", "https://b.com")).await;
3710 let ids = log.request_ids().await;
3711 assert_eq!(ids, vec!["a", "b"]);
3712 }
3713
3714 #[tokio::test]
3715 async fn test_has_response_and_has_error() {
3716 let log = NetworkLog::new();
3717 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
3718 push_entry(&log, make_request("2", "GET", "https://b.com"), None, Some(make_error("2", "https://b.com", "err"))).await;
3719 push_request(&log, make_request("3", "GET", "https://c.com")).await;
3720 assert!(log.has_response("1").await);
3721 assert!(!log.has_error("1").await);
3722 assert!(!log.has_response("2").await);
3723 assert!(log.has_error("2").await);
3724 assert!(!log.has_response("3").await);
3725 assert!(!log.has_error("3").await);
3726 assert!(!log.has_response("missing").await);
3727 assert!(!log.has_error("missing").await);
3728 }
3729
3730 #[tokio::test]
3733 async fn test_invariant_len_equals_entries_len() {
3734 let log = NetworkLog::new();
3735 for i in 0..50 {
3736 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3737 assert_eq!(log.len().await, log.entries().await.len());
3738 }
3739 }
3740
3741 #[tokio::test]
3742 async fn test_invariant_is_empty_iff_len_zero() {
3743 let log = NetworkLog::new();
3744 assert_eq!(log.is_empty().await, log.len().await == 0);
3745 push_request(&log, make_request("1", "GET", "https://x.com")).await;
3746 assert_eq!(log.is_empty().await, log.len().await == 0);
3747 log.clear().await;
3748 assert_eq!(log.is_empty().await, log.len().await == 0);
3749 }
3750
3751 #[tokio::test]
3752 async fn test_invariant_by_id_indices_are_valid() {
3753 let log = NetworkLog::new();
3754 for i in 0..100 {
3755 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i))).await;
3756 }
3757 let inner = log.inner.read().await;
3758 for (id, idx) in &inner.by_id {
3759 assert!(*idx < inner.entries.len(), "id {} has invalid index {}", id, idx);
3760 assert_eq!(inner.entries[*idx].request.id, *id);
3761 }
3762 }
3763
3764 #[tokio::test]
3765 async fn test_invariant_no_duplicate_ids() {
3766 let log = NetworkLog::new();
3767 for i in 0..100 {
3768 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3769 }
3770 let inner = log.inner.read().await;
3771 let mut seen = std::collections::HashSet::new();
3772 for e in &inner.entries {
3773 assert!(seen.insert(&e.request.id), "duplicate id {}", e.request.id);
3774 }
3775 }
3776
3777 #[tokio::test]
3778 async fn test_invariant_first_and_last_consistent() {
3779 let log = NetworkLog::new();
3780 assert_eq!(log.first().await, None);
3781 assert_eq!(log.last().await, None);
3782 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3783 push_request(&log, make_request("2", "GET", "https://b.com")).await;
3784 let entries = log.entries().await;
3785 assert_eq!(log.first().await.as_deref(), entries.first().map(Arc::as_ref));
3786 assert_eq!(log.last().await.as_deref(), entries.last().map(Arc::as_ref));
3787 }
3788
3789 #[tokio::test]
3790 async fn test_invariant_filter_count_consistent() {
3791 let log = NetworkLog::new();
3792 for i in 0..20 {
3793 let status = if i % 2 == 0 { 200 } else { 404 };
3794 push_entry(&log, make_request(&format!("{}", i), "GET", "https://x.com"), Some(make_response(&format!("{}", i), status, "https://x.com")), None).await;
3795 }
3796 let f = Filter::new().status_range(200..=200);
3797 let filtered = log.filter(f.clone()).await;
3798 let count = log.count(f).await;
3799 assert_eq!(filtered.len(), count);
3800 }
3801
3802 #[tokio::test]
3803 async fn test_invariant_metrics_monotonic() {
3804 let log = NetworkLog::new();
3805 let m0 = log.metrics().await;
3806 push_request(&log, make_request("1", "GET", "https://a.com")).await;
3807 let m1 = log.metrics().await;
3808 assert!(m1.requests_received >= m0.requests_received);
3809 assert!(m1.responses_received >= m0.responses_received);
3810 assert!(m1.errors_received >= m0.errors_received);
3811 }
3812
3813 #[tokio::test]
3814 async fn test_invariant_pending_count_bounded() {
3815 let log = NetworkLog::with_limits(100, 10);
3816 {
3818 let mut inner = log.inner.write().await;
3819 for i in 0..20 {
3820 inner.pending_responses.insert(format!("orphan-{}", i), make_response(&format!("{}", i), 200, "https://x.com"));
3821 }
3822 }
3823 assert_eq!(log.pending_count().await, 20);
3825 let (r, _) = log.drain_pending().await;
3826 assert_eq!(r.len(), 20);
3827 assert_eq!(log.pending_count().await, 0);
3828 }
3829
3830 #[tokio::test]
3831 async fn test_invariant_after_clear_all_empty() {
3832 let log = NetworkLog::new();
3833 for i in 0..50 {
3834 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3835 }
3836 log.clear().await;
3837 assert!(log.is_empty().await);
3838 assert_eq!(log.pending_count().await, 0);
3839 assert!(!log.contains_id("1").await);
3840 let inner = log.inner.read().await;
3841 assert!(inner.by_id.is_empty());
3842 }
3843
3844 #[tokio::test]
3845 async fn test_invariant_after_remove_id_not_present() {
3846 let log = NetworkLog::new();
3847 for i in 0..50 {
3848 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3849 }
3850 log.remove_by_id("25").await;
3851 assert!(!log.contains_id("25").await);
3852 let inner = log.inner.read().await;
3853 assert!(!inner.by_id.contains_key("25"));
3854 assert_eq!(inner.entries.len(), 49);
3855 for (id, idx) in &inner.by_id {
3857 assert!(inner.entries[*idx].request.id == *id);
3858 }
3859 }
3860
3861 #[tokio::test]
3862 async fn test_invariant_retain_all_satisfy_predicate() {
3863 let log = NetworkLog::new();
3864 for i in 0..50 {
3865 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 3))).await;
3866 }
3867 log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com")).await;
3868 for e in log.entries().await {
3869 assert!(!e.request.url.contains("2.com"));
3870 }
3871 assert_eq!(log.len().await, 34); }
3873
3874 #[tokio::test]
3875 async fn test_invariant_nth_matches_entries_index() {
3876 let log = NetworkLog::new();
3877 for i in 0..20 {
3878 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i))).await;
3879 }
3880 let entries = log.entries().await;
3881 for i in 0..entries.len() {
3882 assert_eq!(log.nth(i).await.as_deref(), Some(entries[i].as_ref()));
3883 }
3884 assert!(log.nth(100).await.is_none());
3885 }
3886
3887 #[tokio::test]
3888 async fn test_invariant_last_n_order_preserved() {
3889 let log = NetworkLog::new();
3890 for i in 0..10 {
3891 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3892 }
3893 let last = log.last_n(3).await;
3894 assert_eq!(last.len(), 3);
3895 assert_eq!(last[0].request.id, "7");
3896 assert_eq!(last[1].request.id, "8");
3897 assert_eq!(last[2].request.id, "9");
3898 }
3899
3900 #[tokio::test]
3901 async fn test_invariant_entries_since_time_based() {
3902 let log = NetworkLog::new();
3903 for i in 0..10 {
3904 let mut req = make_request(&format!("{}", i), "GET", "https://x.com");
3905 req.timestamp = i as u64 * 1000;
3906 push_request(&log, req).await;
3907 }
3908 let since = log.entries_since(5000).await;
3909 assert_eq!(since.len(), 5); for e in &since {
3911 assert!(e.request.timestamp >= 5000);
3912 }
3913 }
3914
3915 #[tokio::test]
3918 async fn test_stress_concurrent_ingestion_and_query() {
3919 let log = NetworkLog::new();
3920 let mut handles = Vec::new();
3921
3922 for t in 0..10 {
3924 let log = log.clone();
3925 handles.push(tokio::spawn(async move {
3926 for i in 0..100 {
3927 let id = format!("t{}-{}", t, i);
3928 push_request(&log, make_request(&id, "GET", "https://x.com")).await;
3929 }
3930 }));
3931 }
3932
3933 for _ in 0..10 {
3935 let log = log.clone();
3936 handles.push(tokio::spawn(async move {
3937 for _ in 0..100 {
3938 let _ = log.len().await;
3939 let _ = log.entries().await;
3940 let _ = log.first().await;
3941 let _ = log.last().await;
3942 let _ = log.metrics().await;
3943 tokio::task::yield_now().await;
3944 }
3945 }));
3946 }
3947
3948 for h in handles {
3949 h.await.unwrap();
3950 }
3951
3952 assert_eq!(log.len().await, 1000);
3953 let inner = log.inner.read().await;
3954 assert_eq!(inner.by_id.len(), 1000);
3955 }
3956
3957 #[tokio::test]
3958 async fn test_stress_concurrent_subscribers() {
3959 let log = NetworkLog::new();
3960 let mut rxs = Vec::new();
3961 for _ in 0..50 {
3962 rxs.push(log.subscribe().await);
3963 }
3964
3965 for i in 0..100 {
3966 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3967 }
3968
3969 for mut rx in rxs {
3972 let mut count = 0;
3973 while rx.try_recv().is_ok() {
3974 count += 1;
3975 }
3976 assert!(count <= 100);
3977 }
3978 }
3979
3980 #[tokio::test]
3981 async fn test_stress_eviction_under_load() {
3982 let log = NetworkLog::with_limits(100, 10);
3983 for i in 0..500 {
3984 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
3985 }
3986 assert_eq!(log.len().await, 100);
3987 let m = log.metrics().await;
3988 assert!(m.entries_evicted > 0);
3989 }
3990
3991 #[tokio::test]
3992 async fn test_stress_retain_under_load() {
3993 let log = NetworkLog::new();
3994 for i in 0..1000 {
3995 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 10))).await;
3996 }
3997 log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com")).await;
3998 assert_eq!(log.len().await, 200);
3999 let inner = log.inner.read().await;
4000 assert_eq!(inner.by_id.len(), 200);
4001 }
4002
4003 #[tokio::test]
4004 async fn test_stress_memory_estimate_grows_with_entries() {
4005 let log = NetworkLog::new();
4006 let m0 = log.memory_estimate().await;
4007 for i in 0..100 {
4008 let mut req = make_request(&format!("{}", i), "POST", "https://x.com");
4009 req.post_data = Some("x".repeat(1000));
4010 push_request(&log, req).await;
4011 }
4012 let m1 = log.memory_estimate().await;
4013 assert!(m1 > m0);
4014 }
4015
4016 #[tokio::test]
4019 async fn test_har_is_valid_json() {
4020 let log = NetworkLog::new();
4021 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
4022 let path = std::path::Path::new("/tmp/foxdriver_har_valid.json");
4023 log.save_as_har(path, Some("test")).await.unwrap();
4024 let content = std::fs::read_to_string(path).unwrap();
4025 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4026 assert_eq!(parsed["log"]["version"], "1.2");
4027 assert!(parsed["log"]["entries"].is_array());
4028 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 1);
4029 }
4030
4031 #[tokio::test]
4032 async fn test_har_has_required_fields() {
4033 let log = NetworkLog::new();
4034 push_entry(&log, make_request("1", "POST", "https://api.example.com"), Some(make_response("1", 201, "https://api.example.com")), None).await;
4035 let path = std::path::Path::new("/tmp/foxdriver_har_fields.json");
4036 log.save_as_har(path, None).await.unwrap();
4037 let content = std::fs::read_to_string(path).unwrap();
4038 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4039 let entry = &parsed["log"]["entries"][0];
4040 assert!(entry["startedDateTime"].is_string());
4041 assert!(entry["request"]["method"].is_string());
4042 assert!(entry["request"]["url"].is_string());
4043 assert!(entry["response"]["status"].is_number());
4044 assert!(entry["response"]["statusText"].is_string());
4045 assert!(entry["timings"].is_object());
4046 }
4047
4048 #[tokio::test]
4049 async fn test_har_empty_log_has_zero_entries() {
4050 let log = NetworkLog::new();
4051 let path = std::path::Path::new("/tmp/foxdriver_har_empty_valid.json");
4052 log.save_as_har(path, None).await.unwrap();
4053 let content = std::fs::read_to_string(path).unwrap();
4054 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4055 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 0);
4056 }
4057
4058 #[tokio::test]
4059 async fn test_har_post_data_present() {
4060 let log = NetworkLog::new();
4061 let mut req = make_request("1", "POST", "https://api.example.com");
4062 req.post_data = Some(r#"{"key":"value"}"#.into());
4063 req.headers = vec![CapturedHeader { name: "Content-Type".into(), value: "application/json".into() }];
4064 push_entry(&log, req, Some(make_response("1", 200, "https://api.example.com")), None).await;
4065 let path = std::path::Path::new("/tmp/foxdriver_har_postdata.json");
4066 log.save_as_har(path, None).await.unwrap();
4067 let content = std::fs::read_to_string(path).unwrap();
4068 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4069 let post_data = &parsed["log"]["entries"][0]["request"]["postData"];
4070 assert!(post_data["text"].is_string());
4071 assert!(post_data["mimeType"].is_string());
4072 }
4073
4074 #[tokio::test]
4077 async fn test_ingest_response_for_unknown_request_stashes_pending() {
4078 let log = NetworkLog::new();
4079 {
4081 let mut inner = log.inner.write().await;
4082 inner.pending_responses.insert("future-req".into(), make_response("future-req", 200, "https://a.com"));
4083 }
4084 assert_eq!(log.pending_count().await, 1);
4085 {
4087 let mut inner = log.inner.write().await;
4088 let resp = inner.pending_responses.remove("future-req");
4089 let entry = Arc::new(NetworkEntry {
4090 request: make_request("future-req", "GET", "https://a.com"),
4091 response: resp,
4092 error: None,
4093 });
4094 inner.push_entry(entry);
4095 }
4096 assert_eq!(log.pending_count().await, 0);
4097 assert!(log.has_response("future-req").await);
4098 }
4099
4100 #[tokio::test]
4101 async fn test_ingest_error_for_unknown_request_stashes_pending() {
4102 let log = NetworkLog::new();
4103 {
4104 let mut inner = log.inner.write().await;
4105 inner.pending_errors.insert("future-err".into(), make_error("future-err", "https://a.com", "timeout"));
4106 }
4107 assert_eq!(log.pending_count().await, 1);
4108 {
4110 let mut inner = log.inner.write().await;
4111 let err = inner.pending_errors.remove("future-err");
4112 let entry = Arc::new(NetworkEntry {
4113 request: make_request("future-err", "GET", "https://a.com"),
4114 response: None,
4115 error: err,
4116 });
4117 inner.push_entry(entry);
4118 }
4119 assert_eq!(log.pending_count().await, 0);
4120 assert!(log.has_error("future-err").await);
4121 }
4122
4123 #[tokio::test]
4124 async fn test_ingest_duplicate_request_ignored() {
4125 let log = NetworkLog::new();
4126 push_request(&log, make_request("1", "GET", "https://first.com")).await;
4127 push_request(&log, make_request("1", "GET", "https://second.com")).await;
4128 assert_eq!(log.len().await, 1);
4129 assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
4130 }
4131
4132 #[tokio::test]
4135 async fn test_filter_empty_log() {
4136 let log = NetworkLog::new();
4137 let f = Filter::new().method("GET").status_range(200..=200);
4138 assert!(log.filter(f).await.is_empty());
4139 assert_eq!(log.count(Filter::new()).await, 0);
4140 }
4141
4142 #[tokio::test]
4143 async fn test_find_by_url_empty_log() {
4144 let log = NetworkLog::new();
4145 assert!(log.find_by_url("anything").await.is_none());
4146 }
4147
4148 #[tokio::test]
4149 async fn test_find_by_url_regex_empty_log() {
4150 let log = NetworkLog::new();
4151 let re = regex::Regex::new(".*").unwrap();
4152 assert!(log.find_by_url_regex(&re).await.is_none());
4153 }
4154
4155 #[tokio::test]
4156 async fn test_endpoints_empty_log() {
4157 let log = NetworkLog::new();
4158 assert!(log.endpoints().await.is_empty());
4159 }
4160
4161 #[tokio::test]
4162 async fn test_hostnames_empty_log() {
4163 let log = NetworkLog::new();
4164 assert!(log.hostnames().await.is_empty());
4165 }
4166
4167 #[tokio::test]
4168 async fn test_distinct_methods_empty_log_returns_empty() {
4169 let log = NetworkLog::new();
4170 assert!(log.distinct_methods().await.is_empty());
4171 }
4172
4173 #[tokio::test]
4174 async fn test_distinct_statuses_empty_log() {
4175 let log = NetworkLog::new();
4176 assert!(log.distinct_statuses().await.is_empty());
4177 }
4178
4179 #[tokio::test]
4180 async fn test_total_bytes_empty_log() {
4181 let log = NetworkLog::new();
4182 assert_eq!(log.total_bytes_in().await, 0);
4183 assert_eq!(log.total_bytes_out().await, 0);
4184 }
4185
4186 #[tokio::test]
4187 async fn test_nth_out_of_bounds() {
4188 let log = NetworkLog::new();
4189 assert!(log.nth(0).await.is_none());
4190 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4191 assert!(log.nth(1).await.is_none());
4192 assert!(log.nth(100).await.is_none());
4193 }
4194
4195 #[tokio::test]
4196 async fn test_last_n_greater_than_len() {
4197 let log = NetworkLog::new();
4198 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4199 assert_eq!(log.last_n(100).await.len(), 1);
4200 }
4201
4202 #[tokio::test]
4203 async fn test_find_by_status_no_matches() {
4204 let log = NetworkLog::new();
4205 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
4206 assert!(log.find_by_status(404).await.is_empty());
4207 }
4208
4209 #[tokio::test]
4210 async fn test_entries_since_future_timestamp() {
4211 let log = NetworkLog::new();
4212 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4213 assert!(log.entries_since(u64::MAX).await.is_empty());
4214 }
4215
4216 #[tokio::test]
4217 async fn test_contains_id_false() {
4218 let log = NetworkLog::new();
4219 assert!(!log.contains_id("nonexistent").await);
4220 }
4221
4222 #[tokio::test]
4223 async fn test_remove_by_id_nonexistent() {
4224 let log = NetworkLog::new();
4225 assert!(log.remove_by_id("nonexistent").await.is_none());
4226 }
4227
4228 #[tokio::test]
4229 async fn test_metrics_on_empty_log() {
4230 let log = NetworkLog::new();
4231 let m = log.metrics().await;
4232 assert_eq!(m.requests_received, 0);
4233 assert_eq!(m.responses_received, 0);
4234 assert_eq!(m.errors_received, 0);
4235 assert_eq!(m.entries_evicted, 0);
4236 assert_eq!(m.broadcast_drops, 0);
4237 assert_eq!(m.duplicate_responses, 0);
4238 assert_eq!(m.duplicate_errors, 0);
4239 }
4240
4241 #[tokio::test]
4242 async fn test_memory_estimate_empty() {
4243 let log = NetworkLog::new();
4244 let est = log.memory_estimate().await;
4245 assert!(est > 0 || est == 0);
4248 }
4249
4250 #[tokio::test]
4251 async fn test_with_limits_zero_zero() {
4252 let log = NetworkLog::with_limits(0, 0);
4253 for i in 0..100 {
4254 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4255 }
4256 assert_eq!(log.len().await, 100);
4257 }
4258
4259 #[tokio::test]
4260 async fn test_with_limits_small() {
4261 let log = NetworkLog::with_limits(5, 5);
4262 for i in 0..20 {
4263 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4264 }
4265 assert!(log.len().await <= 5);
4266 }
4267
4268 #[tokio::test]
4269 async fn test_wait_for_url_timeout_zero() {
4270 let log = NetworkLog::new();
4271 let result = log.wait_for_url("nonexistent", std::time::Duration::from_secs(0)).await;
4272 assert!(result.is_none());
4273 }
4274
4275 #[tokio::test]
4276 async fn test_wait_for_response_timeout_zero() {
4277 let log = NetworkLog::new();
4278 let result = log.wait_for_response("nonexistent", std::time::Duration::from_secs(0)).await;
4279 assert!(result.is_none());
4280 }
4281
4282 #[tokio::test]
4285 async fn test_filter_header_case_insensitive() {
4286 let log = NetworkLog::new();
4287 let mut req = make_request("1", "GET", "https://a.com");
4288 req.headers = vec![CapturedHeader { name: "X-Custom".into(), value: "SecretValue".into() }];
4289 push_request(&log, req).await;
4290 assert_eq!(log.filter(Filter::new().header("x-custom", "secretvalue")).await.len(), 1);
4291 assert_eq!(log.filter(Filter::new().header("X-CUSTOM", "SECRETVALUE")).await.len(), 1);
4292 }
4293
4294 #[tokio::test]
4295 async fn test_filter_combined_conditions() {
4296 let log = NetworkLog::new();
4297 push_entry(&log, make_request("1", "POST", "https://api.example.com"), Some(make_response("1", 201, "https://api.example.com")), None).await;
4298 push_entry(&log, make_request("2", "GET", "https://api.example.com"), Some(make_response("2", 200, "https://api.example.com")), None).await;
4299 push_entry(&log, make_request("3", "POST", "https://api.example.com"), Some(make_response("3", 500, "https://api.example.com")), None).await;
4300 let f = Filter::new().method("POST").status_range(200..=299);
4301 assert_eq!(log.filter(f).await.len(), 1);
4302 }
4303
4304 #[tokio::test]
4305 async fn test_filter_with_response_only() {
4306 let log = NetworkLog::new();
4307 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4308 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 200, "https://b.com")), None).await;
4309 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
4310 assert_eq!(log.filter(Filter::new().without_response()).await.len(), 1);
4311 }
4312
4313 #[tokio::test]
4314 async fn test_filter_with_error_only() {
4315 let log = NetworkLog::new();
4316 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4317 push_entry(&log, make_request("2", "GET", "https://b.com"), None, Some(make_error("2", "https://b.com", "err"))).await;
4318 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
4319 }
4320
4321 #[tokio::test]
4324 async fn test_to_curl_empty_body() {
4325 let req = make_request("1", "GET", "https://a.com");
4326 let entry = NetworkEntry { request: req, response: None, error: None };
4327 let curl = entry.to_curl();
4328 assert!(!curl.contains("-d"));
4329 assert!(curl.contains("curl -X GET"));
4330 }
4331
4332 #[tokio::test]
4333 async fn test_to_curl_unicode_url() {
4334 let req = make_request("1", "GET", "https://例え.jp/テスト");
4335 let entry = NetworkEntry { request: req, response: None, error: None };
4336 let curl = entry.to_curl();
4337 assert!(curl.contains("https://例え.jp/テスト"));
4338 }
4339
4340 #[tokio::test]
4341 async fn test_to_curl_strips_auto_headers() {
4342 let mut req = make_request("1", "GET", "https://a.com");
4343 req.headers = vec![
4344 CapturedHeader { name: "Host".into(), value: "a.com".into() },
4345 CapturedHeader { name: "Accept-Encoding".into(), value: "gzip".into() },
4346 CapturedHeader { name: "Connection".into(), value: "keep-alive".into() },
4347 CapturedHeader { name: "X-Custom".into(), value: "value".into() },
4348 ];
4349 let entry = NetworkEntry { request: req, response: None, error: None };
4350 let curl = entry.to_curl();
4351 assert!(!curl.contains("Host:"));
4352 assert!(!curl.contains("Accept-Encoding:"));
4353 assert!(!curl.contains("Connection:"));
4354 assert!(curl.contains("X-Custom:"));
4355 }
4356
4357 #[tokio::test]
4360 async fn test_query_params_multiple() {
4361 let req = make_request("1", "GET", "https://a.com?a=1&b=2&c=3");
4362 let params = req.query_params();
4363 assert_eq!(params.len(), 3);
4364 assert!(params.contains(&("a".into(), "1".into())));
4365 assert!(params.contains(&("b".into(), "2".into())));
4366 assert!(params.contains(&("c".into(), "3".into())));
4367 }
4368
4369 #[tokio::test]
4370 async fn test_json_body_invalid() {
4371 let mut req = make_request("1", "POST", "https://a.com");
4372 req.post_data = Some("not json".into());
4373 assert!(req.json_body().is_none());
4374 }
4375
4376 #[tokio::test]
4377 async fn test_json_body_valid() {
4378 let mut req = make_request("1", "POST", "https://a.com");
4379 req.post_data = Some(r#"{"key":"value"}"#.into());
4380 let json = req.json_body().unwrap();
4381 assert_eq!(json["key"], "value");
4382 }
4383
4384 #[tokio::test]
4387 async fn test_request_header_case_insensitive() {
4388 let mut req = make_request("1", "GET", "https://a.com");
4389 req.headers = vec![CapturedHeader { name: "Content-Type".into(), value: "application/json".into() }];
4390 assert_eq!(req.request_header("content-type"), Some("application/json"));
4391 assert_eq!(req.request_header("Content-Type"), Some("application/json"));
4392 assert_eq!(req.request_header("CONTENT-TYPE"), Some("application/json"));
4393 }
4394
4395 #[tokio::test]
4396 async fn test_request_header_missing() {
4397 let req = make_request("1", "GET", "https://a.com");
4398 assert_eq!(req.request_header("x-missing"), None);
4399 }
4400
4401 #[tokio::test]
4402 async fn test_response_header_case_insensitive() {
4403 let mut resp = make_response("1", 200, "https://a.com");
4404 resp.headers = vec![CapturedHeader { name: "X-Response-Header".into(), value: "val".into() }];
4405 let entry = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: Some(resp), error: None };
4406 assert_eq!(entry.response_header("x-response-header"), Some("val"));
4407 assert_eq!(entry.response_header("X-RESPONSE-HEADER"), Some("val"));
4408 }
4409
4410 #[tokio::test]
4413 async fn test_network_entry_final_url() {
4414 let req = make_request("1", "GET", "https://a.com");
4415 let resp = make_response("1", 200, "https://b.com");
4416 let entry = NetworkEntry { request: req.clone(), response: Some(resp), error: None };
4417 assert_eq!(entry.final_url(), "https://b.com");
4418 let entry2 = NetworkEntry { request: req, response: None, error: None };
4419 assert_eq!(entry2.final_url(), "https://a.com");
4420 }
4421
4422 #[tokio::test]
4423 async fn test_network_entry_status() {
4424 let entry = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: Some(make_response("1", 404, "https://a.com")), error: None };
4425 assert_eq!(entry.status(), Some(404));
4426 let entry2 = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: None, error: None };
4427 assert_eq!(entry2.status(), None);
4428 }
4429
4430 use proptest::prelude::*;
4433
4434 #[tokio::test]
4435 async fn test_proptest_filter_never_panics() {
4436 let log = NetworkLog::new();
4437 for i in 0..50 {
4438 let mut req = make_request(&format!("{}", i), &["GET", "POST", "PUT", "DELETE"][i % 4], &format!("https://{}.com", i));
4439 req.headers = vec![CapturedHeader { name: "X-Id".into(), value: format!("{}", i) }];
4440 push_request(&log, req).await;
4441 }
4442
4443 let _ = log.filter(Filter::new().method("GET")).await;
4445 let _ = log.filter(Filter::new().status_range(100..=599)).await;
4446 let _ = log.filter(Filter::new().url_contains("com")).await;
4447 let _ = log.filter(Filter::new().header("x-id", "25")).await;
4448 let _ = log.filter(Filter::new().with_response()).await;
4449 let _ = log.filter(Filter::new().without_response()).await;
4450 let _ = log.filter(Filter::new().with_error()).await;
4451 let _ = log.filter(Filter::new().method("GET").url_contains("com").with_response()).await;
4452 }
4453
4454 #[tokio::test]
4455 async fn test_proptest_concurrent_subscribe_and_push() {
4456 let log = NetworkLog::new();
4457 let mut handles = Vec::new();
4458
4459 for _ in 0..20 {
4460 let log = log.clone();
4461 handles.push(tokio::spawn(async move {
4462 let mut rx = log.subscribe().await;
4463 let mut count = 0;
4464 while count < 50 {
4465 if rx.try_recv().is_ok() {
4466 count += 1;
4467 } else {
4468 tokio::task::yield_now().await;
4469 }
4470 }
4471 }));
4472 }
4473
4474 let log2 = log.clone();
4475 let pusher = tokio::spawn(async move {
4476 for i in 0..100 {
4477 push_request(&log2, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4478 if i % 10 == 0 {
4479 tokio::task::yield_now().await;
4480 }
4481 }
4482 });
4483
4484 pusher.await.unwrap();
4485 for h in handles {
4486 let _ = h.await;
4487 }
4488 }
4489
4490 #[tokio::test]
4491 async fn test_proptest_random_operations() {
4492 let log = NetworkLog::new();
4493 for i in 0..100 {
4494 push_request(&log, make_request(&format!("{}", i), "GET", &format!("https://{}.com", i))).await;
4495 }
4496
4497 log.remove_by_id("50").await;
4499 log.retain(|e| e.request.id != "25").await;
4500 let _ = log.drain_pending().await;
4501 log.clear().await;
4502
4503 assert!(log.is_empty().await);
4504 assert_eq!(log.pending_count().await, 0);
4505 }
4506
4507 proptest! {
4510 #![proptest_config(ProptestConfig::with_cases(256))]
4511
4512 #[test]
4513 fn prop_query_params_no_crash(url in r"https://[a-z0-9]+\.com(/[a-z]*)*(\?[a-z]+=[a-z0-9]*)*") {
4514 let req = CapturedRequest {
4515 id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
4516 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
4517 destination: "document".into(), initiator_type: None,
4518 timing: CapturedTiming::default(),
4519 };
4520 let _ = req.query_params(); }
4522
4523 #[test]
4524 fn prop_request_header_case_insensitive(
4525 name in "[A-Za-z_-]{1,20}",
4526 value in "[a-z0-9]{1,30}",
4527 lookup in "[A-Za-z_-]{1,20}"
4528 ) {
4529 let req = CapturedRequest {
4530 id: "1".into(), context: None, url: "https://a.com".into(), method: "GET".into(),
4531 headers: vec![CapturedHeader { name: name.clone(), value: value.clone() }],
4532 cookies: vec![], post_data: None, timestamp: 0,
4533 destination: "document".into(), initiator_type: None,
4534 timing: CapturedTiming::default(),
4535 };
4536 let result = req.request_header(&lookup);
4537 if name.to_lowercase() == lookup.to_lowercase() {
4538 prop_assert_eq!(result, Some(value.as_str()));
4539 }
4540 }
4541
4542 #[test]
4543 fn prop_json_body_valid_json_roundtrips(obj in "\\{.*\\}") {
4544 let req = CapturedRequest {
4545 id: "1".into(), context: None, url: "https://a.com".into(), method: "POST".into(),
4546 headers: vec![], cookies: vec![], post_data: Some(obj.clone()),
4547 timestamp: 0, destination: "document".into(), initiator_type: None,
4548 timing: CapturedTiming::default(),
4549 };
4550 if let Some(val) = req.json_body() {
4552 let re_encoded = serde_json::to_string(&val).unwrap();
4553 let re_parsed: serde_json::Value = serde_json::from_str(&re_encoded).unwrap();
4554 prop_assert_eq!(val, re_parsed);
4555 }
4556 }
4557
4558 #[test]
4559 fn prop_filter_empty_matches_anything(
4560 method in "(GET|POST|PUT|DELETE|PATCH)",
4561 url in r"https://[a-z]+\.com",
4562 status in 100u16..599u16
4563 ) {
4564 let entry = NetworkEntry {
4565 request: CapturedRequest {
4566 id: "1".into(), context: None, url: url.clone().into(), method: method.into(),
4567 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
4568 destination: "document".into(), initiator_type: None,
4569 timing: CapturedTiming::default(),
4570 },
4571 response: Some(CapturedResponse {
4572 id: "1".into(), url: url.into(), protocol: "h2".into(), status,
4573 status_text: "OK".into(), headers: vec![], mime_type: "text/html".into(),
4574 body_size: None, from_cache: false,
4575 }),
4576 error: None,
4577 };
4578 let filter = Filter::new();
4579 prop_assert!(filter.matches(&entry));
4580 }
4581
4582 #[test]
4583 fn prop_to_curl_always_contains_url(url in r"https://[a-z]+\.com(/[a-z]*)*") {
4584 let entry = NetworkEntry {
4585 request: CapturedRequest {
4586 id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
4587 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
4588 destination: "document".into(), initiator_type: None,
4589 timing: CapturedTiming::default(),
4590 },
4591 response: None, error: None,
4592 };
4593 let curl = entry.to_curl();
4594 prop_assert!(curl.contains(&url), "curl missing URL: {}", curl);
4595 }
4596
4597 #[test]
4598 fn prop_to_curl_get_has_no_data_flag(url in r"https://[a-z]+\.com") {
4599 let entry = NetworkEntry {
4600 request: CapturedRequest {
4601 id: "1".into(), context: None, url: url.into(), method: "GET".into(),
4602 headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
4603 destination: "document".into(), initiator_type: None,
4604 timing: CapturedTiming::default(),
4605 },
4606 response: None, error: None,
4607 };
4608 let curl = entry.to_curl();
4609 prop_assert!(!curl.contains(" -d "), "GET curl should not have -d: {}", curl);
4610 }
4611 }
4612
4613 #[tokio::test]
4616 async fn test_push_entry_with_very_long_url() {
4617 let log = NetworkLog::new();
4618 let long_url = format!("https://a.com/{}", "x".repeat(10_000));
4619 push_request(&log, make_request("1", "GET", &long_url)).await;
4620 assert_eq!(log.first().await.unwrap().request.url.len(), 14 + 10_000);
4621 }
4622
4623 #[tokio::test]
4624 async fn test_push_entry_with_unicode_url() {
4625 let log = NetworkLog::new();
4626 push_request(&log, make_request("1", "GET", "https://例え.jp/テスト")).await;
4627 assert_eq!(log.first().await.unwrap().request.url, "https://例え.jp/テスト");
4628 }
4629
4630 #[tokio::test]
4631 async fn test_filter_url_regex_complex() {
4632 let log = NetworkLog::new();
4633 push_request(&log, make_request("1", "GET", "https://api.example.com/v1/users")).await;
4634 push_request(&log, make_request("2", "GET", "https://api.example.com/v2/items")).await;
4635 push_request(&log, make_request("3", "GET", "https://other.com/v1/users")).await;
4636 let re = regex::Regex::new(r"api\.example\.com/v\d+/users").unwrap();
4637 let found = log.find_by_url_regex(&re).await;
4638 assert_eq!(found.unwrap().request.id, "1");
4639 }
4640
4641 #[tokio::test]
4642 async fn test_filter_url_regex_no_match() {
4643 let log = NetworkLog::new();
4644 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4645 let re = regex::Regex::new(r"nomatch").unwrap();
4646 assert!(log.find_by_url_regex(&re).await.is_none());
4647 }
4648
4649 #[tokio::test]
4650 async fn test_hostnames_with_subdomains() {
4651 let log = NetworkLog::new();
4652 push_request(&log, make_request("1", "GET", "https://sub1.sub2.example.com/path")).await;
4653 push_request(&log, make_request("2", "GET", "https://other.org")).await;
4654 let hosts = log.hostnames().await;
4655 assert_eq!(hosts.len(), 2);
4656 assert!(hosts.contains(&"sub1.sub2.example.com".into()));
4657 }
4658
4659 #[tokio::test]
4660 async fn test_endpoints_deduplicates_full_url() {
4661 let log = NetworkLog::new();
4662 push_request(&log, make_request("1", "GET", "https://a.com/path?x=1")).await;
4663 push_request(&log, make_request("2", "GET", "https://a.com/path?y=2")).await;
4664 let endpoints = log.endpoints().await;
4665 assert_eq!(endpoints.len(), 2);
4667 }
4668
4669 #[tokio::test]
4670 async fn test_endpoints_with_ports() {
4671 let log = NetworkLog::new();
4672 push_request(&log, make_request("1", "GET", "https://a.com:8080/path")).await;
4673 push_request(&log, make_request("2", "GET", "https://a.com:9090/path")).await;
4674 let endpoints = log.endpoints().await;
4675 assert_eq!(endpoints.len(), 2);
4676 }
4677
4678 #[tokio::test]
4679 async fn test_distinct_methods_sorted() {
4680 let log = NetworkLog::new();
4681 push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
4682 push_request(&log, make_request("2", "GET", "https://a.com")).await;
4683 push_request(&log, make_request("3", "POST", "https://a.com")).await;
4684 push_request(&log, make_request("4", "GET", "https://a.com")).await;
4685 assert_eq!(log.distinct_methods().await, vec!["DELETE", "GET", "POST"]);
4686 }
4687
4688 #[tokio::test]
4689 async fn test_distinct_statuses_sorted() {
4690 let log = NetworkLog::new();
4691 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 500, "https://a.com")), None).await;
4692 push_entry(&log, make_request("2", "GET", "https://a.com"), Some(make_response("2", 200, "https://a.com")), None).await;
4693 push_entry(&log, make_request("3", "GET", "https://a.com"), Some(make_response("3", 404, "https://a.com")), None).await;
4694 push_entry(&log, make_request("4", "GET", "https://a.com"), Some(make_response("4", 200, "https://a.com")), None).await;
4695 assert_eq!(log.distinct_statuses().await, vec![200, 404, 500]);
4696 }
4697
4698 #[tokio::test]
4699 async fn test_total_bytes_in_with_none_body_size() {
4700 let log = NetworkLog::new();
4701 let mut resp = make_response("1", 200, "https://a.com");
4702 resp.body_size = None;
4703 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(resp), None).await;
4704 assert_eq!(log.total_bytes_in().await, 0);
4705 }
4706
4707 #[tokio::test]
4708 async fn test_total_bytes_out_with_post_data() {
4709 let log = NetworkLog::new();
4710 let mut req = make_request("1", "POST", "https://a.com");
4711 req.post_data = Some("payload".into());
4712 push_request(&log, req).await;
4713 assert_eq!(log.total_bytes_out().await, 7);
4714 }
4715
4716 #[tokio::test]
4717 async fn test_total_bytes_out_empty() {
4718 let log = NetworkLog::new();
4719 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4720 assert_eq!(log.total_bytes_out().await, 0);
4721 }
4722
4723 #[tokio::test]
4724 async fn test_completed_returns_only_with_response_or_error() {
4725 let log = NetworkLog::new();
4726 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4727 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 200, "https://b.com")), None).await;
4728 push_entry(&log, make_request("3", "GET", "https://c.com"), None, Some(make_error("3", "https://c.com", "err"))).await;
4729 let completed = log.completed().await;
4730 assert_eq!(completed.len(), 2);
4731 }
4732
4733 #[tokio::test]
4734 async fn test_count_with_no_filter() {
4735 let log = NetworkLog::new();
4736 for i in 0..50 {
4737 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4738 }
4739 assert_eq!(log.count(Filter::new()).await, 50);
4740 }
4741
4742 #[tokio::test]
4743 async fn test_find_by_url_partial_match() {
4744 let log = NetworkLog::new();
4745 push_request(&log, make_request("1", "GET", "https://api.example.com/users")).await;
4746 push_request(&log, make_request("2", "GET", "https://other.com/users")).await;
4747 assert_eq!(log.find_by_url("api.example").await.unwrap().request.id, "1");
4748 }
4749
4750 #[tokio::test]
4751 async fn test_subscribe_receives_existing_and_new() {
4752 let log = NetworkLog::new();
4753 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4754 let mut rx = log.subscribe().await;
4755 push_request(&log, make_request("2", "GET", "https://b.com")).await;
4756 let received = rx.try_recv().unwrap();
4758 assert_eq!(received.request.id, "2");
4759 assert!(rx.try_recv().is_err());
4760 }
4761
4762 #[tokio::test]
4763 async fn test_metrics_after_eviction() {
4764 let log = NetworkLog::with_limits(10, 5);
4765 for i in 0..25 {
4766 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4767 }
4768 let m = log.metrics().await;
4769 assert!(m.entries_evicted > 0);
4770 }
4771
4772 #[tokio::test]
4773 async fn test_eviction_preserves_order_of_remaining() {
4774 let log = NetworkLog::with_limits(10, 5);
4775 for i in 0..20 {
4776 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
4777 }
4778 let ids = log.request_ids().await;
4779 for (i, id) in ids.iter().enumerate() {
4781 for j in (i + 1)..ids.len() {
4782 let a: u32 = id.parse().unwrap();
4783 let b: u32 = ids[j].parse().unwrap();
4784 assert!(a < b, "order violated: {} before {}", a, b);
4785 }
4786 }
4787 }
4788
4789 #[tokio::test]
4790 async fn test_captured_timing_defaults() {
4791 let timing = CapturedTiming::default();
4792 assert!(timing.dns_start_ms.is_none());
4793 assert!(timing.dns_end_ms.is_none());
4794 assert!(timing.connect_start_ms.is_none());
4795 assert!(timing.connect_end_ms.is_none());
4796 assert!(timing.tls_start_ms.is_none());
4797 assert!(timing.response_start_ms.is_none());
4798 assert!(timing.response_end_ms.is_none());
4799 }
4800
4801 #[tokio::test]
4802 async fn test_captured_request_header_missing() {
4803 let req = make_request("1", "GET", "https://a.com");
4804 assert_eq!(req.request_header("X-Missing"), None);
4805 }
4806
4807 #[tokio::test]
4808 async fn test_network_entry_is_error() {
4809 let entry = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: None, error: Some(make_error("1", "https://a.com", "err")) };
4810 assert!(entry.is_error());
4811 assert!(!entry.has_response());
4812 }
4813
4814 #[tokio::test]
4815 async fn test_network_entry_has_response() {
4816 let entry = NetworkEntry { request: make_request("1", "GET", "https://a.com"), response: Some(make_response("1", 200, "https://a.com")), error: None };
4817 assert!(entry.has_response());
4818 assert!(!entry.is_error());
4819 }
4820
4821 #[tokio::test]
4822 async fn test_save_to_json_empty_log() {
4823 let log = NetworkLog::new();
4824 let path = std::path::Path::new("/tmp/foxdriver_json_empty.json");
4825 log.save_to_json(path).await.unwrap();
4826 let content = std::fs::read_to_string(path).unwrap();
4827 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
4828 assert!(parsed.is_empty());
4829 }
4830
4831 #[tokio::test]
4832 async fn test_save_to_json_roundtrip_entries() {
4833 let log = NetworkLog::new();
4834 let mut req = make_request("1", "POST", "https://api.example.com");
4835 req.headers = vec![CapturedHeader { name: "Content-Type".into(), value: "application/json".into() }];
4836 req.post_data = Some(r#"{"key":"value"}"#.into());
4837 push_entry(&log, req, Some(make_response("1", 201, "https://api.example.com")), None).await;
4838 let path = std::path::Path::new("/tmp/foxdriver_json_roundtrip.json");
4839 log.save_to_json(path).await.unwrap();
4840 let content = std::fs::read_to_string(path).unwrap();
4841 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
4842 assert_eq!(parsed.len(), 1);
4843 assert_eq!(parsed[0].request.id, "1");
4844 assert_eq!(parsed[0].request.method, "POST");
4845 assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
4846 }
4847
4848 #[tokio::test]
4849 async fn test_har_with_multiple_entries() {
4850 let log = NetworkLog::new();
4851 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
4852 push_entry(&log, make_request("2", "POST", "https://b.com"), Some(make_response("2", 201, "https://b.com")), None).await;
4853 let path = std::path::Path::new("/tmp/foxdriver_har_multi.json");
4854 log.save_as_har(path, Some("test page")).await.unwrap();
4855 let content = std::fs::read_to_string(path).unwrap();
4856 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4857 assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 2);
4858 assert_eq!(parsed["log"]["pages"][0]["title"], "test page");
4859 }
4860
4861 #[tokio::test]
4862 async fn test_har_error_entry_has_no_response() {
4863 let log = NetworkLog::new();
4864 push_entry(&log, make_request("1", "GET", "https://a.com"), None, Some(make_error("1", "https://a.com", "timeout"))).await;
4865 let path = std::path::Path::new("/tmp/foxdriver_har_error.json");
4866 log.save_as_har(path, None).await.unwrap();
4867 let content = std::fs::read_to_string(path).unwrap();
4868 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4869 let entry = &parsed["log"]["entries"][0];
4870 assert!(entry["response"].is_null());
4871 }
4872
4873 #[tokio::test]
4876 async fn test_metrics_requests_received_manual_increment() {
4877 let log = NetworkLog::new();
4878 let m0 = log.metrics().await;
4879 {
4880 let mut inner = log.inner.write().await;
4881 inner.metrics.requests_received += 5;
4882 }
4883 let m1 = log.metrics().await;
4884 assert_eq!(m1.requests_received, m0.requests_received + 5);
4885 }
4886
4887 #[tokio::test]
4888 async fn test_metrics_responses_received_manual_increment() {
4889 let log = NetworkLog::new();
4890 let m0 = log.metrics().await;
4891 {
4892 let mut inner = log.inner.write().await;
4893 inner.metrics.responses_received += 3;
4894 }
4895 let m1 = log.metrics().await;
4896 assert_eq!(m1.responses_received, m0.responses_received + 3);
4897 }
4898
4899 #[tokio::test]
4900 async fn test_metrics_errors_received_manual_increment() {
4901 let log = NetworkLog::new();
4902 let m0 = log.metrics().await;
4903 {
4904 let mut inner = log.inner.write().await;
4905 inner.metrics.errors_received += 2;
4906 }
4907 let m1 = log.metrics().await;
4908 assert_eq!(m1.errors_received, m0.errors_received + 2);
4909 }
4910
4911 #[tokio::test]
4912 async fn test_deduplication_does_not_affect_metrics() {
4913 let log = NetworkLog::new();
4914 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4915 let m1 = log.metrics().await;
4916 push_request(&log, make_request("1", "GET", "https://b.com")).await; let m2 = log.metrics().await;
4918 assert_eq!(m2.requests_received, m1.requests_received); assert_eq!(log.len().await, 1); }
4921
4922 #[tokio::test]
4925 async fn test_filter_status_range_boundary_inclusive() {
4926 let log = NetworkLog::new();
4927 push_entry(&log, make_request("1", "GET", "https://a.com"), Some(make_response("1", 200, "https://a.com")), None).await;
4928 push_entry(&log, make_request("2", "GET", "https://a.com"), Some(make_response("2", 299, "https://a.com")), None).await;
4929 push_entry(&log, make_request("3", "GET", "https://a.com"), Some(make_response("3", 300, "https://a.com")), None).await;
4930 assert_eq!(log.filter(Filter::new().status_range(200..=299)).await.len(), 2);
4931 }
4932
4933 #[tokio::test]
4934 async fn test_filter_destination_deep() {
4935 let log = NetworkLog::new();
4936 let mut req1 = make_request("1", "GET", "https://a.com");
4937 req1.destination = "document".into();
4938 let mut req2 = make_request("2", "GET", "https://b.com");
4939 req2.destination = "image".into();
4940 push_request(&log, req1).await;
4941 push_request(&log, req2).await;
4942 assert_eq!(log.filter(Filter::new().destination("document")).await.len(), 1);
4943 assert_eq!(log.filter(Filter::new().destination("image")).await.len(), 1);
4944 assert_eq!(log.filter(Filter::new().destination("script")).await.len(), 0);
4945 }
4946
4947 #[tokio::test]
4948 async fn test_filter_combined_with_response_and_without_response() {
4949 let log = NetworkLog::new();
4950 push_request(&log, make_request("1", "GET", "https://a.com")).await;
4951 push_entry(&log, make_request("2", "GET", "https://b.com"), Some(make_response("2", 200, "https://b.com")), None).await;
4952 push_entry(&log, make_request("3", "GET", "https://c.com"), None, Some(make_error("3", "https://c.com", "err"))).await;
4953 assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
4954 assert_eq!(log.filter(Filter::new().without_response()).await.len(), 2);
4955 assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
4956 }
4957
4958 #[tokio::test]
4959 async fn test_filter_url_regex_complex_pattern() {
4960 let log = NetworkLog::new();
4961 push_request(&log, make_request("1", "GET", "https://api.example.com/v1/users/123")).await;
4962 push_request(&log, make_request("2", "GET", "https://api.example.com/v2/items/456")).await;
4963 push_request(&log, make_request("3", "GET", "https://other.com/v1/users/789")).await;
4964 let matches = log.filter(Filter::new().url_regex(r"api\.example\.com/v\d+/(users|items)/\d+").unwrap()).await;
4965 assert_eq!(matches.len(), 2);
4966 }
4967
4968 #[tokio::test]
4971 async fn test_captured_request_serde_roundtrip() {
4972 let req = make_request("1", "POST", "https://a.com");
4973 let json = serde_json::to_string(&req).unwrap();
4974 let de: CapturedRequest = serde_json::from_str(&json).unwrap();
4975 assert_eq!(de.id, req.id);
4976 assert_eq!(de.method, req.method);
4977 assert_eq!(de.url, req.url);
4978 }
4979
4980 #[tokio::test]
4981 async fn test_captured_response_serde_roundtrip() {
4982 let resp = make_response("1", 404, "https://a.com");
4983 let json = serde_json::to_string(&resp).unwrap();
4984 let de: CapturedResponse = serde_json::from_str(&json).unwrap();
4985 assert_eq!(de.status, 404);
4986 assert_eq!(de.url, "https://a.com");
4987 }
4988
4989 #[tokio::test]
4990 async fn test_captured_error_serde_roundtrip() {
4991 let err = make_error("1", "https://a.com", "timeout");
4992 let json = serde_json::to_string(&err).unwrap();
4993 let de: CapturedError = serde_json::from_str(&json).unwrap();
4994 assert_eq!(de.error_text, "timeout");
4995 }
4996
4997 #[tokio::test]
4998 async fn test_network_entry_serde_roundtrip() {
4999 let entry = NetworkEntry {
5000 request: make_request("1", "GET", "https://a.com"),
5001 response: Some(make_response("1", 200, "https://a.com")),
5002 error: None,
5003 };
5004 let json = serde_json::to_string(&entry).unwrap();
5005 let de: NetworkEntry = serde_json::from_str(&json).unwrap();
5006 assert_eq!(de.request.id, "1");
5007 assert!(de.response.is_some());
5008 assert!(de.error.is_none());
5009 }
5010
5011 #[tokio::test]
5014 async fn test_concurrent_remove_and_query() {
5015 let log = NetworkLog::new();
5016 for i in 0..100 {
5017 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
5018 }
5019 let mut handles = Vec::new();
5020 for _ in 0..10 {
5021 let log = log.clone();
5022 handles.push(tokio::spawn(async move {
5023 for _ in 0..10 {
5024 let _ = log.remove_by_id("50").await;
5025 let _ = log.len().await;
5026 let _ = log.contains_id("50").await;
5027 }
5028 }));
5029 }
5030 for h in handles {
5031 h.await.unwrap();
5032 }
5033 }
5034
5035 #[tokio::test]
5036 async fn test_concurrent_retain_and_read() {
5037 let log = NetworkLog::new();
5038 for i in 0..100 {
5039 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
5040 }
5041 let mut handles = Vec::new();
5042 for _ in 0..5 {
5043 let log = log.clone();
5044 handles.push(tokio::spawn(async move {
5045 log.retain(|e| e.request.id != "50").await;
5046 }));
5047 }
5048 for _ in 0..5 {
5049 let log = log.clone();
5050 handles.push(tokio::spawn(async move {
5051 for _ in 0..20 {
5052 let _ = log.len().await;
5053 tokio::task::yield_now().await;
5054 }
5055 }));
5056 }
5057 for h in handles {
5058 h.await.unwrap();
5059 }
5060 }
5061
5062 #[tokio::test]
5063 async fn test_concurrent_drain_pending_and_ingest() {
5064 let log = NetworkLog::new();
5065 let mut handles = Vec::new();
5066 for _ in 0..5 {
5067 let log = log.clone();
5068 handles.push(tokio::spawn(async move {
5069 for i in 0..20 {
5070 push_request(&log, make_request(&format!("{}", i), "GET", "https://x.com")).await;
5071 }
5072 }));
5073 }
5074 for _ in 0..5 {
5075 let log = log.clone();
5076 handles.push(tokio::spawn(async move {
5077 for _ in 0..20 {
5078 let _ = log.drain_pending().await;
5079 tokio::task::yield_now().await;
5080 }
5081 }));
5082 }
5083 for h in handles {
5084 h.await.unwrap();
5085 }
5086 }
5087
5088 #[tokio::test]
5091 async fn test_wait_for_url_receives_new_entry() {
5092 let log = NetworkLog::new();
5093 let log2 = log.clone();
5094 let waiter = tokio::spawn(async move {
5095 log2.wait_for_url("target", std::time::Duration::from_secs(1)).await
5096 });
5097 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
5098 push_request(&log, make_request("1", "GET", "https://target.com")).await;
5099 let result = waiter.await.unwrap();
5100 assert!(result.is_some());
5101 assert_eq!(result.unwrap().request.url, "https://target.com");
5102 }
5103
5104 #[tokio::test]
5105 async fn test_wait_for_response_receives_update() {
5106 let log = NetworkLog::new();
5107 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5108 let log2 = log.clone();
5109 let waiter = tokio::spawn(async move {
5110 log2.wait_for_response("1", std::time::Duration::from_secs(1)).await
5111 });
5112 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
5113 {
5114 let mut inner = log.inner.write().await;
5115 let mut new_entry = (*inner.entries[0]).clone();
5116 new_entry.response = Some(make_response("1", 200, "https://a.com"));
5117 inner.entries[0] = Arc::new(new_entry);
5118 let _ = inner.tx.send(inner.entries[0].clone());
5119 }
5120 let result = waiter.await.unwrap();
5121 assert!(result.is_some());
5122 assert!(result.unwrap().has_response());
5123 }
5124
5125 #[tokio::test]
5126 async fn test_filter_combined_method_status_url() {
5127 let log = NetworkLog::new();
5128 push_request(&log, make_request("1", "POST", "https://api.example.com/v1")).await;
5129 push_request(&log, make_request("2", "GET", "https://api.example.com/v2")).await;
5130 {
5131 let mut inner = log.inner.write().await;
5132 let mut e = (*inner.entries[0]).clone();
5133 e.response = Some(make_response("1", 201, "https://api.example.com/v1"));
5134 inner.entries[0] = Arc::new(e);
5135 }
5136 let f = Filter::new()
5137 .method("POST")
5138 .status_range(200..=299)
5139 .url_contains("v1");
5140 let hits = log.filter(f).await;
5141 assert_eq!(hits.len(), 1);
5142 assert_eq!(hits[0].request.id, "1");
5143 }
5144
5145 #[tokio::test]
5146 async fn test_filter_has_error_true() {
5147 let log = NetworkLog::new();
5148 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5149 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5150 {
5151 let mut inner = log.inner.write().await;
5152 let mut e = (*inner.entries[1]).clone();
5153 e.error = Some(CapturedError { id: "2".into(), url: "https://b.com".into(), error_text: "net::ERR_FAILED".into() });
5154 inner.entries[1] = Arc::new(e);
5155 }
5156 let hits = log.filter(Filter::new().with_error()).await;
5157 assert_eq!(hits.len(), 1);
5158 assert_eq!(hits[0].request.id, "2");
5159 }
5160
5161
5162 #[tokio::test]
5163 async fn test_find_by_url_exact_vs_substring() {
5164 let log = NetworkLog::new();
5165 push_request(&log, make_request("1", "GET", "https://example.com/path")).await;
5166 let found = log.find_by_url("/path").await;
5167 assert!(found.is_some());
5168 let not_found = log.find_by_url("/other").await;
5169 assert!(not_found.is_none());
5170 }
5171
5172 #[tokio::test]
5173 async fn test_endpoints_domain_only_url() {
5174 let log = NetworkLog::new();
5175 push_request(&log, make_request("1", "GET", "https://example.com")).await;
5176 let eps = log.endpoints().await;
5177 assert_eq!(eps, vec!["https://example.com"]);
5178 }
5179
5180 #[tokio::test]
5181 async fn test_hostnames_idn_url() {
5182 let log = NetworkLog::new();
5183 push_request(&log, make_request("1", "GET", "https://münchen.de/shop")).await;
5184 let hosts = log.hostnames().await;
5185 assert_eq!(hosts, vec!["xn--mnchen-3ya.de"]);
5187 }
5188
5189 #[tokio::test]
5190 async fn test_total_bytes_in_missing_body_size() {
5191 let log = NetworkLog::new();
5192 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5193 {
5194 let mut inner = log.inner.write().await;
5195 let mut e = (*inner.entries[0]).clone();
5196 e.response = Some(CapturedResponse {
5197 id: "1".into(), url: "https://a.com".into(), protocol: "h2".into(),
5198 status: 200, status_text: "OK".into(), headers: vec![],
5199 mime_type: "text/html".into(), body_size: None, from_cache: false,
5200 });
5201 inner.entries[0] = Arc::new(e);
5202 }
5203 assert_eq!(log.total_bytes_in().await, 0);
5204 }
5205
5206 #[tokio::test]
5207 async fn test_total_bytes_out_empty_post_data() {
5208 let log = NetworkLog::new();
5209 let mut req = make_request("1", "POST", "https://a.com");
5210 req.post_data = Some("".into());
5211 push_request(&log, req).await;
5212 assert_eq!(log.total_bytes_out().await, 0);
5213 }
5214
5215 #[test]
5216 fn test_to_curl_post_json() {
5217 let entry = NetworkEntry {
5218 request: CapturedRequest {
5219 id: "1".into(), context: None, url: "https://api.com".into(), method: "POST".into(),
5220 headers: vec![
5221 CapturedHeader { name: "Content-Type".into(), value: "application/json".into() },
5222 ],
5223 cookies: vec![], post_data: Some(r#"{"key":"val"}"#.into()),
5224 timestamp: 0, destination: "document".into(), initiator_type: None,
5225 timing: CapturedTiming::default(),
5226 },
5227 response: None, error: None,
5228 };
5229 let curl = entry.to_curl();
5230 assert!(curl.contains("-X POST"));
5231 assert!(curl.contains("-H 'Content-Type: application/json'"));
5232 assert!(curl.contains(r#"-d '{"key":"val"}'"#));
5233 }
5234
5235 #[test]
5236 fn test_to_curl_header_with_quote() {
5237 let entry = NetworkEntry {
5238 request: CapturedRequest {
5239 id: "1".into(), context: None, url: "https://a.com".into(), method: "GET".into(),
5240 headers: vec![CapturedHeader { name: "X-Token".into(), value: "it's ok".into() }],
5241 cookies: vec![], post_data: None,
5242 timestamp: 0, destination: "document".into(), initiator_type: None,
5243 timing: CapturedTiming::default(),
5244 },
5245 response: None, error: None,
5246 };
5247 let curl = entry.to_curl();
5248 assert!(curl.contains("X-Token: it'\\''s ok"));
5249 }
5250
5251 #[tokio::test]
5252 async fn test_retain_keeps_some_removes_others() {
5253 let log = NetworkLog::new();
5254 push_request(&log, make_request("1", "GET", "https://keep.com")).await;
5255 push_request(&log, make_request("2", "GET", "https://drop.com")).await;
5256 push_request(&log, make_request("3", "GET", "https://keep.com/page")).await;
5257 log.retain(|e| e.request.url.contains("keep")).await;
5258 let ids = log.request_ids().await;
5259 assert_eq!(ids.len(), 2);
5260 assert!(ids.contains(&"1".into()));
5261 assert!(ids.contains(&"3".into()));
5262 }
5263
5264 #[tokio::test]
5265 async fn test_request_ids_ordering_after_eviction() {
5266 let log = NetworkLog::with_limits(4, 100);
5267 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5268 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5269 push_request(&log, make_request("3", "GET", "https://c.com")).await;
5270 push_request(&log, make_request("4", "GET", "https://d.com")).await;
5271 push_request(&log, make_request("5", "GET", "https://e.com")).await; let ids = log.request_ids().await;
5273 assert_eq!(ids.len(), 3);
5275 assert_eq!(ids, vec!["3".to_string(), "4".to_string(), "5".to_string()]);
5276 }
5277
5278 #[tokio::test]
5279 async fn test_has_response_false_for_nonexistent() {
5280 let log = NetworkLog::new();
5281 assert!(!log.has_response("nope").await);
5282 }
5283
5284 #[tokio::test]
5285 async fn test_has_error_false_for_nonexistent() {
5286 let log = NetworkLog::new();
5287 assert!(!log.has_error("nope").await);
5288 }
5289
5290 #[tokio::test]
5291 async fn test_memory_estimate_increases_with_entries() {
5292 let log = NetworkLog::new();
5293 let empty = log.memory_estimate().await;
5294 push_request(&log, make_request("1", "GET", "https://example.com/some/long/path/here")).await;
5295 let with_one = log.memory_estimate().await;
5296 assert!(with_one > empty);
5297 }
5298
5299 #[tokio::test]
5300 async fn test_save_to_json_empty_log_roundtrip() {
5301 let log = NetworkLog::new();
5302 let tmp = std::env::temp_dir().join(format!("foxdriver_empty_json_{}.json", std::process::id()));
5303 log.save_to_json(&tmp).await.unwrap();
5304 let content = std::fs::read_to_string(&tmp).unwrap();
5305 assert!(content.contains("[]"));
5306 let _ = std::fs::remove_file(&tmp);
5307 }
5308
5309 #[tokio::test]
5310 async fn test_save_as_har_empty_log() {
5311 let log = NetworkLog::new();
5312 let tmp = std::env::temp_dir().join(format!("foxdriver_empty_har_{}.har", std::process::id()));
5313 log.save_as_har(&tmp, Some("empty")).await.unwrap();
5314 let content = std::fs::read_to_string(&tmp).unwrap();
5315 assert!(content.contains("\"entries\""));
5316 assert!(content.contains("\"pages\""));
5317 let _ = std::fs::remove_file(&tmp);
5318 }
5319
5320 #[tokio::test]
5321 async fn test_filter_destination_matching() {
5322 let log = NetworkLog::new();
5323 let mut req = make_request("1", "GET", "https://a.com");
5324 req.destination = "image".into();
5325 push_request(&log, req).await;
5326 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5327 let hits = log.filter(Filter::new().destination("image")).await;
5328 assert_eq!(hits.len(), 1);
5329 assert_eq!(hits[0].request.id, "1");
5330 }
5331
5332 #[tokio::test]
5333 async fn test_distinct_methods_empty_log() {
5334 let log = NetworkLog::new();
5335 assert!(log.distinct_methods().await.is_empty());
5336 }
5337
5338 #[tokio::test]
5339 async fn test_distinct_statuses_no_responses() {
5340 let log = NetworkLog::new();
5341 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5342 assert!(log.distinct_statuses().await.is_empty());
5343 }
5344
5345 #[tokio::test]
5346 async fn test_total_bytes_in_all_none() {
5347 let log = NetworkLog::new();
5348 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5349 {
5350 let mut inner = log.inner.write().await;
5351 let mut e = (*inner.entries[0]).clone();
5352 e.response = Some(CapturedResponse {
5353 id: "1".into(), url: "https://a.com".into(), protocol: "h2".into(),
5354 status: 200, status_text: "OK".into(), headers: vec![],
5355 mime_type: "text/html".into(), body_size: None, from_cache: false,
5356 });
5357 inner.entries[0] = Arc::new(e);
5358 }
5359 assert_eq!(log.total_bytes_in().await, 0);
5360 }
5361
5362 #[tokio::test]
5363 async fn test_total_bytes_out_no_post_data() {
5364 let log = NetworkLog::new();
5365 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5366 assert_eq!(log.total_bytes_out().await, 0);
5367 }
5368
5369 #[tokio::test]
5370 async fn test_save_to_json_roundtrip_with_response() {
5371 let log = NetworkLog::new();
5372 push_request(&log, make_request("1", "POST", "https://api.com")).await;
5373 {
5374 let mut inner = log.inner.write().await;
5375 let mut e = (*inner.entries[0]).clone();
5376 e.response = Some(make_response("1", 201, "https://api.com"));
5377 inner.entries[0] = Arc::new(e);
5378 }
5379 let tmp = std::env::temp_dir().join(format!("foxdriver_json_rt_{}.json", std::process::id()));
5380 log.save_to_json(&tmp).await.unwrap();
5381 let content = std::fs::read_to_string(&tmp).unwrap();
5382 let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5383 assert_eq!(parsed.len(), 1);
5384 assert_eq!(parsed[0].request.id, "1");
5385 assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
5386 let _ = std::fs::remove_file(&tmp);
5387 }
5388
5389 #[tokio::test]
5390 async fn test_save_as_har_with_response_entry() {
5391 let log = NetworkLog::new();
5392 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5393 {
5394 let mut inner = log.inner.write().await;
5395 let mut e = (*inner.entries[0]).clone();
5396 e.response = Some(make_response("1", 200, "https://a.com"));
5397 inner.entries[0] = Arc::new(e);
5398 }
5399 let tmp = std::env::temp_dir().join(format!("foxdriver_har_resp_{}.har", std::process::id()));
5400 log.save_as_har(&tmp, Some("testpage")).await.unwrap();
5401 let content = std::fs::read_to_string(&tmp).unwrap();
5402 assert!(content.contains("\"testpage\""));
5403 assert!(content.contains("200"));
5404 let _ = std::fs::remove_file(&tmp);
5405 }
5406
5407 #[tokio::test]
5408 async fn test_has_response_true_when_present() {
5409 let log = NetworkLog::new();
5410 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5411 assert!(!log.has_response("1").await);
5412 {
5413 let mut inner = log.inner.write().await;
5414 let mut e = (*inner.entries[0]).clone();
5415 e.response = Some(make_response("1", 200, "https://a.com"));
5416 inner.entries[0] = Arc::new(e);
5417 }
5418 assert!(log.has_response("1").await);
5419 }
5420
5421 #[tokio::test]
5422 async fn test_has_error_true_when_present() {
5423 let log = NetworkLog::new();
5424 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5425 assert!(!log.has_error("1").await);
5426 {
5427 let mut inner = log.inner.write().await;
5428 let mut e = (*inner.entries[0]).clone();
5429 e.error = Some(CapturedError { id: "1".into(), url: "https://a.com".into(), error_text: "fail".into() });
5430 inner.entries[0] = Arc::new(e);
5431 }
5432 assert!(log.has_error("1").await);
5433 }
5434
5435 #[tokio::test]
5436 async fn test_memory_estimate_decreases_after_clear() {
5437 let log = NetworkLog::new();
5438 push_request(&log, make_request("1", "GET", "https://example.com/very/long/path/here/for/bytes")).await;
5439 let before = log.memory_estimate().await;
5440 log.clear().await;
5441 let after = log.memory_estimate().await;
5442 assert!(after < before);
5443 }
5444
5445 #[tokio::test]
5446 async fn test_memory_estimate_decreases_after_retain() {
5447 let log = NetworkLog::new();
5448 push_request(&log, make_request("1", "GET", "https://a.com/loooooong")).await;
5449 push_request(&log, make_request("2", "GET", "https://b.com/loooooong")).await;
5450 let before = log.memory_estimate().await;
5451 log.retain(|e| e.request.id == "1").await;
5452 let after = log.memory_estimate().await;
5453 assert!(after < before);
5454 }
5455
5456 #[tokio::test]
5457 async fn test_completed_excludes_in_flight() {
5458 let log = NetworkLog::new();
5459 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5460 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5461 {
5462 let mut inner = log.inner.write().await;
5463 let mut e = (*inner.entries[1]).clone();
5464 e.response = Some(make_response("2", 200, "https://b.com"));
5465 inner.entries[1] = Arc::new(e);
5466 }
5467 let completed = log.completed().await;
5468 assert_eq!(completed.len(), 1);
5469 assert_eq!(completed[0].request.id, "2");
5470 }
5471
5472 #[tokio::test]
5473 async fn test_completed_includes_errors() {
5474 let log = NetworkLog::new();
5475 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5476 {
5477 let mut inner = log.inner.write().await;
5478 let mut e = (*inner.entries[0]).clone();
5479 e.error = Some(CapturedError { id: "1".into(), url: "https://a.com".into(), error_text: "net::ERR_ABORTED".into() });
5480 inner.entries[0] = Arc::new(e);
5481 }
5482 let completed = log.completed().await;
5483 assert_eq!(completed.len(), 1);
5484 assert!(completed[0].is_error());
5485 }
5486
5487 #[tokio::test]
5488 async fn test_find_by_url_regex_complex_pattern() {
5489 let log = NetworkLog::new();
5490 push_request(&log, make_request("1", "GET", "https://api.example.com/v1/users")).await;
5491 push_request(&log, make_request("2", "GET", "https://api.example.com/v2/items")).await;
5492 let re = regex::Regex::new(r"/v\d+/users").unwrap();
5493 let found = log.find_by_url_regex(&re).await;
5494 assert_eq!(found.unwrap().request.id, "1");
5495 }
5496
5497 #[tokio::test]
5498 async fn test_nth_out_of_bounds_returns_none() {
5499 let log = NetworkLog::new();
5500 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5501 assert!(log.nth(5).await.is_none());
5502 }
5503
5504 #[tokio::test]
5505 async fn test_first_last_both_none_on_empty() {
5506 let log = NetworkLog::new();
5507 assert!(log.first().await.is_none());
5508 assert!(log.last().await.is_none());
5509 }
5510
5511 #[tokio::test]
5512 async fn test_remove_by_id_missing_returns_none() {
5513 let log = NetworkLog::new();
5514 assert!(log.remove_by_id("nope").await.is_none());
5515 }
5516
5517 #[tokio::test]
5518 async fn test_contains_id_true_and_false() {
5519 let log = NetworkLog::new();
5520 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5521 assert!(log.contains_id("1").await);
5522 assert!(!log.contains_id("2").await);
5523 }
5524
5525 #[tokio::test]
5526 async fn test_endpoints_dedupes_by_full_url() {
5527 let log = NetworkLog::new();
5529 push_request(&log, make_request("1", "GET", "https://a.com/page?x=1")).await;
5530 push_request(&log, make_request("2", "GET", "https://a.com/page?x=2")).await;
5531 let eps = log.endpoints().await;
5532 assert_eq!(eps.len(), 2);
5533 }
5534
5535 #[tokio::test]
5536 async fn test_hostnames_dedupes() {
5537 let log = NetworkLog::new();
5538 push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
5539 push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
5540 let hosts = log.hostnames().await;
5541 assert_eq!(hosts.len(), 1);
5542 assert_eq!(hosts[0], "a.com");
5543 }
5544
5545 #[tokio::test]
5546 async fn test_drain_pending_empties_maps() {
5547 let log = NetworkLog::new();
5548 {
5550 let mut inner = log.inner.write().await;
5551 inner.pending_responses.insert("1".into(), make_response("1", 200, "https://a.com"));
5552 inner.pending_errors.insert("2".into(), CapturedError { id: "2".into(), url: "https://b.com".into(), error_text: "err".into() });
5553 }
5554 assert_eq!(log.pending_count().await, 2);
5555 let (resps, errs) = log.drain_pending().await;
5556 assert_eq!(resps.len(), 1);
5557 assert_eq!(errs.len(), 1);
5558 assert_eq!(log.pending_count().await, 0);
5559 }
5560
5561 #[tokio::test]
5562 async fn test_metrics_zero_on_fresh_log() {
5563 let log = NetworkLog::new();
5564 let m = log.metrics().await;
5565 assert_eq!(m.requests_received, 0);
5566 assert_eq!(m.responses_received, 0);
5567 assert_eq!(m.errors_received, 0);
5568 assert_eq!(m.entries_evicted, 0);
5569 assert_eq!(m.broadcast_drops, 0);
5570 assert_eq!(m.duplicate_responses, 0);
5571 assert_eq!(m.duplicate_errors, 0);
5572 }
5573
5574 #[tokio::test]
5575 async fn test_metrics_track_entries() {
5576 let log = NetworkLog::new();
5577 push_request(&log, make_request("1", "GET", "https://a.com")).await;
5578 push_request(&log, make_request("2", "GET", "https://b.com")).await;
5579 let m = log.metrics().await;
5582 assert_eq!(m.max_entries, 50_000);
5583 assert_eq!(m.max_entries, log.inner.read().await.max_entries);
5584 }
5585}