1use crate::error::{BenchError, Result};
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::Path;
12
13#[derive(Debug, Deserialize)]
19pub struct HarArchive {
20 pub log: HarLog,
22}
23
24#[derive(Debug, Deserialize)]
26pub struct HarLog {
27 pub entries: Vec<HarEntry>,
29}
30
31#[derive(Debug, Deserialize)]
33pub struct HarEntry {
34 pub request: HarRequest,
36 pub response: HarResponse,
38}
39
40#[derive(Debug, Deserialize)]
42pub struct HarQueryParam {
43 pub name: String,
45 pub value: String,
47}
48
49#[derive(Debug, Deserialize)]
51pub struct HarRequest {
52 pub method: String,
54 pub url: String,
56 #[serde(default)]
58 pub headers: Vec<HarHeader>,
59 #[serde(rename = "queryString", default)]
61 pub query_string: Vec<HarQueryParam>,
62}
63
64#[derive(Debug, Deserialize)]
66pub struct HarResponse {
67 pub status: u16,
69 #[serde(default)]
71 pub headers: Vec<HarHeader>,
72 #[serde(default)]
74 pub content: Option<HarContent>,
75}
76
77#[derive(Debug, Deserialize)]
79pub struct HarHeader {
80 pub name: String,
82 pub value: String,
84}
85
86#[derive(Debug, Deserialize)]
88pub struct HarContent {
89 #[serde(rename = "mimeType", default)]
91 pub mime_type: Option<String>,
92 #[serde(default)]
94 pub text: Option<String>,
95}
96
97#[derive(Debug, Clone)]
103pub struct HarToCustomOptions {
104 pub base_url: Option<String>,
107 pub skip_static: bool,
110 pub include_headers: Vec<String>,
113 pub max_entries: usize,
115}
116
117impl Default for HarToCustomOptions {
118 fn default() -> Self {
119 Self {
120 base_url: None,
121 skip_static: true,
122 include_headers: Vec::new(),
123 max_entries: 0,
124 }
125 }
126}
127
128#[derive(Debug, Serialize)]
133struct OutputConfig {
134 custom_checks: Vec<OutputCheck>,
135}
136
137#[derive(Debug, Serialize)]
138struct OutputCheck {
139 name: String,
140 path: String,
141 method: String,
142 expected_status: u16,
143 #[serde(skip_serializing_if = "HashMap::is_empty")]
144 expected_headers: HashMap<String, String>,
145 #[serde(skip_serializing_if = "Vec::is_empty")]
146 expected_body_fields: Vec<OutputBodyField>,
147}
148
149#[derive(Debug, Serialize)]
150struct OutputBodyField {
151 name: String,
152 #[serde(rename = "type")]
153 field_type: String,
154}
155
156const STATIC_EXTENSIONS: &[&str] = &[
161 ".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf",
162 ".map", ".eot",
163];
164
165const SKIP_HEADERS: &[&str] = &[
170 "connection",
171 "transfer-encoding",
172 "date",
173 "server",
174 "content-length",
175 "vary",
176 "x-request-id",
177];
178
179pub fn generate_custom_yaml_from_har(
185 har_path: &Path,
186 options: HarToCustomOptions,
187) -> Result<String> {
188 let raw = std::fs::read_to_string(har_path).map_err(|e| {
189 BenchError::Other(format!("Failed to read HAR file '{}': {}", har_path.display(), e))
190 })?;
191
192 let archive: HarArchive = serde_json::from_str(&raw).map_err(|e| {
193 BenchError::Other(format!("Failed to parse HAR file '{}': {}", har_path.display(), e))
194 })?;
195
196 generate_custom_yaml(&archive, &options)
197}
198
199fn generate_custom_yaml(archive: &HarArchive, options: &HarToCustomOptions) -> Result<String> {
201 let base_url = match &options.base_url {
203 Some(url) => url.trim_end_matches('/').to_string(),
204 None => detect_base_url(&archive.log.entries)?,
205 };
206
207 let header_matchers = build_header_matchers(&options.include_headers);
208
209 let mut checks = Vec::new();
210
211 for entry in &archive.log.entries {
212 if options.max_entries > 0 && checks.len() >= options.max_entries {
213 break;
214 }
215
216 let path_only = extract_path(&entry.request.url, &base_url);
217
218 if options.skip_static && is_static_asset(&path_only) {
220 continue;
221 }
222
223 let path = match extract_query_string(&entry.request) {
225 Some(qs) => format!("{}?{}", path_only, qs),
226 None => path_only.clone(),
227 };
228
229 let method = entry.request.method.to_uppercase();
230
231 let mut expected_headers = HashMap::new();
233 if !header_matchers.is_empty() {
234 for h in &entry.response.headers {
235 let lower = h.name.to_lowercase();
236 if SKIP_HEADERS.contains(&lower.as_str()) {
237 continue;
238 }
239 if header_matches(&lower, &header_matchers) {
240 expected_headers.insert(h.name.clone(), regex_escape(&h.value));
242 }
243 }
244 }
245
246 let expected_body_fields = extract_body_fields(entry);
248
249 let slug = path_only.replace('/', "-").trim_matches('-').to_string();
251 let name =
252 format!("custom:har:{}-{}-{}", method.to_lowercase(), slug, entry.response.status);
253
254 checks.push(OutputCheck {
255 name,
256 path,
257 method,
258 expected_status: entry.response.status,
259 expected_headers,
260 expected_body_fields,
261 });
262 }
263
264 let config = OutputConfig {
265 custom_checks: checks,
266 };
267
268 serde_yaml::to_string(&config)
269 .map_err(|e| BenchError::Other(format!("Failed to serialize YAML: {}", e)))
270}
271
272fn detect_base_url(entries: &[HarEntry]) -> Result<String> {
278 let first = entries
279 .first()
280 .ok_or_else(|| BenchError::Other("HAR file contains no entries".to_string()))?;
281
282 let parsed = url::Url::parse(&first.request.url).map_err(|e| {
283 BenchError::Other(format!("Failed to parse URL '{}': {}", first.request.url, e))
284 })?;
285
286 let mut base = format!("{}://{}", parsed.scheme(), parsed.host_str().unwrap_or("localhost"));
287 if let Some(port) = parsed.port() {
288 base.push_str(&format!(":{}", port));
289 }
290 Ok(base)
291}
292
293fn extract_path(full_url: &str, base_url: &str) -> String {
295 if let Some(rest) = full_url.strip_prefix(base_url) {
296 if rest.is_empty() {
297 "/".to_string()
298 } else if rest.starts_with('/') {
299 rest.split('?').next().unwrap_or(rest).to_string()
300 } else {
301 format!("/{}", rest.split('?').next().unwrap_or(rest))
302 }
303 } else {
304 match url::Url::parse(full_url) {
306 Ok(parsed) => parsed.path().to_string(),
307 Err(_) => full_url.to_string(),
308 }
309 }
310}
311
312fn extract_query_string(request: &HarRequest) -> Option<String> {
315 if !request.query_string.is_empty() {
316 let pairs: Vec<String> = request
318 .query_string
319 .iter()
320 .map(|p| format!("{}={}", urlencoding::encode(&p.name), urlencoding::encode(&p.value)))
321 .collect();
322 Some(pairs.join("&"))
323 } else {
324 request.url.split_once('?').map(|(_, qs)| qs.to_string())
326 }
327}
328
329fn is_static_asset(path: &str) -> bool {
330 let lower = path.to_lowercase();
331 STATIC_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
332}
333
334const REGEX_META: &[char] = &['*', '+', '?', '[', '|', '^', '$', '.'];
337
338enum HeaderMatcher {
340 Regex(Regex),
341 Exact(String),
342}
343
344fn build_header_matchers(include_headers: &[String]) -> Vec<HeaderMatcher> {
348 include_headers
349 .iter()
350 .map(|h| {
351 let lower = h.to_lowercase();
352 if lower.contains(REGEX_META) {
353 let anchored = format!("^(?:{})$", lower);
355 match Regex::new(&anchored) {
356 Ok(re) => HeaderMatcher::Regex(re),
357 Err(_) => HeaderMatcher::Exact(lower),
359 }
360 } else {
361 HeaderMatcher::Exact(lower)
362 }
363 })
364 .collect()
365}
366
367fn header_matches(lower_name: &str, matchers: &[HeaderMatcher]) -> bool {
369 matchers.iter().any(|m| match m {
370 HeaderMatcher::Exact(exact) => lower_name == exact,
371 HeaderMatcher::Regex(re) => re.is_match(lower_name),
372 })
373}
374
375fn regex_escape(s: &str) -> String {
377 let mut out = String::with_capacity(s.len() + 8);
378 for ch in s.chars() {
379 if "\\^$.|?*+()[]{}".contains(ch) {
380 out.push('\\');
381 }
382 out.push(ch);
383 }
384 out
385}
386
387const MAX_BODY_FIELD_DEPTH: usize = 3;
389
390fn extract_body_fields(entry: &HarEntry) -> Vec<OutputBodyField> {
397 let content = match &entry.response.content {
398 Some(c) => c,
399 None => return Vec::new(),
400 };
401
402 let mime = content.mime_type.as_deref().unwrap_or("");
404 if !mime.contains("json") {
405 return Vec::new();
406 }
407
408 let text = match &content.text {
409 Some(t) if !t.is_empty() => t,
410 _ => return Vec::new(),
411 };
412
413 let value: serde_json::Value = match serde_json::from_str(text) {
414 Ok(v) => v,
415 Err(_) => return Vec::new(),
416 };
417
418 let mut fields = Vec::new();
419 collect_body_fields(&value, "", &mut fields, 0);
420 fields
421}
422
423fn collect_body_fields(
428 value: &serde_json::Value,
429 prefix: &str,
430 out: &mut Vec<OutputBodyField>,
431 depth: usize,
432) {
433 match value {
434 serde_json::Value::Object(map) => {
435 for (k, v) in map {
436 let name = if prefix.is_empty() {
437 k.clone()
438 } else {
439 format!("{}.{}", prefix, k)
440 };
441 out.push(OutputBodyField {
442 name: name.clone(),
443 field_type: json_type_name(v),
444 });
445 if depth < MAX_BODY_FIELD_DEPTH {
447 match v {
448 serde_json::Value::Object(_) => {
449 collect_body_fields(v, &name, out, depth + 1);
450 }
451 serde_json::Value::Array(arr) => {
452 if let Some(serde_json::Value::Object(_)) = arr.first() {
454 let arr_prefix = format!("{}[]", name);
455 collect_body_fields(
456 arr.first().unwrap(),
457 &arr_prefix,
458 out,
459 depth + 1,
460 );
461 }
462 }
463 _ => {}
464 }
465 }
466 }
467 }
468 serde_json::Value::Array(arr) => {
470 if let Some(serde_json::Value::Object(_)) = arr.first() {
471 collect_body_fields(arr.first().unwrap(), prefix, out, depth);
475 }
476 }
477 _ => {}
478 }
479}
480
481fn json_type_name(v: &serde_json::Value) -> String {
482 match v {
483 serde_json::Value::String(_) => "string".to_string(),
484 serde_json::Value::Number(n) => {
485 if n.is_i64() || n.is_u64() {
486 "integer".to_string()
487 } else {
488 "number".to_string()
489 }
490 }
491 serde_json::Value::Bool(_) => "boolean".to_string(),
492 serde_json::Value::Array(_) => "array".to_string(),
493 serde_json::Value::Object(_) => "object".to_string(),
494 serde_json::Value::Null => "string".to_string(), }
496}
497
498#[cfg(test)]
503mod tests {
504 use super::*;
505
506 fn sample_har() -> HarArchive {
507 HarArchive {
508 log: HarLog {
509 entries: vec![
510 HarEntry {
511 request: HarRequest {
512 method: "GET".to_string(),
513 url: "http://localhost:3000/api/users".to_string(),
514 headers: vec![],
515 query_string: vec![],
516 },
517 response: HarResponse {
518 status: 200,
519 headers: vec![
520 HarHeader {
521 name: "content-type".to_string(),
522 value: "application/json".to_string(),
523 },
524 HarHeader {
525 name: "x-request-id".to_string(),
526 value: "abc-123".to_string(),
527 },
528 ],
529 content: Some(HarContent {
530 mime_type: Some("application/json".to_string()),
531 text: Some(
532 r#"[{"id": 1, "name": "Alice", "active": true}]"#.to_string(),
533 ),
534 }),
535 },
536 },
537 HarEntry {
538 request: HarRequest {
539 method: "POST".to_string(),
540 url: "http://localhost:3000/api/users".to_string(),
541 headers: vec![],
542 query_string: vec![],
543 },
544 response: HarResponse {
545 status: 201,
546 headers: vec![HarHeader {
547 name: "content-type".to_string(),
548 value: "application/json".to_string(),
549 }],
550 content: Some(HarContent {
551 mime_type: Some("application/json".to_string()),
552 text: Some(r#"{"id": 2, "name": "Bob"}"#.to_string()),
553 }),
554 },
555 },
556 HarEntry {
557 request: HarRequest {
558 method: "GET".to_string(),
559 url: "http://localhost:3000/static/app.js".to_string(),
560 headers: vec![],
561 query_string: vec![],
562 },
563 response: HarResponse {
564 status: 200,
565 headers: vec![],
566 content: None,
567 },
568 },
569 ],
570 },
571 }
572 }
573
574 #[test]
575 fn test_basic_generation() {
576 let har = sample_har();
577 let options = HarToCustomOptions {
578 skip_static: true,
579 ..Default::default()
580 };
581 let yaml = generate_custom_yaml(&har, &options).unwrap();
582
583 let config: super::super::custom::CustomConformanceConfig =
585 serde_yaml::from_str(&yaml).unwrap();
586 assert_eq!(config.custom_checks.len(), 2);
587 assert_eq!(config.custom_checks[0].method, "GET");
588 assert_eq!(config.custom_checks[0].path, "/api/users");
589 assert_eq!(config.custom_checks[0].expected_status, 200);
590 assert_eq!(config.custom_checks[1].method, "POST");
591 assert_eq!(config.custom_checks[1].expected_status, 201);
592 }
593
594 #[test]
595 fn test_body_field_extraction() {
596 let har = sample_har();
597 let options = HarToCustomOptions::default();
598 let yaml = generate_custom_yaml(&har, &options).unwrap();
599
600 let config: super::super::custom::CustomConformanceConfig =
601 serde_yaml::from_str(&yaml).unwrap();
602
603 let fields = &config.custom_checks[0].expected_body_fields;
605 assert_eq!(fields.len(), 3);
606 assert!(fields.iter().any(|f| f.name == "id" && f.field_type == "integer"));
607 assert!(fields.iter().any(|f| f.name == "name" && f.field_type == "string"));
608 assert!(fields.iter().any(|f| f.name == "active" && f.field_type == "boolean"));
609 }
610
611 #[test]
612 fn test_include_headers() {
613 let har = sample_har();
614 let options = HarToCustomOptions {
615 include_headers: vec!["content-type".to_string()],
616 ..Default::default()
617 };
618 let yaml = generate_custom_yaml(&har, &options).unwrap();
619
620 let config: super::super::custom::CustomConformanceConfig =
621 serde_yaml::from_str(&yaml).unwrap();
622
623 let headers = &config.custom_checks[0].expected_headers;
625 assert!(headers.contains_key("content-type"));
626 assert!(!headers.contains_key("x-request-id"));
627 }
628
629 #[test]
630 fn test_skip_static_false() {
631 let har = sample_har();
632 let options = HarToCustomOptions {
633 skip_static: false,
634 ..Default::default()
635 };
636 let yaml = generate_custom_yaml(&har, &options).unwrap();
637
638 let config: super::super::custom::CustomConformanceConfig =
639 serde_yaml::from_str(&yaml).unwrap();
640 assert_eq!(config.custom_checks.len(), 3);
642 }
643
644 #[test]
645 fn test_max_entries() {
646 let har = sample_har();
647 let options = HarToCustomOptions {
648 skip_static: false,
649 max_entries: 1,
650 ..Default::default()
651 };
652 let yaml = generate_custom_yaml(&har, &options).unwrap();
653
654 let config: super::super::custom::CustomConformanceConfig =
655 serde_yaml::from_str(&yaml).unwrap();
656 assert_eq!(config.custom_checks.len(), 1);
657 }
658
659 #[test]
660 fn test_custom_base_url() {
661 let har = sample_har();
662 let options = HarToCustomOptions {
663 base_url: Some("http://localhost:3000/api".to_string()),
664 ..Default::default()
665 };
666 let yaml = generate_custom_yaml(&har, &options).unwrap();
667
668 let config: super::super::custom::CustomConformanceConfig =
669 serde_yaml::from_str(&yaml).unwrap();
670 assert_eq!(config.custom_checks[0].path, "/users");
671 }
672
673 #[test]
674 fn test_detect_base_url() {
675 let entries = vec![HarEntry {
676 request: HarRequest {
677 method: "GET".to_string(),
678 url: "https://api.example.com:8443/v1/health".to_string(),
679 headers: vec![],
680 query_string: vec![],
681 },
682 response: HarResponse {
683 status: 200,
684 headers: vec![],
685 content: None,
686 },
687 }];
688
689 let base = detect_base_url(&entries).unwrap();
690 assert_eq!(base, "https://api.example.com:8443");
691 }
692
693 #[test]
694 fn test_empty_entries() {
695 let archive = HarArchive {
696 log: HarLog { entries: vec![] },
697 };
698 let result = detect_base_url(&archive.log.entries);
699 assert!(result.is_err());
700 }
701
702 #[test]
703 fn test_regex_escape() {
704 assert_eq!(regex_escape("application/json"), "application/json");
705 assert_eq!(regex_escape("text/html; charset=utf-8"), "text/html; charset=utf-8");
706 assert_eq!(regex_escape("foo.bar"), "foo\\.bar");
707 assert_eq!(regex_escape("a(b)"), "a\\(b\\)");
708 }
709
710 #[test]
711 fn test_extract_path_with_query_string() {
712 let path = extract_path(
713 "http://localhost:3000/api/users?page=1&limit=10",
714 "http://localhost:3000",
715 );
716 assert_eq!(path, "/api/users");
717 }
718
719 #[test]
720 fn test_extract_query_string_from_structured() {
721 let request = HarRequest {
722 method: "GET".to_string(),
723 url: "http://localhost:3000/api/users?page=1&limit=10".to_string(),
724 headers: vec![],
725 query_string: vec![
726 HarQueryParam {
727 name: "page".to_string(),
728 value: "1".to_string(),
729 },
730 HarQueryParam {
731 name: "limit".to_string(),
732 value: "10".to_string(),
733 },
734 ],
735 };
736 let qs = extract_query_string(&request).unwrap();
737 assert_eq!(qs, "page=1&limit=10");
738 }
739
740 #[test]
741 fn test_extract_query_string_from_url_fallback() {
742 let request = HarRequest {
743 method: "GET".to_string(),
744 url: "http://localhost:3000/api/users?page=1&limit=10".to_string(),
745 headers: vec![],
746 query_string: vec![],
747 };
748 let qs = extract_query_string(&request).unwrap();
749 assert_eq!(qs, "page=1&limit=10");
750 }
751
752 #[test]
753 fn test_extract_query_string_none_when_absent() {
754 let request = HarRequest {
755 method: "GET".to_string(),
756 url: "http://localhost:3000/api/users".to_string(),
757 headers: vec![],
758 query_string: vec![],
759 };
760 assert!(extract_query_string(&request).is_none());
761 }
762
763 #[test]
764 fn test_har_with_query_params_in_yaml() {
765 let har = HarArchive {
766 log: HarLog {
767 entries: vec![HarEntry {
768 request: HarRequest {
769 method: "GET".to_string(),
770 url: "http://localhost:3000/api/users?page=1&limit=10".to_string(),
771 headers: vec![],
772 query_string: vec![
773 HarQueryParam {
774 name: "page".to_string(),
775 value: "1".to_string(),
776 },
777 HarQueryParam {
778 name: "limit".to_string(),
779 value: "10".to_string(),
780 },
781 ],
782 },
783 response: HarResponse {
784 status: 200,
785 headers: vec![],
786 content: None,
787 },
788 }],
789 },
790 };
791 let options = HarToCustomOptions::default();
792 let yaml = generate_custom_yaml(&har, &options).unwrap();
793
794 let config: super::super::custom::CustomConformanceConfig =
795 serde_yaml::from_str(&yaml).unwrap();
796 assert_eq!(config.custom_checks[0].path, "/api/users?page=1&limit=10");
797 }
798
799 #[test]
800 fn test_include_headers_regex_pattern() {
801 let har = HarArchive {
802 log: HarLog {
803 entries: vec![HarEntry {
804 request: HarRequest {
805 method: "GET".to_string(),
806 url: "http://localhost:3000/api/data".to_string(),
807 headers: vec![],
808 query_string: vec![],
809 },
810 response: HarResponse {
811 status: 200,
812 headers: vec![
813 HarHeader {
814 name: "content-type".to_string(),
815 value: "application/json".to_string(),
816 },
817 HarHeader {
818 name: "content-length".to_string(),
819 value: "42".to_string(),
820 },
821 HarHeader {
822 name: "x-api-version".to_string(),
823 value: "2".to_string(),
824 },
825 HarHeader {
826 name: "x-api-request-id".to_string(),
827 value: "abc".to_string(),
828 },
829 HarHeader {
830 name: "x-other".to_string(),
831 value: "ignored".to_string(),
832 },
833 HarHeader {
834 name: "cache-control".to_string(),
835 value: "no-cache".to_string(),
836 },
837 ],
838 content: None,
839 },
840 }],
841 },
842 };
843
844 let options = HarToCustomOptions {
845 include_headers: vec![
847 "content-.*".to_string(),
848 "x-api-.*".to_string(),
849 "cache-control".to_string(),
850 ],
851 ..Default::default()
852 };
853 let yaml = generate_custom_yaml(&har, &options).unwrap();
854 let config: super::super::custom::CustomConformanceConfig =
855 serde_yaml::from_str(&yaml).unwrap();
856
857 let headers = &config.custom_checks[0].expected_headers;
858 assert!(headers.contains_key("content-type"), "content-type should match content-.*");
860 assert!(!headers.contains_key("content-length"), "content-length is in skip list");
862 assert!(headers.contains_key("x-api-version"), "x-api-version should match x-api-.*");
864 assert!(
865 headers.contains_key("x-api-request-id"),
866 "x-api-request-id should match x-api-.*"
867 );
868 assert!(!headers.contains_key("x-other"), "x-other should not match");
870 assert!(headers.contains_key("cache-control"), "cache-control exact match");
872 }
873
874 #[test]
875 fn test_include_headers_exact_no_regex() {
876 let matchers = build_header_matchers(&["x-custom".to_string()]);
878 assert!(header_matches("x-custom", &matchers));
879 assert!(!header_matches("x-custom-extra", &matchers));
880 assert!(!header_matches("x-custo", &matchers));
881 }
882
883 #[test]
884 fn test_nested_body_field_extraction() {
885 let entry = HarEntry {
886 request: HarRequest {
887 method: "GET".to_string(),
888 url: "http://localhost:3000/api/data".to_string(),
889 headers: vec![],
890 query_string: vec![],
891 },
892 response: HarResponse {
893 status: 200,
894 headers: vec![],
895 content: Some(HarContent {
896 mime_type: Some("application/json".to_string()),
897 text: Some(
898 r#"{"total": 10, "results": {"name": "Alice", "count": 5}, "tags": ["a"]}"#
899 .to_string(),
900 ),
901 }),
902 },
903 };
904
905 let fields = extract_body_fields(&entry);
906 let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
907
908 assert!(names.contains(&"total"));
910 assert!(names.contains(&"results"));
911 assert!(names.contains(&"tags"));
912 assert!(names.contains(&"results.name"));
914 assert!(names.contains(&"results.count"));
915
916 let results_name = fields.iter().find(|f| f.name == "results.name").unwrap();
918 assert_eq!(results_name.field_type, "string");
919 let results_count = fields.iter().find(|f| f.name == "results.count").unwrap();
920 assert_eq!(results_count.field_type, "integer");
921 }
922
923 #[test]
924 fn test_nested_array_body_field_extraction() {
925 let entry = HarEntry {
926 request: HarRequest {
927 method: "GET".to_string(),
928 url: "http://localhost:3000/api/data".to_string(),
929 headers: vec![],
930 query_string: vec![],
931 },
932 response: HarResponse {
933 status: 200,
934 headers: vec![],
935 content: Some(HarContent {
936 mime_type: Some("application/json".to_string()),
937 text: Some(r#"{"items": [{"id": 1, "label": "foo"}]}"#.to_string()),
938 }),
939 },
940 };
941
942 let fields = extract_body_fields(&entry);
943 let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
944
945 assert!(names.contains(&"items"));
946 assert!(names.contains(&"items[].id"));
947 assert!(names.contains(&"items[].label"));
948 }
949
950 #[test]
951 fn test_nested_depth_limit() {
952 let entry = HarEntry {
954 request: HarRequest {
955 method: "GET".to_string(),
956 url: "http://localhost:3000/deep".to_string(),
957 headers: vec![],
958 query_string: vec![],
959 },
960 response: HarResponse {
961 status: 200,
962 headers: vec![],
963 content: Some(HarContent {
964 mime_type: Some("application/json".to_string()),
965 text: Some(r#"{"a": {"b": {"c": {"d": {"e": 1}}}}}"#.to_string()),
966 }),
967 },
968 };
969
970 let fields = extract_body_fields(&entry);
971 let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
972
973 assert!(names.contains(&"a"));
975 assert!(names.contains(&"a.b"));
977 assert!(names.contains(&"a.b.c"));
979 assert!(names.contains(&"a.b.c.d"));
981 assert!(!names.contains(&"a.b.c.d.e"), "should not recurse beyond depth 3");
983 }
984
985 #[test]
986 fn test_check_name_format() {
987 let har = sample_har();
988 let options = HarToCustomOptions::default();
989 let yaml = generate_custom_yaml(&har, &options).unwrap();
990
991 let config: super::super::custom::CustomConformanceConfig =
992 serde_yaml::from_str(&yaml).unwrap();
993 assert!(config.custom_checks[0].name.starts_with("custom:"));
994 }
995}