1use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Har {
24 pub log: HarLog,
26}
27
28impl Har {
29 #[must_use]
31 pub fn new() -> Self {
32 Self { log: HarLog::new() }
33 }
34
35 pub fn from_json(json: &str) -> Result<Self, HarError> {
41 serde_json::from_str(json).map_err(|e| HarError::ParseError(e.to_string()))
42 }
43
44 pub fn to_json(&self) -> Result<String, HarError> {
50 serde_json::to_string_pretty(self).map_err(|e| HarError::SerializeError(e.to_string()))
51 }
52
53 #[must_use]
55 pub fn entry_count(&self) -> usize {
56 self.log.entries.len()
57 }
58
59 pub fn add_entry(&mut self, entry: HarEntry) {
61 self.log.entries.push(entry);
62 }
63
64 #[must_use]
66 pub fn find_by_url(&self, url: &str) -> Option<&HarEntry> {
67 self.log.entries.iter().find(|e| e.request.url == url)
68 }
69
70 #[must_use]
72 pub fn find_matching(&self, pattern: &str) -> Vec<&HarEntry> {
73 self.log
74 .entries
75 .iter()
76 .filter(|e| url_matches_pattern(&e.request.url, pattern))
77 .collect()
78 }
79}
80
81impl Default for Har {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct HarLog {
90 pub version: String,
92 pub creator: HarCreator,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub browser: Option<HarBrowser>,
97 pub entries: Vec<HarEntry>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub comment: Option<String>,
102}
103
104impl HarLog {
105 #[must_use]
107 pub fn new() -> Self {
108 Self {
109 version: "1.2".to_string(),
110 creator: HarCreator::probar(),
111 browser: None,
112 entries: Vec::new(),
113 comment: None,
114 }
115 }
116}
117
118impl Default for HarLog {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct HarCreator {
127 pub name: String,
129 pub version: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub comment: Option<String>,
134}
135
136impl HarCreator {
137 #[must_use]
139 pub fn probar() -> Self {
140 Self {
141 name: "Probar".to_string(),
142 version: env!("CARGO_PKG_VERSION").to_string(),
143 comment: None,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct HarBrowser {
151 pub name: String,
153 pub version: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub comment: Option<String>,
158}
159
160impl HarBrowser {
161 #[must_use]
163 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
164 Self {
165 name: name.into(),
166 version: version.into(),
167 comment: None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct HarEntry {
175 #[serde(rename = "startedDateTime")]
177 pub started_date_time: String,
178 pub time: f64,
180 pub request: HarRequest,
182 pub response: HarResponse,
184 pub cache: HarCache,
186 pub timings: HarTimings,
188 #[serde(rename = "serverIPAddress", skip_serializing_if = "Option::is_none")]
190 pub server_ip_address: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub connection: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub comment: Option<String>,
197}
198
199impl HarEntry {
200 #[must_use]
202 pub fn new(request: HarRequest, response: HarResponse) -> Self {
203 Self {
204 started_date_time: chrono_now_iso(),
205 time: 0.0,
206 request,
207 response,
208 cache: HarCache::default(),
209 timings: HarTimings::default(),
210 server_ip_address: None,
211 connection: None,
212 comment: None,
213 }
214 }
215
216 #[must_use]
218 pub fn with_time(mut self, time_ms: f64) -> Self {
219 self.time = time_ms;
220 self
221 }
222
223 #[must_use]
225 pub fn with_server_ip(mut self, ip: impl Into<String>) -> Self {
226 self.server_ip_address = Some(ip.into());
227 self
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct HarRequest {
234 pub method: String,
236 pub url: String,
238 #[serde(rename = "httpVersion")]
240 pub http_version: String,
241 pub cookies: Vec<HarCookie>,
243 pub headers: Vec<HarHeader>,
245 #[serde(rename = "queryString")]
247 pub query_string: Vec<HarQueryParam>,
248 #[serde(rename = "postData", skip_serializing_if = "Option::is_none")]
250 pub post_data: Option<HarPostData>,
251 #[serde(rename = "headersSize")]
253 pub headers_size: i64,
254 #[serde(rename = "bodySize")]
256 pub body_size: i64,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub comment: Option<String>,
260}
261
262impl HarRequest {
263 #[must_use]
265 pub fn get(url: impl Into<String>) -> Self {
266 Self::new("GET", url)
267 }
268
269 #[must_use]
271 pub fn post(url: impl Into<String>) -> Self {
272 Self::new("POST", url)
273 }
274
275 #[must_use]
277 pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
278 Self {
279 method: method.into(),
280 url: url.into(),
281 http_version: "HTTP/1.1".to_string(),
282 cookies: Vec::new(),
283 headers: Vec::new(),
284 query_string: Vec::new(),
285 post_data: None,
286 headers_size: -1,
287 body_size: -1,
288 comment: None,
289 }
290 }
291
292 #[must_use]
294 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
295 self.headers.push(HarHeader::new(name, value));
296 self
297 }
298
299 #[must_use]
301 pub fn with_post_data(mut self, data: HarPostData) -> Self {
302 self.post_data = Some(data);
303 self
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct HarResponse {
310 pub status: u16,
312 #[serde(rename = "statusText")]
314 pub status_text: String,
315 #[serde(rename = "httpVersion")]
317 pub http_version: String,
318 pub cookies: Vec<HarCookie>,
320 pub headers: Vec<HarHeader>,
322 pub content: HarContent,
324 #[serde(rename = "redirectURL")]
326 pub redirect_url: String,
327 #[serde(rename = "headersSize")]
329 pub headers_size: i64,
330 #[serde(rename = "bodySize")]
332 pub body_size: i64,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub comment: Option<String>,
336}
337
338impl HarResponse {
339 #[must_use]
341 pub fn ok() -> Self {
342 Self::new(200, "OK")
343 }
344
345 #[must_use]
347 pub fn not_found() -> Self {
348 Self::new(404, "Not Found")
349 }
350
351 #[must_use]
353 pub fn new(status: u16, status_text: impl Into<String>) -> Self {
354 Self {
355 status,
356 status_text: status_text.into(),
357 http_version: "HTTP/1.1".to_string(),
358 cookies: Vec::new(),
359 headers: Vec::new(),
360 content: HarContent::default(),
361 redirect_url: String::new(),
362 headers_size: -1,
363 body_size: -1,
364 comment: None,
365 }
366 }
367
368 #[must_use]
370 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
371 self.headers.push(HarHeader::new(name, value));
372 self
373 }
374
375 #[must_use]
377 pub fn with_content(mut self, content: HarContent) -> Self {
378 self.content = content;
379 self
380 }
381
382 #[must_use]
384 pub fn with_json(mut self, body: impl Into<String>) -> Self {
385 self.content = HarContent::json(body);
386 self.headers
387 .push(HarHeader::new("Content-Type", "application/json"));
388 self
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct HarHeader {
395 pub name: String,
397 pub value: String,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub comment: Option<String>,
402}
403
404impl HarHeader {
405 #[must_use]
407 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
408 Self {
409 name: name.into(),
410 value: value.into(),
411 comment: None,
412 }
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct HarCookie {
419 pub name: String,
421 pub value: String,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub path: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub domain: Option<String>,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub expires: Option<String>,
432 #[serde(rename = "httpOnly", skip_serializing_if = "Option::is_none")]
434 pub http_only: Option<bool>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub secure: Option<bool>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub comment: Option<String>,
441}
442
443impl HarCookie {
444 #[must_use]
446 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
447 Self {
448 name: name.into(),
449 value: value.into(),
450 path: None,
451 domain: None,
452 expires: None,
453 http_only: None,
454 secure: None,
455 comment: None,
456 }
457 }
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct HarQueryParam {
463 pub name: String,
465 pub value: String,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub comment: Option<String>,
470}
471
472impl HarQueryParam {
473 #[must_use]
475 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
476 Self {
477 name: name.into(),
478 value: value.into(),
479 comment: None,
480 }
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct HarPostData {
487 #[serde(rename = "mimeType")]
489 pub mime_type: String,
490 #[serde(default, skip_serializing_if = "Vec::is_empty")]
492 pub params: Vec<HarPostParam>,
493 pub text: String,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub comment: Option<String>,
498}
499
500impl HarPostData {
501 #[must_use]
503 pub fn json(body: impl Into<String>) -> Self {
504 Self {
505 mime_type: "application/json".to_string(),
506 params: Vec::new(),
507 text: body.into(),
508 comment: None,
509 }
510 }
511
512 #[must_use]
514 pub fn form(params: Vec<HarPostParam>) -> Self {
515 Self {
516 mime_type: "application/x-www-form-urlencoded".to_string(),
517 params,
518 text: String::new(),
519 comment: None,
520 }
521 }
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct HarPostParam {
527 pub name: String,
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub value: Option<String>,
532 #[serde(rename = "fileName", skip_serializing_if = "Option::is_none")]
534 pub file_name: Option<String>,
535 #[serde(rename = "contentType", skip_serializing_if = "Option::is_none")]
537 pub content_type: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub comment: Option<String>,
541}
542
543impl HarPostParam {
544 #[must_use]
546 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
547 Self {
548 name: name.into(),
549 value: Some(value.into()),
550 file_name: None,
551 content_type: None,
552 comment: None,
553 }
554 }
555}
556
557#[derive(Debug, Clone, Default, Serialize, Deserialize)]
559pub struct HarContent {
560 pub size: i64,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub compression: Option<i64>,
565 #[serde(rename = "mimeType")]
567 pub mime_type: String,
568 #[serde(skip_serializing_if = "Option::is_none")]
570 pub text: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub encoding: Option<String>,
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub comment: Option<String>,
577}
578
579impl HarContent {
580 #[must_use]
582 pub fn json(body: impl Into<String>) -> Self {
583 let text = body.into();
584 let size = text.len() as i64;
585 Self {
586 size,
587 compression: None,
588 mime_type: "application/json".to_string(),
589 text: Some(text),
590 encoding: None,
591 comment: None,
592 }
593 }
594
595 #[must_use]
597 pub fn text(body: impl Into<String>) -> Self {
598 let text = body.into();
599 let size = text.len() as i64;
600 Self {
601 size,
602 compression: None,
603 mime_type: "text/plain".to_string(),
604 text: Some(text),
605 encoding: None,
606 comment: None,
607 }
608 }
609
610 #[must_use]
612 pub fn html(body: impl Into<String>) -> Self {
613 let text = body.into();
614 let size = text.len() as i64;
615 Self {
616 size,
617 compression: None,
618 mime_type: "text/html".to_string(),
619 text: Some(text),
620 encoding: None,
621 comment: None,
622 }
623 }
624}
625
626#[derive(Debug, Clone, Default, Serialize, Deserialize)]
628pub struct HarCache {
629 #[serde(rename = "beforeRequest", skip_serializing_if = "Option::is_none")]
631 pub before_request: Option<HarCacheState>,
632 #[serde(rename = "afterRequest", skip_serializing_if = "Option::is_none")]
634 pub after_request: Option<HarCacheState>,
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub comment: Option<String>,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct HarCacheState {
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub expires: Option<String>,
646 #[serde(rename = "lastAccess", skip_serializing_if = "Option::is_none")]
648 pub last_access: Option<String>,
649 #[serde(rename = "eTag", skip_serializing_if = "Option::is_none")]
651 pub etag: Option<String>,
652 #[serde(rename = "hitCount", skip_serializing_if = "Option::is_none")]
654 pub hit_count: Option<u32>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub comment: Option<String>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct HarTimings {
663 pub blocked: f64,
665 pub dns: f64,
667 pub connect: f64,
669 pub send: f64,
671 pub wait: f64,
673 pub receive: f64,
675 pub ssl: f64,
677 #[serde(skip_serializing_if = "Option::is_none")]
679 pub comment: Option<String>,
680}
681
682impl Default for HarTimings {
683 fn default() -> Self {
684 Self {
685 blocked: -1.0,
686 dns: -1.0,
687 connect: -1.0,
688 send: 0.0,
689 wait: 0.0,
690 receive: 0.0,
691 ssl: -1.0,
692 comment: None,
693 }
694 }
695}
696
697impl HarTimings {
698 #[must_use]
700 pub fn new() -> Self {
701 Self::default()
702 }
703
704 #[must_use]
706 pub fn total(&self) -> f64 {
707 let mut total = 0.0;
708 if self.blocked > 0.0 {
709 total += self.blocked;
710 }
711 if self.dns > 0.0 {
712 total += self.dns;
713 }
714 if self.connect > 0.0 {
715 total += self.connect;
716 }
717 total += self.send;
718 total += self.wait;
719 total += self.receive;
720 total
721 }
722}
723
724#[derive(Debug, Clone)]
730pub struct HarOptions {
731 pub not_found: NotFoundBehavior,
733 pub update: bool,
735 pub url_pattern: Option<String>,
737}
738
739impl Default for HarOptions {
740 fn default() -> Self {
741 Self {
742 not_found: NotFoundBehavior::Fallback,
743 update: false,
744 url_pattern: None,
745 }
746 }
747}
748
749impl HarOptions {
750 #[must_use]
752 pub fn abort_on_not_found() -> Self {
753 Self {
754 not_found: NotFoundBehavior::Abort,
755 ..Default::default()
756 }
757 }
758
759 #[must_use]
761 pub fn fallback_on_not_found() -> Self {
762 Self {
763 not_found: NotFoundBehavior::Fallback,
764 ..Default::default()
765 }
766 }
767
768 #[must_use]
770 pub fn with_update(mut self, update: bool) -> Self {
771 self.update = update;
772 self
773 }
774
775 #[must_use]
777 pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
778 self.url_pattern = Some(pattern.into());
779 self
780 }
781}
782
783#[derive(Debug, Clone, Copy, PartialEq, Eq)]
785pub enum NotFoundBehavior {
786 Abort,
788 Fallback,
790}
791
792#[derive(Debug)]
794pub struct HarRecorder {
795 har: Har,
797 path: PathBuf,
799 active: bool,
801 filter: Option<String>,
803}
804
805impl HarRecorder {
806 #[must_use]
808 pub fn new(path: impl Into<PathBuf>) -> Self {
809 Self {
810 har: Har::new(),
811 path: path.into(),
812 active: false,
813 filter: None,
814 }
815 }
816
817 pub fn start(&mut self) {
819 self.active = true;
820 }
821
822 pub fn stop(&mut self) {
824 self.active = false;
825 }
826
827 #[must_use]
829 pub fn is_active(&self) -> bool {
830 self.active
831 }
832
833 pub fn set_filter(&mut self, pattern: impl Into<String>) {
835 self.filter = Some(pattern.into());
836 }
837
838 pub fn record(&mut self, entry: HarEntry) {
840 if !self.active {
841 return;
842 }
843
844 if let Some(ref pattern) = self.filter {
846 if !url_matches_pattern(&entry.request.url, pattern) {
847 return;
848 }
849 }
850
851 self.har.add_entry(entry);
852 }
853
854 #[must_use]
856 pub fn har(&self) -> &Har {
857 &self.har
858 }
859
860 #[must_use]
862 pub fn entry_count(&self) -> usize {
863 self.har.entry_count()
864 }
865
866 pub fn save(&self) -> Result<(), HarError> {
872 let json = self.har.to_json()?;
873 std::fs::write(&self.path, json).map_err(|e| HarError::IoError(e.to_string()))
874 }
875}
876
877#[derive(Debug)]
879pub struct HarPlayer {
880 har: Har,
882 options: HarOptions,
884}
885
886impl HarPlayer {
887 #[must_use]
889 pub fn new(har: Har, options: HarOptions) -> Self {
890 Self { har, options }
891 }
892
893 pub fn from_file(path: impl Into<PathBuf>, options: HarOptions) -> Result<Self, HarError> {
899 let path = path.into();
900 let content =
901 std::fs::read_to_string(&path).map_err(|e| HarError::IoError(e.to_string()))?;
902 let har = Har::from_json(&content)?;
903 Ok(Self::new(har, options))
904 }
905
906 #[must_use]
908 pub fn find_response(&self, method: &str, url: &str) -> Option<&HarResponse> {
909 if let Some(ref pattern) = self.options.url_pattern {
911 if !url_matches_pattern(url, pattern) {
912 return None;
913 }
914 }
915
916 self.har.log.entries.iter().find_map(|entry| {
918 if entry.request.method == method && entry.request.url == url {
919 Some(&entry.response)
920 } else {
921 None
922 }
923 })
924 }
925
926 #[must_use]
928 pub fn not_found_behavior(&self) -> NotFoundBehavior {
929 self.options.not_found
930 }
931
932 #[must_use]
934 pub fn entry_count(&self) -> usize {
935 self.har.entry_count()
936 }
937}
938
939#[derive(Debug, Clone, PartialEq, Eq)]
945pub enum HarError {
946 ParseError(String),
948 SerializeError(String),
950 IoError(String),
952 NotFound(String),
954}
955
956impl std::fmt::Display for HarError {
957 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
958 match self {
959 Self::ParseError(msg) => write!(f, "HAR parse error: {msg}"),
960 Self::SerializeError(msg) => write!(f, "HAR serialize error: {msg}"),
961 Self::IoError(msg) => write!(f, "HAR I/O error: {msg}"),
962 Self::NotFound(url) => write!(f, "Request not found in HAR: {url}"),
963 }
964 }
965}
966
967impl std::error::Error for HarError {}
968
969fn chrono_now_iso() -> String {
975 "2024-01-01T00:00:00.000Z".to_string()
977}
978
979fn url_matches_pattern(url: &str, pattern: &str) -> bool {
981 let clean_pattern = pattern
984 .replace("**", "")
985 .replace('*', "")
986 .trim_matches('/')
987 .to_string();
988
989 if clean_pattern.is_empty() {
990 return true; }
992
993 url.contains(&clean_pattern)
994}
995
996#[cfg(test)]
1001#[allow(clippy::unwrap_used, clippy::expect_used)]
1002mod tests {
1003 use super::*;
1004
1005 #[test]
1010 fn h0_har_01_new_creates_empty_har() {
1011 let har = Har::new();
1012 assert_eq!(har.log.version, "1.2");
1013 assert_eq!(har.entry_count(), 0);
1014 }
1015
1016 #[test]
1017 fn h0_har_02_log_has_probar_creator() {
1018 let har = Har::new();
1019 assert_eq!(har.log.creator.name, "Probar");
1020 }
1021
1022 #[test]
1023 fn h0_har_03_add_entry() {
1024 let mut har = Har::new();
1025 let entry = HarEntry::new(HarRequest::get("http://example.com"), HarResponse::ok());
1026 har.add_entry(entry);
1027 assert_eq!(har.entry_count(), 1);
1028 }
1029
1030 #[test]
1031 fn h0_har_04_find_by_url() {
1032 let mut har = Har::new();
1033 har.add_entry(HarEntry::new(
1034 HarRequest::get("http://example.com/api"),
1035 HarResponse::ok(),
1036 ));
1037 assert!(har.find_by_url("http://example.com/api").is_some());
1038 assert!(har.find_by_url("http://other.com").is_none());
1039 }
1040
1041 #[test]
1042 fn h0_har_05_serialization_roundtrip() {
1043 let mut har = Har::new();
1044 har.add_entry(HarEntry::new(
1045 HarRequest::get("http://test.com"),
1046 HarResponse::ok(),
1047 ));
1048 let json = har.to_json().unwrap();
1049 let parsed = Har::from_json(&json).unwrap();
1050 assert_eq!(parsed.entry_count(), 1);
1051 }
1052
1053 #[test]
1054 fn h0_har_06_request_get() {
1055 let req = HarRequest::get("http://test.com");
1056 assert_eq!(req.method, "GET");
1057 assert_eq!(req.url, "http://test.com");
1058 }
1059
1060 #[test]
1061 fn h0_har_07_request_post() {
1062 let req = HarRequest::post("http://test.com");
1063 assert_eq!(req.method, "POST");
1064 }
1065
1066 #[test]
1067 fn h0_har_08_request_with_header() {
1068 let req = HarRequest::get("http://test.com").with_header("Accept", "application/json");
1069 assert_eq!(req.headers.len(), 1);
1070 assert_eq!(req.headers[0].name, "Accept");
1071 }
1072
1073 #[test]
1074 fn h0_har_09_response_ok() {
1075 let resp = HarResponse::ok();
1076 assert_eq!(resp.status, 200);
1077 assert_eq!(resp.status_text, "OK");
1078 }
1079
1080 #[test]
1081 fn h0_har_10_response_not_found() {
1082 let resp = HarResponse::not_found();
1083 assert_eq!(resp.status, 404);
1084 }
1085
1086 #[test]
1091 fn h0_har_11_response_with_json() {
1092 let resp = HarResponse::ok().with_json(r#"{"key": "value"}"#);
1093 assert_eq!(resp.content.mime_type, "application/json");
1094 assert!(resp.content.text.is_some());
1095 }
1096
1097 #[test]
1098 fn h0_har_12_content_json() {
1099 let content = HarContent::json(r#"{"test": true}"#);
1100 assert_eq!(content.mime_type, "application/json");
1101 assert!(content.size > 0);
1102 }
1103
1104 #[test]
1105 fn h0_har_13_content_text() {
1106 let content = HarContent::text("Hello, World!");
1107 assert_eq!(content.mime_type, "text/plain");
1108 }
1109
1110 #[test]
1111 fn h0_har_14_content_html() {
1112 let content = HarContent::html("<html></html>");
1113 assert_eq!(content.mime_type, "text/html");
1114 }
1115
1116 #[test]
1117 fn h0_har_15_post_data_json() {
1118 let data = HarPostData::json(r#"{"name": "test"}"#);
1119 assert_eq!(data.mime_type, "application/json");
1120 }
1121
1122 #[test]
1123 fn h0_har_16_post_data_form() {
1124 let data = HarPostData::form(vec![HarPostParam::new("field", "value")]);
1125 assert_eq!(data.mime_type, "application/x-www-form-urlencoded");
1126 assert_eq!(data.params.len(), 1);
1127 }
1128
1129 #[test]
1130 fn h0_har_17_cookie_creation() {
1131 let cookie = HarCookie::new("session", "abc123");
1132 assert_eq!(cookie.name, "session");
1133 assert_eq!(cookie.value, "abc123");
1134 }
1135
1136 #[test]
1137 fn h0_har_18_header_creation() {
1138 let header = HarHeader::new("X-Custom", "value");
1139 assert_eq!(header.name, "X-Custom");
1140 assert_eq!(header.value, "value");
1141 }
1142
1143 #[test]
1144 fn h0_har_19_query_param() {
1145 let param = HarQueryParam::new("page", "1");
1146 assert_eq!(param.name, "page");
1147 assert_eq!(param.value, "1");
1148 }
1149
1150 #[test]
1151 fn h0_har_20_entry_with_time() {
1152 let entry =
1153 HarEntry::new(HarRequest::get("http://test.com"), HarResponse::ok()).with_time(150.0);
1154 assert!((entry.time - 150.0).abs() < f64::EPSILON);
1155 }
1156
1157 #[test]
1162 fn h0_har_21_recorder_new() {
1163 let recorder = HarRecorder::new("test.har");
1164 assert!(!recorder.is_active());
1165 assert_eq!(recorder.entry_count(), 0);
1166 }
1167
1168 #[test]
1169 fn h0_har_22_recorder_start_stop() {
1170 let mut recorder = HarRecorder::new("test.har");
1171 recorder.start();
1172 assert!(recorder.is_active());
1173 recorder.stop();
1174 assert!(!recorder.is_active());
1175 }
1176
1177 #[test]
1178 fn h0_har_23_recorder_record_when_active() {
1179 let mut recorder = HarRecorder::new("test.har");
1180 recorder.start();
1181 recorder.record(HarEntry::new(
1182 HarRequest::get("http://test.com"),
1183 HarResponse::ok(),
1184 ));
1185 assert_eq!(recorder.entry_count(), 1);
1186 }
1187
1188 #[test]
1189 fn h0_har_24_recorder_skip_when_inactive() {
1190 let mut recorder = HarRecorder::new("test.har");
1191 recorder.record(HarEntry::new(
1192 HarRequest::get("http://test.com"),
1193 HarResponse::ok(),
1194 ));
1195 assert_eq!(recorder.entry_count(), 0);
1196 }
1197
1198 #[test]
1199 fn h0_har_25_recorder_filter() {
1200 let mut recorder = HarRecorder::new("test.har");
1201 recorder.start();
1202 recorder.set_filter("/api/");
1203 recorder.record(HarEntry::new(
1204 HarRequest::get("http://test.com/api/users"),
1205 HarResponse::ok(),
1206 ));
1207 recorder.record(HarEntry::new(
1208 HarRequest::get("http://test.com/static/image.png"),
1209 HarResponse::ok(),
1210 ));
1211 assert_eq!(recorder.entry_count(), 1);
1213 }
1214
1215 #[test]
1216 fn h0_har_26_options_default() {
1217 let options = HarOptions::default();
1218 assert_eq!(options.not_found, NotFoundBehavior::Fallback);
1219 assert!(!options.update);
1220 }
1221
1222 #[test]
1223 fn h0_har_27_options_abort_on_not_found() {
1224 let options = HarOptions::abort_on_not_found();
1225 assert_eq!(options.not_found, NotFoundBehavior::Abort);
1226 }
1227
1228 #[test]
1229 fn h0_har_28_options_with_update() {
1230 let options = HarOptions::default().with_update(true);
1231 assert!(options.update);
1232 }
1233
1234 #[test]
1235 fn h0_har_29_options_with_pattern() {
1236 let options = HarOptions::default().with_pattern("**/api/**");
1237 assert!(options.url_pattern.is_some());
1238 }
1239
1240 #[test]
1241 fn h0_har_30_recorder_har_access() {
1242 let recorder = HarRecorder::new("test.har");
1243 let har = recorder.har();
1244 assert_eq!(har.log.version, "1.2");
1245 }
1246
1247 #[test]
1252 fn h0_har_31_player_new() {
1253 let har = Har::new();
1254 let player = HarPlayer::new(har, HarOptions::default());
1255 assert_eq!(player.entry_count(), 0);
1256 }
1257
1258 #[test]
1259 fn h0_har_32_player_find_response() {
1260 let mut har = Har::new();
1261 har.add_entry(HarEntry::new(
1262 HarRequest::get("http://test.com/api"),
1263 HarResponse::ok().with_json(r#"{"found": true}"#),
1264 ));
1265 let player = HarPlayer::new(har, HarOptions::default());
1266 let resp = player.find_response("GET", "http://test.com/api");
1267 assert!(resp.is_some());
1268 assert_eq!(resp.unwrap().status, 200);
1269 }
1270
1271 #[test]
1272 fn h0_har_33_player_not_found() {
1273 let har = Har::new();
1274 let player = HarPlayer::new(har, HarOptions::default());
1275 let resp = player.find_response("GET", "http://missing.com");
1276 assert!(resp.is_none());
1277 }
1278
1279 #[test]
1280 fn h0_har_34_player_not_found_behavior() {
1281 let player = HarPlayer::new(Har::new(), HarOptions::abort_on_not_found());
1282 assert_eq!(player.not_found_behavior(), NotFoundBehavior::Abort);
1283 }
1284
1285 #[test]
1286 fn h0_har_35_timings_default() {
1287 let timings = HarTimings::default();
1288 assert!(timings.blocked < 0.0);
1289 assert!(timings.dns < 0.0);
1290 }
1291
1292 #[test]
1293 fn h0_har_36_timings_total() {
1294 let mut timings = HarTimings::new();
1295 timings.send = 10.0;
1296 timings.wait = 50.0;
1297 timings.receive = 20.0;
1298 assert!((timings.total() - 80.0).abs() < f64::EPSILON);
1299 }
1300
1301 #[test]
1302 fn h0_har_37_browser_info() {
1303 let browser = HarBrowser::new("Chromium", "120.0");
1304 assert_eq!(browser.name, "Chromium");
1305 assert_eq!(browser.version, "120.0");
1306 }
1307
1308 #[test]
1309 fn h0_har_38_entry_with_server_ip() {
1310 let entry = HarEntry::new(HarRequest::get("http://test.com"), HarResponse::ok())
1311 .with_server_ip("192.168.1.1");
1312 assert_eq!(entry.server_ip_address, Some("192.168.1.1".to_string()));
1313 }
1314
1315 #[test]
1316 fn h0_har_39_error_display() {
1317 let err = HarError::ParseError("invalid json".to_string());
1318 assert!(format!("{err}").contains("parse error"));
1319 }
1320
1321 #[test]
1322 fn h0_har_40_error_not_found() {
1323 let err = HarError::NotFound("http://missing.com".to_string());
1324 assert!(format!("{err}").contains("not found"));
1325 }
1326
1327 #[test]
1332 fn h0_har_41_find_matching_empty() {
1333 let har = Har::new();
1334 let matches = har.find_matching("**/api/**");
1335 assert!(matches.is_empty());
1336 }
1337
1338 #[test]
1339 fn h0_har_42_cache_default() {
1340 let cache = HarCache::default();
1341 assert!(cache.before_request.is_none());
1342 assert!(cache.after_request.is_none());
1343 }
1344
1345 #[test]
1346 fn h0_har_43_response_with_header() {
1347 let resp = HarResponse::ok().with_header("X-Request-Id", "123");
1348 assert_eq!(resp.headers.len(), 1);
1349 }
1350
1351 #[test]
1352 fn h0_har_44_request_with_post_data() {
1353 let req = HarRequest::post("http://test.com").with_post_data(HarPostData::json(r#"{}"#));
1354 assert!(req.post_data.is_some());
1355 }
1356
1357 #[test]
1358 fn h0_har_45_response_with_content() {
1359 let resp = HarResponse::ok().with_content(HarContent::text("body"));
1360 assert_eq!(resp.content.text, Some("body".to_string()));
1361 }
1362
1363 #[test]
1364 fn h0_har_46_parse_error() {
1365 let result = Har::from_json("invalid json");
1366 assert!(result.is_err());
1367 }
1368
1369 #[test]
1370 fn h0_har_47_har_default() {
1371 let har = Har::default();
1372 assert_eq!(har.log.version, "1.2");
1373 }
1374
1375 #[test]
1376 fn h0_har_48_log_default() {
1377 let log = HarLog::default();
1378 assert!(log.entries.is_empty());
1379 }
1380
1381 #[test]
1382 fn h0_har_49_timings_new() {
1383 let timings = HarTimings::new();
1384 assert!(timings.ssl < 0.0);
1385 }
1386
1387 #[test]
1388 fn h0_har_50_content_default() {
1389 let content = HarContent::default();
1390 assert!(content.text.is_none());
1391 assert_eq!(content.size, 0);
1392 }
1393
1394 #[test]
1399 fn h0_har_51_error_serialize_display() {
1400 let err = HarError::SerializeError("failed to serialize".to_string());
1401 let msg = format!("{err}");
1402 assert!(msg.contains("serialize error"));
1403 assert!(msg.contains("failed to serialize"));
1404 }
1405
1406 #[test]
1407 fn h0_har_52_error_io_display() {
1408 let err = HarError::IoError("file not found".to_string());
1409 let msg = format!("{err}");
1410 assert!(msg.contains("I/O error"));
1411 assert!(msg.contains("file not found"));
1412 }
1413
1414 #[test]
1415 fn h0_har_53_options_fallback_on_not_found() {
1416 let options = HarOptions::fallback_on_not_found();
1417 assert_eq!(options.not_found, NotFoundBehavior::Fallback);
1418 assert!(!options.update);
1419 assert!(options.url_pattern.is_none());
1420 }
1421
1422 #[test]
1423 fn h0_har_54_player_find_response_with_pattern_no_match() {
1424 let mut har = Har::new();
1425 har.add_entry(HarEntry::new(
1426 HarRequest::get("http://test.com/users"),
1427 HarResponse::ok(),
1428 ));
1429 let options = HarOptions::default().with_pattern("/api/");
1430 let player = HarPlayer::new(har, options);
1431 let resp = player.find_response("GET", "http://test.com/users");
1433 assert!(resp.is_none());
1434 }
1435
1436 #[test]
1437 fn h0_har_55_player_find_response_with_pattern_match() {
1438 let mut har = Har::new();
1439 har.add_entry(HarEntry::new(
1440 HarRequest::get("http://test.com/api/users"),
1441 HarResponse::ok().with_json(r#"{"users": []}"#),
1442 ));
1443 let options = HarOptions::default().with_pattern("/api/");
1444 let player = HarPlayer::new(har, options);
1445 let resp = player.find_response("GET", "http://test.com/api/users");
1446 assert!(resp.is_some());
1447 assert_eq!(resp.unwrap().status, 200);
1448 }
1449
1450 #[test]
1451 fn h0_har_56_player_find_response_method_mismatch() {
1452 let mut har = Har::new();
1453 har.add_entry(HarEntry::new(
1454 HarRequest::get("http://test.com/api"),
1455 HarResponse::ok(),
1456 ));
1457 let player = HarPlayer::new(har, HarOptions::default());
1458 let resp = player.find_response("POST", "http://test.com/api");
1460 assert!(resp.is_none());
1461 }
1462
1463 #[test]
1464 fn h0_har_57_timings_total_with_positive_values() {
1465 let mut timings = HarTimings::new();
1466 timings.blocked = 5.0;
1467 timings.dns = 10.0;
1468 timings.connect = 15.0;
1469 timings.send = 2.0;
1470 timings.wait = 100.0;
1471 timings.receive = 50.0;
1472 assert!((timings.total() - 182.0).abs() < f64::EPSILON);
1474 }
1475
1476 #[test]
1477 fn h0_har_58_timings_total_with_mixed_values() {
1478 let mut timings = HarTimings::new();
1479 timings.connect = 20.0;
1481 timings.send = 5.0;
1482 timings.wait = 30.0;
1483 timings.receive = 10.0;
1484 assert!((timings.total() - 65.0).abs() < f64::EPSILON);
1486 }
1487
1488 #[test]
1489 fn h0_har_59_find_matching_with_matches() {
1490 let mut har = Har::new();
1491 har.add_entry(HarEntry::new(
1492 HarRequest::get("http://test.com/api/users"),
1493 HarResponse::ok(),
1494 ));
1495 har.add_entry(HarEntry::new(
1496 HarRequest::get("http://test.com/api/posts"),
1497 HarResponse::ok(),
1498 ));
1499 har.add_entry(HarEntry::new(
1500 HarRequest::get("http://test.com/static/image.png"),
1501 HarResponse::ok(),
1502 ));
1503 let matches = har.find_matching("/api/");
1504 assert_eq!(matches.len(), 2);
1505 }
1506
1507 #[test]
1508 fn h0_har_60_url_matches_pattern_glob_wildcards() {
1509 assert!(url_matches_pattern(
1511 "http://test.com/api/users",
1512 "**/api/**"
1513 ));
1514 assert!(url_matches_pattern("http://test.com/api/test", "*/api/*"));
1516 assert!(url_matches_pattern("http://example.com/path", "example"));
1518 assert!(!url_matches_pattern("http://other.com", "example"));
1520 }
1521
1522 #[test]
1523 fn h0_har_61_url_matches_pattern_empty() {
1524 assert!(url_matches_pattern("http://any.com/path", "**"));
1526 assert!(url_matches_pattern("http://any.com/path", "*"));
1527 assert!(url_matches_pattern("http://any.com/path", ""));
1528 }
1529
1530 #[test]
1531 fn h0_har_62_recorder_save_error() {
1532 let recorder = HarRecorder::new("/nonexistent/path/that/cannot/be/written/test.har");
1533 let result = recorder.save();
1534 assert!(result.is_err());
1535 if let Err(HarError::IoError(msg)) = result {
1536 assert!(!msg.is_empty());
1537 } else {
1538 panic!("Expected IoError");
1539 }
1540 }
1541
1542 #[test]
1543 fn h0_har_63_player_from_file_not_found() {
1544 let result = HarPlayer::from_file("/nonexistent/file.har", HarOptions::default());
1545 assert!(result.is_err());
1546 if let Err(HarError::IoError(msg)) = result {
1547 assert!(!msg.is_empty());
1548 } else {
1549 panic!("Expected IoError");
1550 }
1551 }
1552
1553 #[test]
1554 fn h0_har_64_player_from_file_invalid_json() {
1555 let temp_path = std::env::temp_dir().join("test_invalid_har.json");
1557 std::fs::write(&temp_path, "not valid json").unwrap();
1558 let result = HarPlayer::from_file(&temp_path, HarOptions::default());
1559 std::fs::remove_file(&temp_path).ok();
1560 assert!(result.is_err());
1561 if let Err(HarError::ParseError(msg)) = result {
1562 assert!(!msg.is_empty());
1563 } else {
1564 panic!("Expected ParseError");
1565 }
1566 }
1567
1568 #[test]
1569 fn h0_har_65_recorder_save_and_load_roundtrip() {
1570 let temp_path = std::env::temp_dir().join("test_har_roundtrip.har");
1571 let mut recorder = HarRecorder::new(&temp_path);
1572 recorder.start();
1573 recorder.record(HarEntry::new(
1574 HarRequest::get("http://test.com/api").with_header("Accept", "application/json"),
1575 HarResponse::ok().with_json(r#"{"status": "ok"}"#),
1576 ));
1577 recorder.stop();
1578 recorder.save().expect("Save should succeed");
1579
1580 let player = HarPlayer::from_file(&temp_path, HarOptions::default()).unwrap();
1581 assert_eq!(player.entry_count(), 1);
1582 let resp = player.find_response("GET", "http://test.com/api");
1583 assert!(resp.is_some());
1584 assert_eq!(resp.unwrap().status, 200);
1585
1586 std::fs::remove_file(&temp_path).ok();
1587 }
1588
1589 #[test]
1590 fn h0_har_66_har_error_implements_error_trait() {
1591 let err: Box<dyn std::error::Error> = Box::new(HarError::NotFound("test".to_string()));
1592 assert!(!err.to_string().is_empty());
1594 }
1595
1596 #[test]
1597 fn h0_har_67_request_new_custom_method() {
1598 let req = HarRequest::new("DELETE", "http://test.com/resource/123");
1599 assert_eq!(req.method, "DELETE");
1600 assert_eq!(req.url, "http://test.com/resource/123");
1601 assert_eq!(req.http_version, "HTTP/1.1");
1602 assert!(req.cookies.is_empty());
1603 assert!(req.headers.is_empty());
1604 assert!(req.query_string.is_empty());
1605 assert!(req.post_data.is_none());
1606 assert_eq!(req.headers_size, -1);
1607 assert_eq!(req.body_size, -1);
1608 }
1609
1610 #[test]
1611 fn h0_har_68_response_new_custom_status() {
1612 let resp = HarResponse::new(201, "Created");
1613 assert_eq!(resp.status, 201);
1614 assert_eq!(resp.status_text, "Created");
1615 assert_eq!(resp.http_version, "HTTP/1.1");
1616 assert!(resp.cookies.is_empty());
1617 assert!(resp.headers.is_empty());
1618 assert!(resp.redirect_url.is_empty());
1619 assert_eq!(resp.headers_size, -1);
1620 assert_eq!(resp.body_size, -1);
1621 }
1622
1623 #[test]
1624 fn h0_har_69_chrono_now_iso_format() {
1625 let timestamp = chrono_now_iso();
1627 assert!(timestamp.ends_with('Z'));
1628 assert!(timestamp.contains('T'));
1629 assert!(timestamp.contains('-'));
1630 }
1631
1632 #[test]
1633 fn h0_har_70_find_by_url_multiple_entries() {
1634 let mut har = Har::new();
1635 har.add_entry(HarEntry::new(
1636 HarRequest::get("http://test.com/first"),
1637 HarResponse::new(200, "First"),
1638 ));
1639 har.add_entry(HarEntry::new(
1640 HarRequest::get("http://test.com/second"),
1641 HarResponse::new(201, "Second"),
1642 ));
1643 har.add_entry(HarEntry::new(
1644 HarRequest::get("http://test.com/first"),
1645 HarResponse::new(202, "First Again"),
1646 ));
1647 let entry = har.find_by_url("http://test.com/first");
1649 assert!(entry.is_some());
1650 assert_eq!(entry.unwrap().response.status, 200);
1651 }
1652
1653 #[test]
1654 fn h0_har_71_har_log_browser_and_comment() {
1655 let mut log = HarLog::new();
1656 log.browser = Some(HarBrowser::new("Firefox", "115.0"));
1657 log.comment = Some("Test HAR log".to_string());
1658 assert!(log.browser.is_some());
1659 assert_eq!(log.browser.as_ref().unwrap().name, "Firefox");
1660 assert!(log.comment.is_some());
1661 }
1662
1663 #[test]
1664 fn h0_har_72_cookie_optional_fields() {
1665 let mut cookie = HarCookie::new("session", "abc123");
1666 cookie.path = Some("/".to_string());
1667 cookie.domain = Some("example.com".to_string());
1668 cookie.expires = Some("2025-01-01T00:00:00Z".to_string());
1669 cookie.http_only = Some(true);
1670 cookie.secure = Some(true);
1671 cookie.comment = Some("Session cookie".to_string());
1672
1673 assert_eq!(cookie.path, Some("/".to_string()));
1674 assert_eq!(cookie.domain, Some("example.com".to_string()));
1675 assert_eq!(cookie.http_only, Some(true));
1676 assert_eq!(cookie.secure, Some(true));
1677 }
1678
1679 #[test]
1680 fn h0_har_73_post_param_file_upload() {
1681 let mut param = HarPostParam::new("file", "");
1682 param.value = None;
1683 param.file_name = Some("document.pdf".to_string());
1684 param.content_type = Some("application/pdf".to_string());
1685 param.comment = Some("Uploaded file".to_string());
1686
1687 assert!(param.value.is_none());
1688 assert_eq!(param.file_name, Some("document.pdf".to_string()));
1689 assert_eq!(param.content_type, Some("application/pdf".to_string()));
1690 }
1691
1692 #[test]
1693 fn h0_har_74_content_with_encoding() {
1694 let mut content = HarContent::json(r#"{"data": "test"}"#);
1695 content.encoding = Some("base64".to_string());
1696 content.compression = Some(100);
1697 content.comment = Some("Compressed content".to_string());
1698
1699 assert_eq!(content.encoding, Some("base64".to_string()));
1700 assert_eq!(content.compression, Some(100));
1701 }
1702
1703 #[test]
1704 fn h0_har_75_cache_state_fields() {
1705 let state = HarCacheState {
1706 expires: Some("2025-12-31T23:59:59Z".to_string()),
1707 last_access: Some("2024-01-01T00:00:00Z".to_string()),
1708 etag: Some("abc123".to_string()),
1709 hit_count: Some(42),
1710 comment: Some("Cache hit".to_string()),
1711 };
1712
1713 assert_eq!(state.hit_count, Some(42));
1714 assert!(state.etag.is_some());
1715 }
1716
1717 #[test]
1718 fn h0_har_76_cache_with_states() {
1719 let mut cache = HarCache::default();
1720 cache.before_request = Some(HarCacheState {
1721 expires: None,
1722 last_access: None,
1723 etag: Some("before".to_string()),
1724 hit_count: Some(1),
1725 comment: None,
1726 });
1727 cache.after_request = Some(HarCacheState {
1728 expires: None,
1729 last_access: None,
1730 etag: Some("after".to_string()),
1731 hit_count: Some(2),
1732 comment: None,
1733 });
1734 cache.comment = Some("Cache test".to_string());
1735
1736 assert!(cache.before_request.is_some());
1737 assert!(cache.after_request.is_some());
1738 assert_eq!(
1739 cache.before_request.as_ref().unwrap().etag,
1740 Some("before".to_string())
1741 );
1742 }
1743
1744 #[test]
1745 fn h0_har_77_timings_with_comment() {
1746 let mut timings = HarTimings::new();
1747 timings.comment = Some("Timing comment".to_string());
1748 assert!(timings.comment.is_some());
1749 }
1750
1751 #[test]
1752 fn h0_har_78_entry_optional_fields() {
1753 let mut entry = HarEntry::new(HarRequest::get("http://test.com"), HarResponse::ok());
1754 entry.connection = Some("1234".to_string());
1755 entry.comment = Some("Entry comment".to_string());
1756
1757 assert_eq!(entry.connection, Some("1234".to_string()));
1758 assert!(entry.comment.is_some());
1759 }
1760
1761 #[test]
1762 fn h0_har_79_browser_with_comment() {
1763 let mut browser = HarBrowser::new("Chrome", "120.0");
1764 browser.comment = Some("Browser comment".to_string());
1765 assert!(browser.comment.is_some());
1766 }
1767
1768 #[test]
1769 fn h0_har_80_creator_has_version() {
1770 let creator = HarCreator::probar();
1771 assert_eq!(creator.name, "Probar");
1772 assert!(!creator.version.is_empty());
1774 assert!(creator.comment.is_none());
1775 }
1776
1777 #[test]
1778 fn h0_har_81_header_with_comment() {
1779 let mut header = HarHeader::new("Content-Type", "application/json");
1780 header.comment = Some("Header comment".to_string());
1781 assert!(header.comment.is_some());
1782 }
1783
1784 #[test]
1785 fn h0_har_82_query_param_with_comment() {
1786 let mut param = HarQueryParam::new("page", "1");
1787 param.comment = Some("Pagination".to_string());
1788 assert!(param.comment.is_some());
1789 }
1790
1791 #[test]
1792 fn h0_har_83_post_data_with_comment() {
1793 let mut data = HarPostData::json(r#"{"test": true}"#);
1794 data.comment = Some("POST body".to_string());
1795 assert!(data.comment.is_some());
1796 }
1797
1798 #[test]
1799 fn h0_har_84_request_with_comment() {
1800 let mut req = HarRequest::get("http://test.com");
1801 req.comment = Some("Test request".to_string());
1802 assert!(req.comment.is_some());
1803 }
1804
1805 #[test]
1806 fn h0_har_85_response_with_comment() {
1807 let mut resp = HarResponse::ok();
1808 resp.comment = Some("Test response".to_string());
1809 assert!(resp.comment.is_some());
1810 }
1811
1812 #[test]
1813 fn h0_har_86_serialization_with_all_optional_fields() {
1814 let mut har = Har::new();
1815 har.log.browser = Some(HarBrowser::new("Chrome", "120.0"));
1816 har.log.comment = Some("Test log".to_string());
1817
1818 let mut entry = HarEntry::new(HarRequest::get("http://test.com"), HarResponse::ok())
1819 .with_server_ip("127.0.0.1")
1820 .with_time(100.0);
1821 entry.connection = Some("conn-1".to_string());
1822 entry.comment = Some("Entry".to_string());
1823
1824 har.add_entry(entry);
1825
1826 let json = har.to_json().unwrap();
1828 let parsed = Har::from_json(&json).unwrap();
1829
1830 assert!(parsed.log.browser.is_some());
1831 assert!(parsed.log.comment.is_some());
1832 assert_eq!(parsed.entry_count(), 1);
1833 }
1834
1835 #[test]
1836 fn h0_har_87_timings_zero_values() {
1837 let mut timings = HarTimings::new();
1838 timings.blocked = 0.0; timings.dns = 0.0;
1840 timings.connect = 0.0;
1841 timings.send = 0.0;
1842 timings.wait = 0.0;
1843 timings.receive = 0.0;
1844 assert!((timings.total() - 0.0).abs() < f64::EPSILON);
1846 }
1847
1848 #[test]
1849 fn h0_har_88_url_pattern_with_slashes() {
1850 assert!(url_matches_pattern(
1852 "http://test.com/api/v1/users",
1853 "/api/v1/"
1854 ));
1855 assert!(url_matches_pattern(
1856 "http://test.com/api/v1/users",
1857 "api/v1"
1858 ));
1859 }
1860
1861 #[test]
1862 fn h0_har_89_find_matching_partial_pattern() {
1863 let mut har = Har::new();
1864 har.add_entry(HarEntry::new(
1865 HarRequest::get("http://example.com/users/123"),
1866 HarResponse::ok(),
1867 ));
1868 har.add_entry(HarEntry::new(
1869 HarRequest::get("http://other.com/posts"),
1870 HarResponse::ok(),
1871 ));
1872 let matches = har.find_matching("users");
1874 assert_eq!(matches.len(), 1);
1875 assert_eq!(matches[0].request.url, "http://example.com/users/123");
1876 }
1877
1878 #[test]
1879 fn h0_har_90_not_found_behavior_equality() {
1880 assert_eq!(NotFoundBehavior::Abort, NotFoundBehavior::Abort);
1881 assert_eq!(NotFoundBehavior::Fallback, NotFoundBehavior::Fallback);
1882 assert_ne!(NotFoundBehavior::Abort, NotFoundBehavior::Fallback);
1883 }
1884}