Skip to main content

mockforge_import/import/
postman_import.rs

1//! Postman collection import functionality
2//!
3//! This module handles parsing Postman collections and converting them
4//! to MockForge routes and configurations.
5
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use std::collections::HashMap;
9
10/// Postman collection structure
11#[derive(Debug, Deserialize)]
12pub struct PostmanCollection {
13    /// Collection metadata
14    pub info: CollectionInfo,
15    /// Array of collection items (requests or folders)
16    pub item: Vec<CollectionItem>,
17    /// Collection-level variables
18    #[serde(default)]
19    pub variable: Vec<Variable>,
20}
21
22/// Collection metadata
23#[derive(Debug, Deserialize)]
24pub struct CollectionInfo {
25    /// Postman collection ID
26    #[serde(rename = "_postman_id")]
27    pub postman_id: Option<String>,
28    /// Collection name
29    pub name: String,
30    /// Optional collection description
31    pub description: Option<String>,
32    /// Postman schema URL
33    pub schema: Option<String>,
34}
35
36/// Collection item (can be a request or a folder)
37#[derive(Debug, Deserialize)]
38pub struct CollectionItem {
39    /// Item name
40    pub name: String,
41    /// Child items (for folders)
42    #[serde(default)]
43    pub item: Vec<CollectionItem>, // For folders
44    /// Request data (None if this is a folder)
45    pub request: Option<PostmanRequest>,
46}
47
48/// Postman request structure
49#[derive(Debug, Deserialize)]
50pub struct PostmanRequest {
51    /// HTTP method (GET, POST, PUT, etc.)
52    pub method: String,
53    /// Request headers
54    #[serde(default)]
55    pub header: Vec<Header>,
56    /// Request URL (can be string or structured)
57    pub url: UrlOrString,
58    /// Optional request body
59    #[serde(default)]
60    pub body: Option<RequestBody>,
61    /// Optional authentication configuration
62    pub auth: Option<Auth>,
63}
64
65/// URL can be a string or structured object
66#[derive(Debug, Deserialize)]
67#[serde(untagged)]
68pub enum UrlOrString {
69    /// Simple string URL
70    String(String),
71    /// Structured URL with components
72    Structured(StructuredUrl),
73}
74
75/// Structured URL with host, path, query, etc.
76#[derive(Debug, Deserialize)]
77pub struct StructuredUrl {
78    /// Raw URL string
79    pub raw: Option<String>,
80    /// URL protocol (http, https, etc.)
81    pub protocol: Option<String>,
82    /// URL host parts
83    pub host: Option<Vec<String>>,
84    /// URL path segments
85    pub path: Option<Vec<StringOrVariable>>,
86    /// Query parameters
87    #[serde(default)]
88    pub query: Vec<QueryParam>,
89    /// URL variables
90    #[serde(default)]
91    pub variable: Vec<Variable>,
92}
93
94/// Variable or string in URL components
95#[derive(Debug, Deserialize)]
96#[serde(untagged)]
97pub enum StringOrVariable {
98    /// Plain string value
99    String(String),
100    /// Variable reference
101    Variable(Variable),
102}
103
104/// Query parameter
105#[derive(Debug, Deserialize)]
106pub struct QueryParam {
107    /// Parameter key
108    pub key: Option<String>,
109    /// Parameter value
110    pub value: Option<String>,
111    /// Optional parameter description
112    pub description: Option<String>,
113    /// Whether this parameter is disabled
114    #[serde(default)]
115    pub disabled: bool,
116}
117
118/// HTTP header entry
119#[derive(Debug, Deserialize)]
120pub struct Header {
121    /// Header name
122    pub key: String,
123    /// Header value
124    pub value: String,
125    /// Whether this header is disabled
126    #[serde(default)]
127    pub disabled: bool,
128}
129
130/// Request body structure
131#[derive(Debug, Deserialize)]
132pub struct RequestBody {
133    /// Body mode (raw, urlencoded, formdata, etc.)
134    pub mode: String,
135    /// Raw body content (for raw mode)
136    pub raw: Option<String>,
137    /// URL-encoded form parameters
138    pub urlencoded: Option<Vec<FormParam>>,
139    /// Form data parameters
140    pub formdata: Option<Vec<FormParam>>,
141}
142
143/// Form parameter entry
144#[derive(Debug, Deserialize)]
145pub struct FormParam {
146    /// Parameter key
147    pub key: String,
148    /// Parameter value
149    pub value: String,
150    /// Parameter type (text, file, etc.)
151    #[serde(rename = "type")]
152    pub param_type: Option<String>,
153}
154
155/// Authentication configuration
156#[derive(Debug, Deserialize)]
157pub struct Auth {
158    /// Authentication type (bearer, basic, etc.)
159    #[serde(rename = "type")]
160    pub auth_type: String,
161    /// Authentication configuration (type-specific)
162    #[serde(flatten)]
163    pub config: Value,
164}
165
166/// Postman variable definition
167#[derive(Debug, Deserialize)]
168pub struct Variable {
169    /// Variable name
170    pub key: String,
171    /// Variable value
172    pub value: Option<String>,
173    /// Variable type (string, number, etc.)
174    #[serde(rename = "type")]
175    pub var_type: Option<String>,
176}
177
178/// MockForge route structure for import
179#[derive(Debug, Serialize)]
180pub struct MockForgeRoute {
181    /// HTTP method
182    pub method: String,
183    /// Request path
184    pub path: String,
185    /// Request headers
186    pub headers: HashMap<String, String>,
187    /// Optional request body
188    pub body: Option<String>,
189    /// Mock response for this route
190    pub response: MockForgeResponse,
191}
192
193/// MockForge response structure
194#[derive(Debug, Serialize)]
195pub struct MockForgeResponse {
196    /// HTTP status code
197    pub status: u16,
198    /// Response headers
199    pub headers: HashMap<String, String>,
200    /// Response body
201    pub body: Value,
202}
203
204/// Result of importing a Postman collection
205pub struct ImportResult {
206    /// Converted routes from the collection
207    pub routes: Vec<MockForgeRoute>,
208    /// Extracted variables from the collection
209    pub variables: HashMap<String, String>,
210    /// Warnings encountered during import
211    pub warnings: Vec<String>,
212}
213
214/// Import a Postman collection
215pub fn import_postman_collection(
216    content: &str,
217    base_url: Option<&str>,
218) -> Result<ImportResult, String> {
219    let collection: PostmanCollection = serde_json::from_str(content)
220        .map_err(|e| format!("Failed to parse Postman collection: {}", e))?;
221
222    let mut routes = Vec::new();
223    let mut variables = HashMap::new();
224    let mut warnings = Vec::new();
225
226    // Extract global variables
227    for var in &collection.variable {
228        if let Some(value) = &var.value {
229            variables.insert(var.key.clone(), value.clone());
230        }
231    }
232
233    // Process all items (recursive for folders)
234    process_items(&collection.item, &mut routes, &variables, base_url, &mut warnings);
235
236    Ok(ImportResult {
237        routes,
238        variables,
239        warnings,
240    })
241}
242
243/// Recursively process collection items
244fn process_items(
245    items: &[CollectionItem],
246    routes: &mut Vec<MockForgeRoute>,
247    variables: &HashMap<String, String>,
248    base_url: Option<&str>,
249    warnings: &mut Vec<String>,
250) {
251    for item in items {
252        // Check if this item has a request (flattened fields)
253        if item.request.is_some() {
254            if let Some(request) = &item.request {
255                // This is a request
256                match convert_request_to_route(request, &item.name, variables, base_url) {
257                    Ok(route) => routes.push(route),
258                    Err(e) => {
259                        warnings.push(format!("Failed to convert request '{}': {}", item.name, e))
260                    }
261                }
262            }
263        } else if !item.item.is_empty() {
264            // This is a folder, process recursively
265            process_items(&item.item, routes, variables, base_url, warnings);
266        }
267    }
268}
269
270/// Convert a Postman request to a MockForge route
271fn convert_request_to_route(
272    request: &PostmanRequest,
273    _name: &str,
274    variables: &HashMap<String, String>,
275    base_url: Option<&str>,
276) -> Result<MockForgeRoute, String> {
277    // Build URL
278    let url = build_url(&request.url, variables, base_url)?;
279
280    // Extract headers
281    let mut headers = HashMap::new();
282    for header in &request.header {
283        if !header.disabled && !header.key.is_empty() {
284            headers.insert(header.key.clone(), resolve_variables(&header.value, variables));
285        }
286    }
287
288    // Extract body
289    let body = match &request.body {
290        Some(body) if body.mode == "raw" => {
291            body.raw.as_ref().map(|raw| resolve_variables(raw, variables))
292        }
293        Some(body) if body.mode == "urlencoded" => {
294            if let Some(form_params) = &body.urlencoded {
295                let encoded_params: Vec<String> = form_params
296                    .iter()
297                    .map(|param| {
298                        let key = resolve_variables(&param.key, variables);
299                        let value = resolve_variables(&param.value, variables);
300                        format!("{}={}", key, value)
301                    })
302                    .collect();
303                if encoded_params.is_empty() {
304                    None
305                } else {
306                    Some(encoded_params.join("&"))
307                }
308            } else {
309                None
310            }
311        }
312        Some(body) if body.mode == "formdata" => {
313            if let Some(form_params) = &body.formdata {
314                let encoded_params: Vec<String> = form_params
315                    .iter()
316                    .map(|param| {
317                        let key = resolve_variables(&param.key, variables);
318                        let value = resolve_variables(&param.value, variables);
319                        format!("{}={}", key, value)
320                    })
321                    .collect();
322                if encoded_params.is_empty() {
323                    None
324                } else {
325                    Some(encoded_params.join("&"))
326                }
327            } else {
328                None
329            }
330        }
331        _ => None,
332    };
333
334    // Generate mock response
335    let response = generate_mock_response(request, variables);
336
337    Ok(MockForgeRoute {
338        method: request.method.clone(),
339        path: url,
340        headers,
341        body,
342        response,
343    })
344}
345
346/// Build URL from Postman URL structure
347fn build_url(
348    url: &UrlOrString,
349    variables: &HashMap<String, String>,
350    base_url: Option<&str>,
351) -> Result<String, String> {
352    let raw_url = match url {
353        UrlOrString::String(s) => resolve_variables(s, variables),
354        UrlOrString::Structured(structured) => {
355            if let Some(raw) = &structured.raw {
356                resolve_variables(raw, variables)
357            } else {
358                // Build URL from components
359                let mut url_parts = Vec::new();
360
361                // Protocol
362                if let Some(protocol) = &structured.protocol {
363                    url_parts.push(format!("{}://", protocol));
364                }
365
366                // Host
367                if let Some(host_parts) = &structured.host {
368                    let host = host_parts.join(".");
369                    url_parts.push(resolve_variables(&host, variables));
370                }
371
372                // Path
373                if let Some(path_parts) = &structured.path {
374                    let path: Vec<String> = path_parts
375                        .iter()
376                        .map(|part| match part {
377                            StringOrVariable::String(s) => resolve_variables(s, variables),
378                            StringOrVariable::Variable(var) => {
379                                if let Some(value) = variables.get(&var.key) {
380                                    value.clone()
381                                } else {
382                                    var.key.clone()
383                                }
384                            }
385                        })
386                        .collect();
387                    url_parts.push(path.join("/"));
388                }
389
390                // Query
391                let query_parts: Vec<String> = structured
392                    .query
393                    .iter()
394                    .filter(|q| !q.disabled && q.key.is_some())
395                    .map(|q| {
396                        let key = resolve_variables(q.key.as_ref().unwrap(), variables);
397                        let value = q
398                            .value
399                            .as_ref()
400                            .map(|v| resolve_variables(v, variables))
401                            .unwrap_or_default();
402                        format!("{}={}", key, value)
403                    })
404                    .collect();
405
406                if !query_parts.is_empty() {
407                    url_parts.push(format!("?{}", query_parts.join("&")));
408                }
409
410                url_parts.join("")
411            }
412        }
413    };
414
415    // If base_url is provided, make path relative
416    if let Some(base) = base_url {
417        if raw_url.starts_with(base) {
418            let relative_path = raw_url.trim_start_matches(base).trim_start_matches('/');
419            return Ok(if relative_path.is_empty() {
420                "/".to_string()
421            } else {
422                format!("/{}", relative_path)
423            });
424        }
425    }
426
427    // Extract path from full URL
428    if let Ok(url) = url::Url::parse(&raw_url) {
429        Ok(url.path().to_string())
430    } else {
431        // Assume it's already a path
432        Ok(raw_url)
433    }
434}
435
436/// Resolve variables in a string
437fn resolve_variables(input: &str, variables: &HashMap<String, String>) -> String {
438    let mut result = input.to_string();
439    for (key, value) in variables {
440        let pattern = format!("{{{{{}}}}}", key);
441        result = result.replace(&pattern, value);
442    }
443    result
444}
445
446/// Generate a mock response for the request
447fn generate_mock_response(
448    request: &PostmanRequest,
449    _variables: &HashMap<String, String>,
450) -> MockForgeResponse {
451    let mut headers = HashMap::new();
452    headers.insert("Content-Type".to_string(), "application/json".to_string());
453
454    let body = match request.method.as_str() {
455        "GET" => json!({"message": "Mock GET response", "method": "GET"}),
456        "POST" => json!({"message": "Mock POST response", "method": "POST", "created": true}),
457        "PUT" => json!({"message": "Mock PUT response", "method": "PUT", "updated": true}),
458        "DELETE" => json!({"message": "Mock DELETE response", "method": "DELETE", "deleted": true}),
459        "PATCH" => json!({"message": "Mock PATCH response", "method": "PATCH", "patched": true}),
460        _ => json!({"message": "Mock response", "method": &request.method}),
461    };
462
463    MockForgeResponse {
464        status: 200,
465        headers,
466        body,
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_parse_postman_collection() {
476        let collection_json = r#"{
477            "info": {
478                "_postman_id": "test-id",
479                "name": "Test Collection",
480                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
481            },
482            "item": [
483                {
484                    "name": "Get Users",
485                    "request": {
486                        "method": "GET",
487                        "header": [{"key": "Authorization", "value": "Bearer {{token}}"}],
488                        "url": {"raw": "{{baseUrl}}/users"}
489                    }
490                }
491            ],
492            "variable": [
493                {"key": "baseUrl", "value": "https://api.example.com"},
494                {"key": "token", "value": "test-token"}
495            ]
496        }"#;
497
498        let result =
499            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
500
501        assert_eq!(result.routes.len(), 1);
502        assert_eq!(result.routes[0].method, "GET");
503        assert_eq!(result.routes[0].path, "/users");
504        assert!(result.routes[0].headers.contains_key("Authorization"));
505    }
506
507    #[test]
508    fn test_parse_postman_collection_with_multiple_requests() {
509        let collection_json = r#"{
510            "info": {
511                "name": "Test Collection",
512                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
513            },
514            "item": [
515                {
516                    "name": "Get Users",
517                    "request": {
518                        "method": "GET",
519                        "header": [],
520                        "url": "https://api.example.com/users"
521                    }
522                },
523                {
524                    "name": "Create User",
525                    "request": {
526                        "method": "POST",
527                        "header": [{"key": "Content-Type", "value": "application/json"}],
528                        "url": "https://api.example.com/users",
529                        "body": {
530                            "mode": "raw",
531                            "raw": "{\"name\": \"John\", \"age\": 30}"
532                        }
533                    }
534                },
535                {
536                    "name": "Update User",
537                    "request": {
538                        "method": "PUT",
539                        "header": [{"key": "Content-Type", "value": "application/json"}],
540                        "url": "https://api.example.com/users/123",
541                        "body": {
542                            "mode": "raw",
543                            "raw": "{\"name\": \"Jane\"}"
544                        }
545                    }
546                }
547            ]
548        }"#;
549
550        let result =
551            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
552
553        assert_eq!(result.routes.len(), 3);
554
555        // Check first route (GET)
556        assert_eq!(result.routes[0].method, "GET");
557        assert_eq!(result.routes[0].path, "/users");
558
559        // Check second route (POST)
560        assert_eq!(result.routes[1].method, "POST");
561        assert_eq!(result.routes[1].path, "/users");
562        assert_eq!(result.routes[1].body, Some("{\"name\": \"John\", \"age\": 30}".to_string()));
563        assert_eq!(
564            result.routes[1].headers.get("Content-Type"),
565            Some(&"application/json".to_string())
566        );
567
568        // Check third route (PUT)
569        assert_eq!(result.routes[2].method, "PUT");
570        assert_eq!(result.routes[2].path, "/users/123");
571        assert_eq!(result.routes[2].body, Some("{\"name\": \"Jane\"}".to_string()));
572    }
573
574    #[test]
575    fn test_parse_postman_collection_with_folders() {
576        let collection_json = r#"{
577            "info": {
578                "name": "Test Collection",
579                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
580            },
581            "item": [
582                {
583                    "name": "User Operations",
584                    "item": [
585                        {
586                            "name": "Get Users",
587                            "request": {
588                                "method": "GET",
589                                "header": [],
590                                "url": "https://api.example.com/users"
591                            }
592                        },
593                        {
594                            "name": "Create User",
595                            "request": {
596                                "method": "POST",
597                                "header": [],
598                                "url": "https://api.example.com/users"
599                            }
600                        }
601                    ]
602                },
603                {
604                    "name": "Admin Operations",
605                    "item": [
606                        {
607                            "name": "Get Stats",
608                            "request": {
609                                "method": "GET",
610                                "header": [],
611                                "url": "https://api.example.com/admin/stats"
612                            }
613                        }
614                    ]
615                }
616            ]
617        }"#;
618
619        let result =
620            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
621
622        assert_eq!(result.routes.len(), 3);
623
624        // Check routes are created from nested items
625        assert_eq!(result.routes[0].method, "GET");
626        assert_eq!(result.routes[0].path, "/users");
627
628        assert_eq!(result.routes[1].method, "POST");
629        assert_eq!(result.routes[1].path, "/users");
630
631        assert_eq!(result.routes[2].method, "GET");
632        assert_eq!(result.routes[2].path, "/admin/stats");
633    }
634
635    #[test]
636    fn test_parse_postman_collection_with_query_parameters() {
637        let collection_json = r#"{
638            "info": {
639                "name": "Test Collection",
640                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
641            },
642            "item": [
643                {
644                    "name": "Search Users",
645                    "request": {
646                        "method": "GET",
647                        "header": [],
648                        "url": {
649                            "raw": "https://api.example.com/search?q=test&page=1&limit=10",
650                            "host": ["api", "example", "com"],
651                            "path": ["search"],
652                            "query": [
653                                {"key": "q", "value": "test"},
654                                {"key": "page", "value": "1"},
655                                {"key": "limit", "value": "10"}
656                            ]
657                        }
658                    }
659                }
660            ]
661        }"#;
662
663        let result =
664            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
665
666        assert_eq!(result.routes.len(), 1);
667        assert_eq!(result.routes[0].method, "GET");
668        assert_eq!(result.routes[0].path, "/search?q=test&page=1&limit=10");
669    }
670
671    #[test]
672    fn test_parse_postman_collection_with_different_methods() {
673        let collection_json = r#"{
674            "info": {
675                "name": "Test Collection",
676                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
677            },
678            "item": [
679                {"name": "GET Request", "request": {"method": "GET", "url": "https://api.example.com/get"}},
680                {"name": "POST Request", "request": {"method": "POST", "url": "https://api.example.com/post"}},
681                {"name": "PUT Request", "request": {"method": "PUT", "url": "https://api.example.com/put"}},
682                {"name": "DELETE Request", "request": {"method": "DELETE", "url": "https://api.example.com/delete"}},
683                {"name": "PATCH Request", "request": {"method": "PATCH", "url": "https://api.example.com/patch"}},
684                {"name": "HEAD Request", "request": {"method": "HEAD", "url": "https://api.example.com/head"}},
685                {"name": "OPTIONS Request", "request": {"method": "OPTIONS", "url": "https://api.example.com/options"}}
686            ]
687        }"#;
688
689        let result =
690            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
691
692        assert_eq!(result.routes.len(), 7);
693
694        let expected_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
695        for (i, expected_method) in expected_methods.iter().enumerate() {
696            assert_eq!(result.routes[i].method, *expected_method);
697            assert_eq!(result.routes[i].path, format!("/{}", expected_method.to_lowercase()));
698        }
699    }
700
701    #[test]
702    fn test_parse_postman_collection_with_form_data() {
703        let collection_json = r#"{
704            "info": {
705                "name": "Test Collection",
706                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
707            },
708            "item": [
709                {
710                    "name": "Form Submit",
711                    "request": {
712                        "method": "POST",
713                        "header": [{"key": "Content-Type", "value": "application/x-www-form-urlencoded"}],
714                        "url": "https://api.example.com/form",
715                        "body": {
716                            "mode": "urlencoded",
717                            "urlencoded": [
718                                {"key": "username", "value": "john_doe"},
719                                {"key": "password", "value": "secret123"},
720                                {"key": "remember", "value": "true"}
721                            ]
722                        }
723                    }
724                }
725            ]
726        }"#;
727
728        let result =
729            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
730
731        assert_eq!(result.routes.len(), 1);
732        assert_eq!(result.routes[0].method, "POST");
733        assert_eq!(result.routes[0].path, "/form");
734        assert_eq!(
735            result.routes[0].body,
736            Some("username=john_doe&password=secret123&remember=true".to_string())
737        );
738    }
739
740    #[test]
741    fn test_parse_postman_collection_with_raw_body() {
742        let collection_json = r#"{
743            "info": {
744                "name": "Test Collection",
745                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
746            },
747            "item": [
748                {
749                    "name": "JSON Post",
750                    "request": {
751                        "method": "POST",
752                        "header": [{"key": "Content-Type", "value": "application/json"}],
753                        "url": "https://api.example.com/json",
754                        "body": {
755                            "mode": "raw",
756                            "raw": "{\"message\": \"Hello World\", \"data\": {\"key\": \"value\"}}"
757                        }
758                    }
759                },
760                {
761                    "name": "XML Post",
762                    "request": {
763                        "method": "POST",
764                        "header": [{"key": "Content-Type", "value": "application/xml"}],
765                        "url": "https://api.example.com/xml",
766                        "body": {
767                            "mode": "raw",
768                            "raw": "<root><message>Hello</message><data><key>value</key></data></root>"
769                        }
770                    }
771                }
772            ]
773        }"#;
774
775        let result =
776            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
777
778        assert_eq!(result.routes.len(), 2);
779
780        // Check JSON request
781        assert_eq!(result.routes[0].method, "POST");
782        assert_eq!(result.routes[0].path, "/json");
783        assert_eq!(
784            result.routes[0].body,
785            Some("{\"message\": \"Hello World\", \"data\": {\"key\": \"value\"}}".to_string())
786        );
787
788        // Check XML request
789        assert_eq!(result.routes[1].method, "POST");
790        assert_eq!(result.routes[1].path, "/xml");
791        assert_eq!(
792            result.routes[1].body,
793            Some("<root><message>Hello</message><data><key>value</key></data></root>".to_string())
794        );
795    }
796
797    #[test]
798    fn test_parse_postman_collection_with_auth() {
799        let collection_json = r#"{
800            "info": {
801                "name": "Test Collection",
802                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
803            },
804            "item": [
805                {
806                    "name": "Protected Request",
807                    "request": {
808                        "method": "GET",
809                        "header": [
810                            {"key": "Authorization", "value": "Bearer {{token}}"},
811                            {"key": "X-API-Key", "value": "api-key-123"}
812                        ],
813                        "url": "https://api.example.com/protected",
814                        "auth": {
815                            "type": "bearer",
816                            "bearer": [
817                                {"key": "token", "value": "{{token}}", "type": "string"}
818                            ]
819                        }
820                    }
821                }
822            ],
823            "variable": [
824                {"key": "token", "value": "test-token-abc"}
825            ]
826        }"#;
827
828        let result =
829            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
830
831        assert_eq!(result.routes.len(), 1);
832        assert_eq!(result.routes[0].method, "GET");
833        assert_eq!(result.routes[0].path, "/protected");
834        assert_eq!(
835            result.routes[0].headers.get("Authorization"),
836            Some(&"Bearer test-token-abc".to_string())
837        );
838        assert_eq!(result.routes[0].headers.get("X-API-Key"), Some(&"api-key-123".to_string()));
839    }
840
841    #[test]
842    fn test_parse_postman_collection_with_variables() {
843        let collection_json = r#"{
844            "info": {
845                "name": "Test Collection",
846                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
847            },
848            "item": [
849                {
850                    "name": "Variable Test",
851                    "request": {
852                        "method": "GET",
853                        "header": [
854                            {"key": "X-User-ID", "value": "{{userId}}"},
855                            {"key": "X-Environment", "value": "{{environment}}"}
856                        ],
857                        "url": "{{baseUrl}}/test/{{userId}}?env={{environment}}"
858                    }
859                }
860            ],
861            "variable": [
862                {"key": "baseUrl", "value": "https://api.example.com"},
863                {"key": "userId", "value": "12345"},
864                {"key": "environment", "value": "production"}
865            ]
866        }"#;
867
868        let result =
869            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
870
871        assert_eq!(result.routes.len(), 1);
872        assert_eq!(result.routes[0].method, "GET");
873        assert_eq!(result.routes[0].path, "/test/12345?env=production");
874        assert_eq!(result.routes[0].headers.get("X-User-ID"), Some(&"12345".to_string()));
875        assert_eq!(result.routes[0].headers.get("X-Environment"), Some(&"production".to_string()));
876    }
877
878    #[test]
879    fn test_parse_postman_collection_with_disabled_items() {
880        let collection_json = r#"{
881            "info": {
882                "name": "Test Collection",
883                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
884            },
885            "item": [
886                {
887                    "name": "Enabled Request",
888                    "request": {
889                        "method": "GET",
890                        "url": "https://api.example.com/enabled"
891                    }
892                },
893                {
894                    "name": "Disabled Request",
895                    "request": {
896                        "method": "GET",
897                        "url": "https://api.example.com/disabled"
898                    }
899                }
900            ]
901        }"#;
902
903        let result =
904            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
905
906        // All requests should be imported regardless of disabled status in Postman
907        // (disabled in Postman means not executed during collection run, not that it's invalid)
908        assert_eq!(result.routes.len(), 2);
909    }
910
911    #[test]
912    fn test_parse_postman_collection_with_complex_headers() {
913        let collection_json = r#"{
914            "info": {
915                "name": "Test Collection",
916                "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
917            },
918            "item": [
919                {
920                    "name": "Complex Headers",
921                    "request": {
922                        "method": "GET",
923                        "header": [
924                            {"key": "Authorization", "value": "Bearer token123"},
925                            {"key": "Content-Type", "value": "application/json"},
926                            {"key": "Accept", "value": "application/json"},
927                            {"key": "X-Custom-Header", "value": "custom-value"},
928                            {"key": "X-Request-ID", "value": "req-123"},
929                            {"key": "User-Agent", "value": "PostmanRuntime/7.29.0"},
930                            {"key": "Cache-Control", "value": "no-cache"}
931                        ],
932                        "url": "https://api.example.com/complex"
933                    }
934                }
935            ]
936        }"#;
937
938        let result =
939            import_postman_collection(collection_json, Some("https://api.example.com")).unwrap();
940
941        assert_eq!(result.routes.len(), 1);
942        assert_eq!(result.routes[0].method, "GET");
943        assert_eq!(result.routes[0].path, "/complex");
944
945        // Check that all headers are preserved
946        let headers = &result.routes[0].headers;
947        assert_eq!(headers.get("Authorization"), Some(&"Bearer token123".to_string()));
948        assert_eq!(headers.get("Content-Type"), Some(&"application/json".to_string()));
949        assert_eq!(headers.get("Accept"), Some(&"application/json".to_string()));
950        assert_eq!(headers.get("X-Custom-Header"), Some(&"custom-value".to_string()));
951        assert_eq!(headers.get("X-Request-ID"), Some(&"req-123".to_string()));
952        assert_eq!(headers.get("User-Agent"), Some(&"PostmanRuntime/7.29.0".to_string()));
953        assert_eq!(headers.get("Cache-Control"), Some(&"no-cache".to_string()));
954    }
955}