Skip to main content

mq_rest_admin/
session.rs

1//! MQ REST API session and command execution.
2
3use std::collections::HashMap;
4
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD as BASE64;
7use serde_json::Value;
8
9use crate::auth::{Credentials, perform_ltpa_login};
10use crate::error::{MappingError, MappingIssue, MqRestError, Result};
11use crate::mapping::{map_request_attributes, map_response_list};
12use crate::mapping_data::MAPPING_DATA;
13use crate::mapping_merge::{
14    MappingOverrideMode, merge_mapping_data, replace_mapping_data, validate_mapping_overrides,
15    validate_mapping_overrides_complete,
16};
17use crate::transport::{MqRestTransport, ReqwestTransport};
18
19/// Default response parameters for DISPLAY commands.
20pub const DEFAULT_RESPONSE_PARAMETERS: &[&str] = &["all"];
21
22/// Default CSRF token value.
23pub const DEFAULT_CSRF_TOKEN: &str = "local";
24
25const GATEWAY_HEADER: &str = "ibm-mq-rest-gateway-qmgr";
26const ERROR_INVALID_JSON: &str = "Response body was not valid JSON.";
27const ERROR_NON_OBJECT_RESPONSE: &str = "Response payload was not a JSON object.";
28const ERROR_COMMAND_RESPONSE_NOT_LIST: &str = "Response commandResponse was not a list.";
29const ERROR_COMMAND_RESPONSE_ITEM_NOT_OBJECT: &str =
30    "Response commandResponse item was not an object.";
31
32/// Default MQSC qualifier fallback mappings.
33fn default_mapping_qualifiers() -> HashMap<&'static str, &'static str> {
34    let mut m = HashMap::new();
35    m.insert("QUEUE", "queue");
36    m.insert("QLOCAL", "queue");
37    m.insert("QREMOTE", "queue");
38    m.insert("QALIAS", "queue");
39    m.insert("QMODEL", "queue");
40    m.insert("QMSTATUS", "qmstatus");
41    m.insert("QSTATUS", "qstatus");
42    m.insert("CHANNEL", "channel");
43    m.insert("QMGR", "qmgr");
44    m
45}
46
47/// Builder for constructing an [`MqRestSession`].
48pub struct MqRestSessionBuilder {
49    rest_base_url: String,
50    qmgr_name: String,
51    credentials: Credentials,
52    gateway_qmgr: Option<String>,
53    verify_tls: bool,
54    timeout_seconds: Option<f64>,
55    map_attributes: bool,
56    mapping_strict: bool,
57    mapping_overrides: Option<Value>,
58    mapping_overrides_mode: MappingOverrideMode,
59    csrf_token: Option<String>,
60    transport: Option<Box<dyn MqRestTransport>>,
61}
62
63impl MqRestSessionBuilder {
64    /// Create a new builder with required parameters.
65    #[must_use]
66    pub fn new(
67        rest_base_url: impl Into<String>,
68        qmgr_name: impl Into<String>,
69        credentials: Credentials,
70    ) -> Self {
71        Self {
72            rest_base_url: rest_base_url.into(),
73            qmgr_name: qmgr_name.into(),
74            credentials,
75            gateway_qmgr: None,
76            verify_tls: true,
77            timeout_seconds: Some(30.0),
78            map_attributes: true,
79            mapping_strict: true,
80            mapping_overrides: None,
81            mapping_overrides_mode: MappingOverrideMode::Merge,
82            csrf_token: Some(DEFAULT_CSRF_TOKEN.into()),
83            transport: None,
84        }
85    }
86
87    /// Set the gateway queue manager name.
88    #[must_use]
89    pub fn gateway_qmgr(mut self, name: impl Into<String>) -> Self {
90        self.gateway_qmgr = Some(name.into());
91        self
92    }
93
94    /// Set whether to verify TLS certificates.
95    #[must_use]
96    pub const fn verify_tls(mut self, verify: bool) -> Self {
97        self.verify_tls = verify;
98        self
99    }
100
101    /// Set the HTTP request timeout in seconds.
102    #[must_use]
103    pub const fn timeout_seconds(mut self, timeout: Option<f64>) -> Self {
104        self.timeout_seconds = timeout;
105        self
106    }
107
108    /// Set whether to map attributes between `snake_case` and MQSC names.
109    #[must_use]
110    pub const fn map_attributes(mut self, enabled: bool) -> Self {
111        self.map_attributes = enabled;
112        self
113    }
114
115    /// Set whether mapping failures are strict errors.
116    #[must_use]
117    pub const fn mapping_strict(mut self, strict: bool) -> Self {
118        self.mapping_strict = strict;
119        self
120    }
121
122    /// Set mapping overrides.
123    #[must_use]
124    pub fn mapping_overrides(mut self, overrides: Value) -> Self {
125        self.mapping_overrides = Some(overrides);
126        self
127    }
128
129    /// Set the mapping overrides mode.
130    #[must_use]
131    pub const fn mapping_overrides_mode(mut self, mode: MappingOverrideMode) -> Self {
132        self.mapping_overrides_mode = mode;
133        self
134    }
135
136    /// Set the CSRF token value.
137    #[must_use]
138    pub fn csrf_token(mut self, token: Option<String>) -> Self {
139        self.csrf_token = token;
140        self
141    }
142
143    /// Set a custom transport implementation.
144    #[must_use]
145    pub fn transport(mut self, transport: Box<dyn MqRestTransport>) -> Self {
146        self.transport = Some(transport);
147        self
148    }
149
150    /// Build the session, performing LTPA login if needed.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if mapping override validation fails, certificate files
155    /// cannot be read, or LTPA login fails.
156    pub fn build(self) -> Result<MqRestSession> {
157        let rest_base_url = self.rest_base_url.trim_end_matches('/').to_owned();
158
159        let mapping_data = if let Some(ref overrides) = self.mapping_overrides {
160            validate_mapping_overrides(overrides).map_err(|msg| MqRestError::Response {
161                message: msg,
162                response_text: None,
163            })?;
164            if self.mapping_overrides_mode == MappingOverrideMode::Replace {
165                validate_mapping_overrides_complete(&MAPPING_DATA, overrides).map_err(|msg| {
166                    MqRestError::Response {
167                        message: msg,
168                        response_text: None,
169                    }
170                })?;
171                replace_mapping_data(overrides)
172            } else {
173                merge_mapping_data(&MAPPING_DATA, overrides)
174            }
175        } else {
176            MAPPING_DATA.clone()
177        };
178
179        let transport: Box<dyn MqRestTransport> = if let Some(t) = self.transport {
180            t
181        } else if let Credentials::Certificate {
182            ref cert_path,
183            ref key_path,
184        } = self.credentials
185        {
186            let cert_pem = std::fs::read(cert_path).map_err(|_| MqRestError::Response {
187                message: format!("Failed to read certificate file: {cert_path}"),
188                response_text: None,
189            })?;
190            let key_pem = key_path
191                .as_ref()
192                .map(|p| {
193                    std::fs::read(p).map_err(|_| MqRestError::Response {
194                        message: format!("Failed to read key file: {p}"),
195                        response_text: None,
196                    })
197                })
198                .transpose()?;
199            Box::new(ReqwestTransport::new_with_cert(
200                &cert_pem,
201                key_pem.as_deref(),
202            )?)
203        } else if self.verify_tls {
204            Box::new(ReqwestTransport::new())
205        } else {
206            Box::new(ReqwestTransport::new_insecure())
207        };
208
209        let (ltpa_cookie_name, ltpa_token) = if let Credentials::Ltpa {
210            ref username,
211            ref password,
212        } = self.credentials
213        {
214            let (name, token) = perform_ltpa_login(
215                transport.as_ref(),
216                &rest_base_url,
217                username,
218                password,
219                self.csrf_token.as_deref(),
220                self.timeout_seconds,
221                self.verify_tls,
222            )?;
223            (Some(name), Some(token))
224        } else {
225            (None, None)
226        };
227
228        Ok(MqRestSession {
229            rest_base_url,
230            qmgr_name: self.qmgr_name,
231            gateway_qmgr: self.gateway_qmgr,
232            verify_tls: self.verify_tls,
233            timeout_seconds: self.timeout_seconds,
234            map_attributes: self.map_attributes,
235            mapping_strict: self.mapping_strict,
236            csrf_token: self.csrf_token,
237            credentials: self.credentials,
238            mapping_data,
239            transport,
240            ltpa_cookie_name,
241            ltpa_token,
242            last_response_payload: None,
243            last_response_text: None,
244            last_http_status: None,
245            last_command_payload: None,
246        })
247    }
248}
249
250/// Session wrapper for MQ REST admin calls.
251pub struct MqRestSession {
252    rest_base_url: String,
253    qmgr_name: String,
254    gateway_qmgr: Option<String>,
255    verify_tls: bool,
256    timeout_seconds: Option<f64>,
257    map_attributes: bool,
258    mapping_strict: bool,
259    csrf_token: Option<String>,
260    credentials: Credentials,
261    mapping_data: Value,
262    transport: Box<dyn MqRestTransport>,
263    ltpa_cookie_name: Option<String>,
264    ltpa_token: Option<String>,
265
266    /// The parsed JSON payload from the most recent command.
267    pub last_response_payload: Option<HashMap<String, Value>>,
268    /// The raw HTTP response body from the most recent command.
269    pub last_response_text: Option<String>,
270    /// The HTTP status code from the most recent command.
271    pub last_http_status: Option<u16>,
272    /// The `runCommandJSON` request payload sent for the most recent command.
273    pub last_command_payload: Option<HashMap<String, Value>>,
274}
275
276impl MqRestSession {
277    /// Create a builder for constructing a session.
278    #[must_use]
279    pub fn builder(
280        rest_base_url: impl Into<String>,
281        qmgr_name: impl Into<String>,
282        credentials: Credentials,
283    ) -> MqRestSessionBuilder {
284        MqRestSessionBuilder::new(rest_base_url, qmgr_name, credentials)
285    }
286
287    /// The queue manager name this session targets.
288    #[must_use]
289    pub fn qmgr_name(&self) -> &str {
290        &self.qmgr_name
291    }
292
293    /// The gateway queue manager name, or `None` for direct access.
294    #[must_use]
295    pub fn gateway_qmgr(&self) -> Option<&str> {
296        self.gateway_qmgr.as_deref()
297    }
298
299    /// Core MQSC command dispatch method.
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if attribute mapping, HTTP transport, response parsing,
304    /// or the MQ command itself fails.
305    pub(crate) fn mqsc_command(
306        &mut self,
307        command: &str,
308        mqsc_qualifier: &str,
309        name: Option<&str>,
310        request_parameters: Option<&HashMap<String, Value>>,
311        response_parameters: Option<&[&str]>,
312        where_clause: Option<&str>,
313    ) -> Result<Vec<HashMap<String, Value>>> {
314        let command_upper = command.trim().to_uppercase();
315        let qualifier_upper = mqsc_qualifier.trim().to_uppercase();
316        let mut normalized_request_parameters: HashMap<String, Value> =
317            request_parameters.cloned().unwrap_or_default();
318        let mut normalized_response_parameters =
319            normalize_response_parameters(response_parameters, command_upper == "DISPLAY");
320        let do_map = self.map_attributes;
321        let mapping_qualifier = self.resolve_mapping_qualifier(&command_upper, &qualifier_upper);
322
323        if do_map {
324            normalized_request_parameters = map_request_attributes(
325                &mapping_qualifier,
326                &normalized_request_parameters,
327                self.mapping_strict,
328                Some(&self.mapping_data),
329            )?;
330            normalized_response_parameters = self.map_response_parameters(
331                &command_upper,
332                &qualifier_upper,
333                &mapping_qualifier,
334                &normalized_response_parameters,
335            )?;
336        }
337
338        if let Some(where_str) = where_clause {
339            let trimmed = where_str.trim();
340            if !trimmed.is_empty() {
341                let mapped_where = if do_map {
342                    map_where_keyword(
343                        trimmed,
344                        &mapping_qualifier,
345                        self.mapping_strict,
346                        &self.mapping_data,
347                    )?
348                } else {
349                    trimmed.to_owned()
350                };
351                normalized_request_parameters.insert("WHERE".into(), Value::String(mapped_where));
352            }
353        }
354
355        let payload = build_command_payload(
356            &command_upper,
357            &qualifier_upper,
358            name,
359            &normalized_request_parameters,
360            &normalized_response_parameters,
361        );
362        self.last_command_payload = Some(payload.clone());
363
364        let headers = self.build_headers();
365        let url = self.build_mqsc_url();
366        let transport_response = self.transport.post_json(
367            &url,
368            &payload,
369            &headers,
370            self.timeout_seconds,
371            self.verify_tls,
372        )?;
373
374        self.last_http_status = Some(transport_response.status_code);
375        self.last_response_text = Some(transport_response.text.clone());
376
377        let response_payload = parse_response_payload(&transport_response.text)?;
378        self.last_response_payload = Some(response_payload.clone());
379
380        raise_for_command_errors(&response_payload, transport_response.status_code)?;
381
382        let command_response = extract_command_response(&response_payload)?;
383        let mut parameter_objects: Vec<HashMap<String, Value>> = Vec::new();
384        for item in &command_response {
385            if let Some(parameters) = item.get("parameters").and_then(Value::as_object) {
386                let map: HashMap<String, Value> = parameters
387                    .iter()
388                    .map(|(k, v)| (k.clone(), v.clone()))
389                    .collect();
390                parameter_objects.push(map);
391            } else {
392                parameter_objects.push(HashMap::new());
393            }
394        }
395
396        parameter_objects = flatten_nested_objects(parameter_objects);
397
398        if do_map {
399            let normalized: Vec<HashMap<String, Value>> = parameter_objects
400                .iter()
401                .map(normalize_response_attributes)
402                .collect();
403            Ok(map_response_list(
404                &mapping_qualifier,
405                &normalized,
406                self.mapping_strict,
407                Some(&self.mapping_data),
408            )?)
409        } else {
410            Ok(parameter_objects)
411        }
412    }
413
414    fn build_mqsc_url(&self) -> String {
415        format!(
416            "{}/admin/action/qmgr/{}/mqsc",
417            self.rest_base_url, self.qmgr_name
418        )
419    }
420
421    fn build_headers(&self) -> HashMap<String, String> {
422        let mut headers = HashMap::new();
423        headers.insert("Accept".into(), "application/json".into());
424        match &self.credentials {
425            Credentials::Basic { username, password } => {
426                headers.insert(
427                    "Authorization".into(),
428                    build_basic_auth_header(username, password),
429                );
430            }
431            Credentials::Ltpa { .. } => {
432                if let (Some(name), Some(token)) = (&self.ltpa_cookie_name, &self.ltpa_token) {
433                    headers.insert("Cookie".into(), format!("{name}={token}"));
434                }
435            }
436            Credentials::Certificate { .. } => {}
437        }
438        if let Some(ref token) = self.csrf_token {
439            headers.insert("ibm-mq-rest-csrf-token".into(), token.clone());
440        }
441        if let Some(ref gw) = self.gateway_qmgr {
442            headers.insert(GATEWAY_HEADER.into(), gw.clone());
443        }
444        headers
445    }
446
447    fn map_response_parameters(
448        &self,
449        command: &str,
450        mqsc_qualifier: &str,
451        mapping_qualifier: &str,
452        response_parameters: &[String],
453    ) -> std::result::Result<Vec<String>, MappingError> {
454        if is_all_response_parameters(response_parameters) {
455            return Ok(response_parameters.to_vec());
456        }
457        let macros = get_response_parameter_macros(command, mqsc_qualifier, &self.mapping_data);
458        let macro_lookup: HashMap<String, String> = macros
459            .iter()
460            .map(|m| (m.to_lowercase(), m.clone()))
461            .collect();
462        let qualifier_entry = get_qualifier_entry(mapping_qualifier, &self.mapping_data);
463        let Some(entry) = qualifier_entry else {
464            if self.mapping_strict {
465                return Err(MappingError::new(build_unknown_qualifier_issue(
466                    mapping_qualifier,
467                )));
468            }
469            return Ok(response_parameters.to_vec());
470        };
471        let combined_map = build_snake_to_mqsc_map(entry);
472        let (mapped, issues) = map_response_parameter_names(
473            response_parameters,
474            &macro_lookup,
475            &combined_map,
476            mapping_qualifier,
477        );
478        if self.mapping_strict && !issues.is_empty() {
479            return Err(MappingError::new(issues));
480        }
481        Ok(mapped)
482    }
483
484    fn resolve_mapping_qualifier(&self, command: &str, mqsc_qualifier: &str) -> String {
485        let command_map = get_command_map(&self.mapping_data);
486        let command_key = format!("{command} {mqsc_qualifier}");
487        if let Some(def) = command_map.get(&command_key).and_then(Value::as_object)
488            && let Some(qualifier) = def.get("qualifier").and_then(Value::as_str)
489        {
490            return qualifier.to_owned();
491        }
492        let defaults = default_mapping_qualifiers();
493        if let Some(fallback) = defaults.get(mqsc_qualifier) {
494            return (*fallback).to_owned();
495        }
496        mqsc_qualifier.to_lowercase()
497    }
498}
499
500fn build_basic_auth_header(username: &str, password: &str) -> String {
501    let token = BASE64.encode(format!("{username}:{password}"));
502    format!("Basic {token}")
503}
504
505fn build_command_payload(
506    command: &str,
507    qualifier: &str,
508    name: Option<&str>,
509    request_parameters: &HashMap<String, Value>,
510    response_parameters: &[String],
511) -> HashMap<String, Value> {
512    let mut payload = HashMap::new();
513    payload.insert("type".into(), Value::String("runCommandJSON".into()));
514    payload.insert("command".into(), Value::String(command.into()));
515    payload.insert("qualifier".into(), Value::String(qualifier.into()));
516    if let Some(n) = name
517        && !n.is_empty()
518    {
519        payload.insert("name".into(), Value::String(n.into()));
520    }
521    if !request_parameters.is_empty() {
522        payload.insert(
523            "parameters".into(),
524            serde_json::to_value(request_parameters).unwrap(),
525        );
526    }
527    if !response_parameters.is_empty() {
528        let params: Vec<Value> = response_parameters
529            .iter()
530            .map(|s| Value::String(s.clone()))
531            .collect();
532        payload.insert("responseParameters".into(), Value::Array(params));
533    }
534    payload
535}
536
537fn normalize_response_parameters(
538    response_parameters: Option<&[&str]>,
539    is_display: bool,
540) -> Vec<String> {
541    let Some(params) = response_parameters else {
542        return if is_display {
543            DEFAULT_RESPONSE_PARAMETERS
544                .iter()
545                .map(|s| (*s).to_owned())
546                .collect()
547        } else {
548            Vec::new()
549        };
550    };
551    let normalized: Vec<String> = params.iter().map(|s| (*s).to_owned()).collect();
552    if is_all_response_parameters(&normalized) {
553        DEFAULT_RESPONSE_PARAMETERS
554            .iter()
555            .map(|s| (*s).to_owned())
556            .collect()
557    } else {
558        normalized
559    }
560}
561
562fn is_all_response_parameters(params: &[String]) -> bool {
563    params.iter().any(|p| p.eq_ignore_ascii_case("all"))
564}
565
566fn flatten_nested_objects(
567    parameter_objects: Vec<HashMap<String, Value>>,
568) -> Vec<HashMap<String, Value>> {
569    let mut flattened = Vec::new();
570    for item in parameter_objects {
571        if let Some(Value::Array(objects)) = item.get("objects") {
572            let shared: HashMap<String, Value> = item
573                .iter()
574                .filter(|(k, _)| k.as_str() != "objects")
575                .map(|(k, v)| (k.clone(), v.clone()))
576                .collect();
577            for nested in objects {
578                if let Some(obj) = nested.as_object() {
579                    let mut merged = shared.clone();
580                    for (key, value) in obj {
581                        merged.insert(key.clone(), value.clone());
582                    }
583                    flattened.push(merged);
584                }
585            }
586        } else {
587            flattened.push(item);
588        }
589    }
590    flattened
591}
592
593fn normalize_response_attributes(attributes: &HashMap<String, Value>) -> HashMap<String, Value> {
594    attributes
595        .iter()
596        .map(|(k, v)| (k.to_uppercase(), v.clone()))
597        .collect()
598}
599
600fn parse_response_payload(response_text: &str) -> Result<HashMap<String, Value>> {
601    let decoded: Value =
602        serde_json::from_str(response_text).map_err(|_| MqRestError::Response {
603            message: ERROR_INVALID_JSON.into(),
604            response_text: Some(response_text.to_owned()),
605        })?;
606    match decoded {
607        Value::Object(map) => Ok(map.into_iter().collect()),
608        _ => Err(MqRestError::Response {
609            message: ERROR_NON_OBJECT_RESPONSE.into(),
610            response_text: Some(response_text.to_owned()),
611        }),
612    }
613}
614
615fn extract_command_response(
616    payload: &HashMap<String, Value>,
617) -> Result<Vec<HashMap<String, Value>>> {
618    let Some(cr) = payload.get("commandResponse") else {
619        return Ok(Vec::new());
620    };
621    let arr = cr.as_array().ok_or_else(|| MqRestError::Response {
622        message: ERROR_COMMAND_RESPONSE_NOT_LIST.into(),
623        response_text: None,
624    })?;
625    let mut items = Vec::new();
626    for item in arr {
627        let obj = item.as_object().ok_or_else(|| MqRestError::Response {
628            message: ERROR_COMMAND_RESPONSE_ITEM_NOT_OBJECT.into(),
629            response_text: None,
630        })?;
631        items.push(obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
632    }
633    Ok(items)
634}
635
636fn raise_for_command_errors(payload: &HashMap<String, Value>, status_code: u16) -> Result<()> {
637    let completion_code = extract_optional_i64(payload.get("overallCompletionCode"));
638    let reason_code = extract_optional_i64(payload.get("overallReasonCode"));
639    let has_overall_error = has_error_codes(completion_code, reason_code);
640
641    let mut command_issues: Vec<String> = Vec::new();
642    if let Some(Value::Array(cr)) = payload.get("commandResponse") {
643        for (idx, item) in cr.iter().enumerate() {
644            if let Some(obj) = item.as_object() {
645                let completion_code = extract_optional_i64(obj.get("completionCode"));
646                let reason_code = extract_optional_i64(obj.get("reasonCode"));
647                if has_error_codes(completion_code, reason_code) {
648                    command_issues.push(format!(
649                        "index={idx} completionCode={} reasonCode={}",
650                        completion_code.unwrap_or(0),
651                        reason_code.unwrap_or(0),
652                    ));
653                }
654            }
655        }
656    }
657
658    if has_overall_error || !command_issues.is_empty() {
659        let mut lines = vec!["MQ REST command failed.".to_owned()];
660        if has_overall_error {
661            lines.push(format!(
662                "overallCompletionCode={} overallReasonCode={}",
663                completion_code.unwrap_or(0),
664                reason_code.unwrap_or(0),
665            ));
666        }
667        if !command_issues.is_empty() {
668            lines.push("commandResponse:".into());
669            lines.extend(command_issues);
670        }
671        return Err(MqRestError::Command {
672            payload: payload.clone(),
673            status_code: Some(status_code),
674            message: lines.join("\n"),
675        });
676    }
677    Ok(())
678}
679
680fn extract_optional_i64(value: Option<&Value>) -> Option<i64> {
681    value.and_then(Value::as_i64)
682}
683
684const fn has_error_codes(completion_code: Option<i64>, reason_code: Option<i64>) -> bool {
685    matches!(completion_code, Some(c) if c != 0) || matches!(reason_code, Some(r) if r != 0)
686}
687
688fn get_command_map(mapping_data: &Value) -> HashMap<String, Value> {
689    mapping_data
690        .get("commands")
691        .and_then(Value::as_object)
692        .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
693        .unwrap_or_default()
694}
695
696fn get_response_parameter_macros(
697    command: &str,
698    mqsc_qualifier: &str,
699    mapping_data: &Value,
700) -> Vec<String> {
701    let command_key = format!("{command} {mqsc_qualifier}");
702    let entry = mapping_data
703        .get("commands")
704        .and_then(|c| c.get(&command_key));
705    let Some(entry) = entry.and_then(Value::as_object) else {
706        return Vec::new();
707    };
708    let Some(macros) = entry
709        .get("response_parameter_macros")
710        .and_then(Value::as_array)
711    else {
712        return Vec::new();
713    };
714    macros
715        .iter()
716        .filter_map(Value::as_str)
717        .map(str::to_owned)
718        .collect()
719}
720
721fn build_unknown_qualifier_issue(qualifier: &str) -> Vec<MappingIssue> {
722    vec![MappingIssue {
723        direction: "request".into(),
724        reason: "unknown_qualifier".into(),
725        attribute_name: "*".into(),
726        attribute_value: None,
727        object_index: None,
728        qualifier: Some(qualifier.into()),
729    }]
730}
731
732fn build_snake_to_mqsc_map(qualifier_entry: &Value) -> HashMap<String, String> {
733    let mut response_lookup: HashMap<String, String> = HashMap::new();
734    if let Some(response_key_map) = qualifier_entry
735        .get("response_key_map")
736        .and_then(Value::as_object)
737    {
738        for (mqsc_key, snake_val) in response_key_map {
739            if let Some(snake_key) = snake_val.as_str() {
740                response_lookup
741                    .entry(snake_key.to_owned())
742                    .or_insert_with(|| mqsc_key.clone());
743            }
744        }
745    }
746    let mut combined = response_lookup;
747    if let Some(request_key_map) = qualifier_entry
748        .get("request_key_map")
749        .and_then(Value::as_object)
750    {
751        for (snake_key, mqsc_val) in request_key_map {
752            if let Some(mqsc_key) = mqsc_val.as_str() {
753                combined.insert(snake_key.clone(), mqsc_key.to_owned());
754            }
755        }
756    }
757    combined
758}
759
760fn map_where_keyword(
761    where_str: &str,
762    mapping_qualifier: &str,
763    strict: bool,
764    mapping_data: &Value,
765) -> std::result::Result<String, MappingError> {
766    let parts: Vec<&str> = where_str.splitn(2, char::is_whitespace).collect();
767    let keyword = parts[0];
768    let rest = if parts.len() > 1 { parts[1] } else { "" };
769
770    let qualifier_entry = get_qualifier_entry(mapping_qualifier, mapping_data);
771    let Some(entry) = qualifier_entry else {
772        if strict {
773            return Err(MappingError::new(build_unknown_qualifier_issue(
774                mapping_qualifier,
775            )));
776        }
777        return Ok(where_str.to_owned());
778    };
779
780    let combined_map = build_snake_to_mqsc_map(entry);
781    let mapped_keyword = if let Some(mqsc_key) = combined_map.get(keyword) {
782        mqsc_key.clone()
783    } else {
784        if strict {
785            return Err(MappingError::new(vec![MappingIssue {
786                direction: "request".into(),
787                reason: "unknown_key".into(),
788                attribute_name: keyword.into(),
789                attribute_value: None,
790                object_index: None,
791                qualifier: Some(mapping_qualifier.into()),
792            }]));
793        }
794        keyword.to_owned()
795    };
796
797    if rest.is_empty() {
798        Ok(mapped_keyword)
799    } else {
800        Ok(format!("{mapped_keyword} {rest}"))
801    }
802}
803
804fn map_response_parameter_names(
805    response_parameters: &[String],
806    macro_lookup: &HashMap<String, String>,
807    combined_map: &HashMap<String, String>,
808    mapping_qualifier: &str,
809) -> (Vec<String>, Vec<MappingIssue>) {
810    let mut mapped = Vec::new();
811    let mut issues = Vec::new();
812    for name in response_parameters {
813        if let Some(macro_key) = macro_lookup.get(&name.to_lowercase()) {
814            mapped.push(macro_key.clone());
815            continue;
816        }
817        if let Some(mapped_key) = combined_map.get(name.as_str()) {
818            mapped.push(mapped_key.clone());
819        } else {
820            issues.push(MappingIssue {
821                direction: "request".into(),
822                reason: "unknown_key".into(),
823                attribute_name: name.clone(),
824                attribute_value: None,
825                object_index: None,
826                qualifier: Some(mapping_qualifier.into()),
827            });
828            mapped.push(name.clone());
829        }
830    }
831    (mapped, issues)
832}
833
834fn get_qualifier_entry<'a>(qualifier: &str, mapping_data: &'a Value) -> Option<&'a Value> {
835    mapping_data
836        .get("qualifiers")
837        .and_then(|q| q.get(qualifier))
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use crate::test_helpers::{
844        MockTransport, command_error_response, empty_success_response, error_response,
845        mock_session, mock_session_with_mapping, success_response,
846    };
847    use crate::transport::TransportResponse;
848    use serde_json::json;
849
850    // =====================================================================
851    // Builder tests
852    // =====================================================================
853
854    #[test]
855    fn builder_basic_auth() {
856        let transport = MockTransport::new(vec![]);
857        let session = MqRestSession::builder(
858            "https://host/ibmmq/rest/v2/",
859            "QM1",
860            Credentials::Basic {
861                username: "u".into(),
862                password: "p".into(),
863            },
864        )
865        .transport(Box::new(transport))
866        .build()
867        .unwrap();
868        assert_eq!(session.qmgr_name(), "QM1");
869        assert!(session.gateway_qmgr().is_none());
870    }
871
872    #[test]
873    fn builder_gateway_qmgr() {
874        let transport = MockTransport::new(vec![]);
875        let session = MqRestSession::builder(
876            "https://host/ibmmq/rest/v2",
877            "QM1",
878            Credentials::Basic {
879                username: "u".into(),
880                password: "p".into(),
881            },
882        )
883        .gateway_qmgr("GW1")
884        .transport(Box::new(transport))
885        .build()
886        .unwrap();
887        assert_eq!(session.gateway_qmgr(), Some("GW1"));
888    }
889
890    #[test]
891    fn builder_ltpa_credentials_performs_login() {
892        let mut headers = HashMap::new();
893        headers.insert("Set-Cookie".into(), "LtpaToken2=tok; Path=/".into());
894        let login_response = TransportResponse {
895            status_code: 200,
896            text: "{}".into(),
897            headers,
898        };
899        let transport = MockTransport::new(vec![login_response]);
900        let session = MqRestSession::builder(
901            "https://host/ibmmq/rest/v2",
902            "QM1",
903            Credentials::Ltpa {
904                username: "u".into(),
905                password: "p".into(),
906            },
907        )
908        .transport(Box::new(transport))
909        .build()
910        .unwrap();
911        assert_eq!(session.ltpa_cookie_name.as_deref(), Some("LtpaToken2"));
912        assert_eq!(session.ltpa_token.as_deref(), Some("tok"));
913    }
914
915    #[test]
916    fn builder_mapping_overrides_merge() {
917        let transport = MockTransport::new(vec![]);
918        let overrides = json!({"commands": {}, "qualifiers": {}});
919        let _session = MqRestSession::builder(
920            "https://host/ibmmq/rest/v2",
921            "QM1",
922            Credentials::Basic {
923                username: "u".into(),
924                password: "p".into(),
925            },
926        )
927        .mapping_overrides(overrides)
928        .mapping_overrides_mode(MappingOverrideMode::Merge)
929        .transport(Box::new(transport))
930        .build()
931        .unwrap();
932    }
933
934    #[test]
935    fn builder_invalid_overrides() {
936        let transport = MockTransport::new(vec![]);
937        let overrides = json!("not an object");
938        let result = MqRestSession::builder(
939            "https://host/ibmmq/rest/v2",
940            "QM1",
941            Credentials::Basic {
942                username: "u".into(),
943                password: "p".into(),
944            },
945        )
946        .mapping_overrides(overrides)
947        .transport(Box::new(transport))
948        .build();
949        assert!(result.is_err());
950    }
951
952    #[test]
953    fn builder_fluent_setters() {
954        let transport = MockTransport::new(vec![]);
955        let _session = MqRestSession::builder(
956            "https://host/ibmmq/rest/v2",
957            "QM1",
958            Credentials::Basic {
959                username: "u".into(),
960                password: "p".into(),
961            },
962        )
963        .verify_tls(false)
964        .timeout_seconds(Some(60.0))
965        .map_attributes(false)
966        .mapping_strict(false)
967        .csrf_token(None)
968        .transport(Box::new(transport))
969        .build()
970        .unwrap();
971    }
972
973    // =====================================================================
974    // mqsc_command tests
975    // =====================================================================
976
977    #[test]
978    fn mqsc_command_basic_display() {
979        let mut params = HashMap::new();
980        params.insert("DESCR".into(), json!("test"));
981        let transport = MockTransport::new(vec![success_response(vec![params])]);
982        let mut session = mock_session(transport);
983        let result = session
984            .mqsc_command("DISPLAY", "QUEUE", Some("Q1"), None, None, None)
985            .unwrap();
986        assert_eq!(result.len(), 1);
987        assert_eq!(result[0]["DESCR"], json!("test"));
988    }
989
990    #[test]
991    fn mqsc_command_with_mapping() {
992        let mut params = HashMap::new();
993        params.insert("DESCR".into(), json!("test"));
994        let transport = MockTransport::new(vec![success_response(vec![params])]);
995        let mut session = mock_session_with_mapping(transport);
996        let result = session
997            .mqsc_command("DISPLAY", "QUEUE", Some("Q1"), None, None, None)
998            .unwrap();
999        assert_eq!(result.len(), 1);
1000        assert!(result[0].contains_key("description"));
1001    }
1002
1003    #[test]
1004    fn mqsc_command_non_display_default_params() {
1005        let transport = MockTransport::new(vec![empty_success_response()]);
1006        let mut session = mock_session(transport);
1007        session
1008            .mqsc_command("ALTER", "QMGR", None, None, None, None)
1009            .unwrap();
1010        let payload = session.last_command_payload.unwrap();
1011        assert!(!payload.contains_key("responseParameters"));
1012    }
1013
1014    #[test]
1015    fn mqsc_command_display_default_params() {
1016        let transport = MockTransport::new(vec![empty_success_response()]);
1017        let mut session = mock_session(transport);
1018        session
1019            .mqsc_command("DISPLAY", "QMGR", None, None, None, None)
1020            .unwrap();
1021        let payload = session.last_command_payload.unwrap();
1022        assert!(payload.contains_key("responseParameters"));
1023    }
1024
1025    #[test]
1026    fn mqsc_command_where_clause() {
1027        let transport = MockTransport::new(vec![empty_success_response()]);
1028        let mut session = mock_session(transport);
1029        session
1030            .mqsc_command(
1031                "DISPLAY",
1032                "QUEUE",
1033                Some("*"),
1034                None,
1035                None,
1036                Some("DESCR LK test*"),
1037            )
1038            .unwrap();
1039        let payload = session.last_command_payload.unwrap();
1040        let params = payload["parameters"].as_object().unwrap();
1041        assert!(params.contains_key("WHERE"));
1042    }
1043
1044    #[test]
1045    fn mqsc_command_where_clause_with_mapping() {
1046        let transport = MockTransport::new(vec![empty_success_response()]);
1047        let mut session = mock_session_with_mapping(transport);
1048        session
1049            .mqsc_command(
1050                "DISPLAY",
1051                "QUEUE",
1052                Some("*"),
1053                None,
1054                None,
1055                Some("description LK test*"),
1056            )
1057            .unwrap();
1058        let payload = session.last_command_payload.unwrap();
1059        let params = payload["parameters"].as_object().unwrap();
1060        let where_val = params["WHERE"].as_str().unwrap();
1061        assert!(where_val.starts_with("DESCR"));
1062    }
1063
1064    #[test]
1065    fn mqsc_command_empty_where_clause_ignored() {
1066        let transport = MockTransport::new(vec![empty_success_response()]);
1067        let mut session = mock_session(transport);
1068        session
1069            .mqsc_command("DISPLAY", "QUEUE", Some("*"), None, None, Some("  "))
1070            .unwrap();
1071        let payload = session.last_command_payload.unwrap();
1072        assert!(!payload.contains_key("parameters"));
1073    }
1074
1075    #[test]
1076    fn mqsc_command_transport_error() {
1077        let transport = MockTransport::new(vec![]);
1078        let mut session = mock_session(transport);
1079        let result = session.mqsc_command("DISPLAY", "QMGR", None, None, None, None);
1080        assert!(result.is_err());
1081    }
1082
1083    #[test]
1084    fn mqsc_command_invalid_json() {
1085        let transport = MockTransport::new(vec![TransportResponse {
1086            status_code: 200,
1087            text: "not json".into(),
1088            headers: HashMap::new(),
1089        }]);
1090        let mut session = mock_session(transport);
1091        let result = session.mqsc_command("DISPLAY", "QMGR", None, None, None, None);
1092        assert!(format!("{:?}", result.unwrap_err()).starts_with("Response"));
1093    }
1094
1095    #[test]
1096    fn mqsc_command_non_object_json() {
1097        let transport = MockTransport::new(vec![TransportResponse {
1098            status_code: 200,
1099            text: "[1,2,3]".into(),
1100            headers: HashMap::new(),
1101        }]);
1102        let mut session = mock_session(transport);
1103        let result = session.mqsc_command("DISPLAY", "QMGR", None, None, None, None);
1104        assert!(format!("{:?}", result.unwrap_err()).starts_with("Response"));
1105    }
1106
1107    #[test]
1108    fn mqsc_command_error_response() {
1109        let transport = MockTransport::new(vec![error_response(2, 3008)]);
1110        let mut session = mock_session(transport);
1111        let result = session.mqsc_command("DISPLAY", "QMGR", None, None, None, None);
1112        assert!(format!("{:?}", result.unwrap_err()).starts_with("Command"));
1113    }
1114
1115    #[test]
1116    fn mqsc_command_command_response_not_list() {
1117        let body = json!({
1118            "overallCompletionCode": 0,
1119            "overallReasonCode": 0,
1120            "commandResponse": "not_a_list"
1121        });
1122        let transport = MockTransport::new(vec![TransportResponse {
1123            status_code: 200,
1124            text: body.to_string(),
1125            headers: HashMap::new(),
1126        }]);
1127        let mut session = mock_session(transport);
1128        let result = session.mqsc_command("DISPLAY", "QMGR", None, None, None, None);
1129        assert!(result.is_err());
1130    }
1131
1132    #[test]
1133    fn mqsc_command_empty_command_response() {
1134        let transport = MockTransport::new(vec![empty_success_response()]);
1135        let mut session = mock_session(transport);
1136        let result = session
1137            .mqsc_command("DISPLAY", "QMGR", None, None, None, None)
1138            .unwrap();
1139        assert!(result.is_empty());
1140    }
1141
1142    #[test]
1143    fn mqsc_command_nested_objects_flattened() {
1144        let body = json!({
1145            "overallCompletionCode": 0,
1146            "overallReasonCode": 0,
1147            "commandResponse": [{
1148                "completionCode": 0,
1149                "reasonCode": 0,
1150                "parameters": {
1151                    "shared_key": "shared_val",
1152                    "objects": [
1153                        {"nested_key": "val1"},
1154                        {"nested_key": "val2"}
1155                    ]
1156                }
1157            }]
1158        });
1159        let transport = MockTransport::new(vec![TransportResponse {
1160            status_code: 200,
1161            text: body.to_string(),
1162            headers: HashMap::new(),
1163        }]);
1164        let mut session = mock_session(transport);
1165        let result = session
1166            .mqsc_command("DISPLAY", "QUEUE", Some("*"), None, None, None)
1167            .unwrap();
1168        assert_eq!(result.len(), 2);
1169        assert_eq!(result[0]["shared_key"], json!("shared_val"));
1170        assert_eq!(result[0]["nested_key"], json!("val1"));
1171        assert_eq!(result[1]["nested_key"], json!("val2"));
1172    }
1173
1174    #[test]
1175    fn mqsc_command_with_request_parameters() {
1176        let transport = MockTransport::new(vec![empty_success_response()]);
1177        let mut session = mock_session(transport);
1178        let mut req_params = HashMap::new();
1179        req_params.insert("FORCE".into(), json!("YES"));
1180        session
1181            .mqsc_command("ALTER", "QMGR", None, Some(&req_params), None, None)
1182            .unwrap();
1183        let payload = session.last_command_payload.unwrap();
1184        assert!(payload.contains_key("parameters"));
1185    }
1186
1187    #[test]
1188    fn mqsc_command_with_explicit_response_parameters() {
1189        let transport = MockTransport::new(vec![empty_success_response()]);
1190        let mut session = mock_session(transport);
1191        let resp_params: &[&str] = &["DESCR", "MAXDEPTH"];
1192        session
1193            .mqsc_command("DISPLAY", "QUEUE", Some("*"), None, Some(resp_params), None)
1194            .unwrap();
1195        let payload = session.last_command_payload.unwrap();
1196        let rp = payload["responseParameters"].as_array().unwrap();
1197        assert_eq!(rp.len(), 2);
1198    }
1199
1200    // =====================================================================
1201    // Private helper tests
1202    // =====================================================================
1203
1204    #[test]
1205    fn build_headers_basic_auth() {
1206        let transport = MockTransport::new(vec![]);
1207        let session = MqRestSession::builder(
1208            "https://host/ibmmq/rest/v2",
1209            "QM1",
1210            Credentials::Basic {
1211                username: "admin".into(),
1212                password: "secret".into(),
1213            },
1214        )
1215        .transport(Box::new(transport))
1216        .build()
1217        .unwrap();
1218        let headers = session.build_headers();
1219        assert!(headers["Authorization"].starts_with("Basic "));
1220        assert!(headers.contains_key("ibm-mq-rest-csrf-token"));
1221    }
1222
1223    #[test]
1224    fn build_headers_ltpa() {
1225        let mut login_headers = HashMap::new();
1226        login_headers.insert("Set-Cookie".into(), "LtpaToken2=tok; Path=/".into());
1227        let transport = MockTransport::new(vec![TransportResponse {
1228            status_code: 200,
1229            text: "{}".into(),
1230            headers: login_headers,
1231        }]);
1232        let session = MqRestSession::builder(
1233            "https://host/ibmmq/rest/v2",
1234            "QM1",
1235            Credentials::Ltpa {
1236                username: "u".into(),
1237                password: "p".into(),
1238            },
1239        )
1240        .transport(Box::new(transport))
1241        .build()
1242        .unwrap();
1243        let headers = session.build_headers();
1244        assert!(headers["Cookie"].contains("LtpaToken2=tok"));
1245    }
1246
1247    #[test]
1248    fn build_headers_certificate() {
1249        let transport = MockTransport::new(vec![]);
1250        let session = MqRestSession::builder(
1251            "https://host/ibmmq/rest/v2",
1252            "QM1",
1253            Credentials::Certificate {
1254                cert_path: "/fake".into(),
1255                key_path: None,
1256            },
1257        )
1258        .transport(Box::new(transport))
1259        .build()
1260        .unwrap();
1261        let headers = session.build_headers();
1262        assert!(!headers.contains_key("Authorization"));
1263        assert!(!headers.contains_key("Cookie"));
1264    }
1265
1266    #[test]
1267    fn build_headers_gateway() {
1268        let transport = MockTransport::new(vec![]);
1269        let session = MqRestSession::builder(
1270            "https://host/ibmmq/rest/v2",
1271            "QM1",
1272            Credentials::Basic {
1273                username: "u".into(),
1274                password: "p".into(),
1275            },
1276        )
1277        .gateway_qmgr("GW1")
1278        .transport(Box::new(transport))
1279        .build()
1280        .unwrap();
1281        let headers = session.build_headers();
1282        assert_eq!(headers["ibm-mq-rest-gateway-qmgr"], "GW1");
1283    }
1284
1285    #[test]
1286    fn build_headers_no_csrf() {
1287        let transport = MockTransport::new(vec![]);
1288        let session = MqRestSession::builder(
1289            "https://host/ibmmq/rest/v2",
1290            "QM1",
1291            Credentials::Basic {
1292                username: "u".into(),
1293                password: "p".into(),
1294            },
1295        )
1296        .csrf_token(None)
1297        .transport(Box::new(transport))
1298        .build()
1299        .unwrap();
1300        let headers = session.build_headers();
1301        assert!(!headers.contains_key("ibm-mq-rest-csrf-token"));
1302    }
1303
1304    #[test]
1305    fn build_command_payload_with_name() {
1306        let params = HashMap::new();
1307        let resp: Vec<String> = vec![];
1308        let payload = build_command_payload("DISPLAY", "QUEUE", Some("Q1"), &params, &resp);
1309        assert_eq!(payload["command"], json!("DISPLAY"));
1310        assert_eq!(payload["qualifier"], json!("QUEUE"));
1311        assert_eq!(payload["name"], json!("Q1"));
1312    }
1313
1314    #[test]
1315    fn build_command_payload_without_name() {
1316        let params = HashMap::new();
1317        let resp: Vec<String> = vec![];
1318        let payload = build_command_payload("DISPLAY", "QMGR", None, &params, &resp);
1319        assert!(!payload.contains_key("name"));
1320    }
1321
1322    #[test]
1323    fn build_command_payload_empty_name() {
1324        let params = HashMap::new();
1325        let resp: Vec<String> = vec![];
1326        let payload = build_command_payload("DISPLAY", "QMGR", Some(""), &params, &resp);
1327        assert!(!payload.contains_key("name"));
1328    }
1329
1330    #[test]
1331    fn build_command_payload_with_params() {
1332        let mut params = HashMap::new();
1333        params.insert("FORCE".into(), json!("YES"));
1334        let resp: Vec<String> = vec!["DESCR".into()];
1335        let payload = build_command_payload("ALTER", "QMGR", None, &params, &resp);
1336        assert!(payload.contains_key("parameters"));
1337        assert!(payload.contains_key("responseParameters"));
1338    }
1339
1340    #[test]
1341    fn normalize_response_parameters_none_display() {
1342        let result = normalize_response_parameters(None, true);
1343        assert_eq!(result, vec!["all".to_owned()]);
1344    }
1345
1346    #[test]
1347    fn normalize_response_parameters_none_non_display() {
1348        let result = normalize_response_parameters(None, false);
1349        assert!(result.is_empty());
1350    }
1351
1352    #[test]
1353    fn normalize_response_parameters_all() {
1354        let result = normalize_response_parameters(Some(&["ALL"]), false);
1355        assert_eq!(result, vec!["all".to_owned()]);
1356    }
1357
1358    #[test]
1359    fn normalize_response_parameters_explicit() {
1360        let result = normalize_response_parameters(Some(&["DESCR", "MAXDEPTH"]), true);
1361        assert_eq!(result, vec!["DESCR".to_owned(), "MAXDEPTH".to_owned()]);
1362    }
1363
1364    #[test]
1365    fn flatten_nested_objects_no_nesting() {
1366        let item = {
1367            let mut m = HashMap::new();
1368            m.insert("key".into(), json!("val"));
1369            m
1370        };
1371        let result = flatten_nested_objects(vec![item]);
1372        assert_eq!(result.len(), 1);
1373        assert_eq!(result[0]["key"], json!("val"));
1374    }
1375
1376    #[test]
1377    fn flatten_nested_objects_with_nesting() {
1378        let item = {
1379            let mut m = HashMap::new();
1380            m.insert("shared".into(), json!("s"));
1381            m.insert("objects".into(), json!([{"a": 1}, {"b": 2}]));
1382            m
1383        };
1384        let result = flatten_nested_objects(vec![item]);
1385        assert_eq!(result.len(), 2);
1386        assert_eq!(result[0]["shared"], json!("s"));
1387        assert_eq!(result[0]["a"], json!(1));
1388        assert_eq!(result[1]["b"], json!(2));
1389    }
1390
1391    #[test]
1392    fn parse_response_payload_valid() {
1393        let result = parse_response_payload(r#"{"key": "value"}"#).unwrap();
1394        assert_eq!(result["key"], json!("value"));
1395    }
1396
1397    #[test]
1398    fn parse_response_payload_invalid_json() {
1399        let result = parse_response_payload("not json");
1400        assert!(result.is_err());
1401    }
1402
1403    #[test]
1404    fn parse_response_payload_non_object() {
1405        let result = parse_response_payload("[1,2]");
1406        assert!(result.is_err());
1407    }
1408
1409    #[test]
1410    fn extract_command_response_present() {
1411        let mut payload = HashMap::new();
1412        payload.insert(
1413            "commandResponse".into(),
1414            json!([{"completionCode": 0, "parameters": {}}]),
1415        );
1416        let result = extract_command_response(&payload).unwrap();
1417        assert_eq!(result.len(), 1);
1418    }
1419
1420    #[test]
1421    fn extract_command_response_missing() {
1422        let payload = HashMap::new();
1423        let result = extract_command_response(&payload).unwrap();
1424        assert!(result.is_empty());
1425    }
1426
1427    #[test]
1428    fn extract_command_response_not_list() {
1429        let mut payload = HashMap::new();
1430        payload.insert("commandResponse".into(), json!("string"));
1431        let result = extract_command_response(&payload);
1432        assert!(result.is_err());
1433    }
1434
1435    #[test]
1436    fn extract_command_response_item_not_object() {
1437        let mut payload = HashMap::new();
1438        payload.insert("commandResponse".into(), json!([42]));
1439        let result = extract_command_response(&payload);
1440        assert!(result.is_err());
1441    }
1442
1443    #[test]
1444    fn raise_for_command_errors_none() {
1445        let mut payload = HashMap::new();
1446        payload.insert("overallCompletionCode".into(), json!(0));
1447        payload.insert("overallReasonCode".into(), json!(0));
1448        assert!(raise_for_command_errors(&payload, 200).is_ok());
1449    }
1450
1451    #[test]
1452    fn raise_for_command_errors_overall() {
1453        let mut payload = HashMap::new();
1454        payload.insert("overallCompletionCode".into(), json!(2));
1455        payload.insert("overallReasonCode".into(), json!(3008));
1456        assert!(
1457            format!("{:?}", raise_for_command_errors(&payload, 200).unwrap_err())
1458                .starts_with("Command")
1459        );
1460    }
1461
1462    #[test]
1463    fn raise_for_command_errors_item_level() {
1464        let mut payload = HashMap::new();
1465        payload.insert("overallCompletionCode".into(), json!(0));
1466        payload.insert("overallReasonCode".into(), json!(0));
1467        payload.insert(
1468            "commandResponse".into(),
1469            json!([{"completionCode": 2, "reasonCode": 3008}]),
1470        );
1471        assert!(
1472            format!("{:?}", raise_for_command_errors(&payload, 200).unwrap_err())
1473                .starts_with("Command")
1474        );
1475    }
1476
1477    #[test]
1478    fn raise_for_command_errors_both_overall_and_item() {
1479        let mut payload = HashMap::new();
1480        payload.insert("overallCompletionCode".into(), json!(2));
1481        payload.insert("overallReasonCode".into(), json!(3008));
1482        payload.insert(
1483            "commandResponse".into(),
1484            json!([{"completionCode": 2, "reasonCode": 3008}]),
1485        );
1486        let err_msg = format!("{}", raise_for_command_errors(&payload, 200).unwrap_err());
1487        assert!(err_msg.contains("overallCompletionCode"));
1488        assert!(err_msg.contains("commandResponse"));
1489    }
1490
1491    #[test]
1492    fn map_where_keyword_with_known_key() {
1493        let data = json!({
1494            "qualifiers": {
1495                "queue": {
1496                    "request_key_map": {"description": "DESCR"},
1497                    "response_key_map": {}
1498                }
1499            }
1500        });
1501        let result = map_where_keyword("description LK test*", "queue", false, &data).unwrap();
1502        assert_eq!(result, "DESCR LK test*");
1503    }
1504
1505    #[test]
1506    fn map_where_keyword_unknown_key_non_strict() {
1507        let data =
1508            json!({"qualifiers": {"queue": {"request_key_map": {}, "response_key_map": {}}}});
1509        let result = map_where_keyword("unknown_attr LK test*", "queue", false, &data).unwrap();
1510        assert_eq!(result, "unknown_attr LK test*");
1511    }
1512
1513    #[test]
1514    fn map_where_keyword_unknown_key_strict() {
1515        let data =
1516            json!({"qualifiers": {"queue": {"request_key_map": {}, "response_key_map": {}}}});
1517        let result = map_where_keyword("unknown_attr LK test*", "queue", true, &data);
1518        assert!(result.is_err());
1519    }
1520
1521    #[test]
1522    fn map_where_keyword_unknown_qualifier_non_strict() {
1523        let data = json!({"qualifiers": {}});
1524        let result = map_where_keyword("desc LK x", "noexist", false, &data).unwrap();
1525        assert_eq!(result, "desc LK x");
1526    }
1527
1528    #[test]
1529    fn map_where_keyword_unknown_qualifier_strict() {
1530        let data = json!({"qualifiers": {}});
1531        let result = map_where_keyword("desc LK x", "noexist", true, &data);
1532        assert!(result.is_err());
1533    }
1534
1535    #[test]
1536    fn map_where_keyword_no_rest() {
1537        let data = json!({
1538            "qualifiers": {
1539                "queue": {
1540                    "request_key_map": {"description": "DESCR"},
1541                    "response_key_map": {}
1542                }
1543            }
1544        });
1545        let result = map_where_keyword("description", "queue", false, &data).unwrap();
1546        assert_eq!(result, "DESCR");
1547    }
1548
1549    #[test]
1550    fn resolve_mapping_qualifier_from_commands() {
1551        let transport = MockTransport::new(vec![]);
1552        let session = MqRestSession::builder(
1553            "https://host/ibmmq/rest/v2",
1554            "QM1",
1555            Credentials::Basic {
1556                username: "u".into(),
1557                password: "p".into(),
1558            },
1559        )
1560        .transport(Box::new(transport))
1561        .build()
1562        .unwrap();
1563        // DISPLAY CHSTATUS should resolve to "chstatus" via commands map
1564        let qualifier = session.resolve_mapping_qualifier("DISPLAY", "CHSTATUS");
1565        assert_eq!(qualifier, "chstatus");
1566    }
1567
1568    #[test]
1569    fn resolve_mapping_qualifier_default_fallback() {
1570        let transport = MockTransport::new(vec![]);
1571        let session = MqRestSession::builder(
1572            "https://host/ibmmq/rest/v2",
1573            "QM1",
1574            Credentials::Basic {
1575                username: "u".into(),
1576                password: "p".into(),
1577            },
1578        )
1579        .transport(Box::new(transport))
1580        .build()
1581        .unwrap();
1582        let qualifier = session.resolve_mapping_qualifier("DISPLAY", "QLOCAL");
1583        assert_eq!(qualifier, "queue");
1584    }
1585
1586    #[test]
1587    fn resolve_mapping_qualifier_lowercase_fallback() {
1588        let transport = MockTransport::new(vec![]);
1589        let session = MqRestSession::builder(
1590            "https://host/ibmmq/rest/v2",
1591            "QM1",
1592            Credentials::Basic {
1593                username: "u".into(),
1594                password: "p".into(),
1595            },
1596        )
1597        .transport(Box::new(transport))
1598        .build()
1599        .unwrap();
1600        let qualifier = session.resolve_mapping_qualifier("DISPLAY", "UNKNOWNOBJ");
1601        assert_eq!(qualifier, "unknownobj");
1602    }
1603
1604    #[test]
1605    fn last_response_fields_populated() {
1606        let transport = MockTransport::new(vec![empty_success_response()]);
1607        let mut session = mock_session(transport);
1608        session
1609            .mqsc_command("DISPLAY", "QMGR", None, None, None, None)
1610            .unwrap();
1611        assert!(session.last_response_payload.is_some());
1612        assert!(session.last_response_text.is_some());
1613        assert!(session.last_http_status.is_some());
1614        assert!(session.last_command_payload.is_some());
1615    }
1616
1617    #[test]
1618    fn url_trailing_slash_stripped() {
1619        let transport = MockTransport::new(vec![empty_success_response()]);
1620        let mut session = MqRestSession::builder(
1621            "https://host/ibmmq/rest/v2/",
1622            "QM1",
1623            Credentials::Basic {
1624                username: "u".into(),
1625                password: "p".into(),
1626            },
1627        )
1628        .map_attributes(false)
1629        .transport(Box::new(transport))
1630        .build()
1631        .unwrap();
1632        session
1633            .mqsc_command("DISPLAY", "QMGR", None, None, None, None)
1634            .unwrap();
1635        assert_eq!(session.last_http_status, Some(200));
1636    }
1637
1638    #[test]
1639    fn command_response_item_without_parameters() {
1640        let body = json!({
1641            "overallCompletionCode": 0,
1642            "overallReasonCode": 0,
1643            "commandResponse": [{"completionCode": 0, "reasonCode": 0}]
1644        });
1645        let transport = MockTransport::new(vec![TransportResponse {
1646            status_code: 200,
1647            text: body.to_string(),
1648            headers: HashMap::new(),
1649        }]);
1650        let mut session = mock_session(transport);
1651        let result = session
1652            .mqsc_command("DISPLAY", "QMGR", None, None, None, None)
1653            .unwrap();
1654        assert_eq!(result.len(), 1);
1655        assert!(result[0].is_empty());
1656    }
1657
1658    #[test]
1659    fn command_error_response_test() {
1660        let transport = MockTransport::new(vec![command_error_response()]);
1661        let mut session = mock_session(transport);
1662        let result = session.mqsc_command("DISPLAY", "QUEUE", Some("Q1"), None, None, None);
1663        assert!(format!("{:?}", result.unwrap_err()).starts_with("Command"));
1664    }
1665
1666    // =====================================================================
1667    // Replace mode and Certificate credential paths
1668    // =====================================================================
1669
1670    #[test]
1671    fn builder_replace_mode_incomplete_errors() {
1672        let transport = MockTransport::new(vec![]);
1673        let overrides = json!({"commands": {}, "qualifiers": {}});
1674        let result = MqRestSession::builder(
1675            "https://host/ibmmq/rest/v2",
1676            "QM1",
1677            Credentials::Basic {
1678                username: "u".into(),
1679                password: "p".into(),
1680            },
1681        )
1682        .mapping_overrides(overrides)
1683        .mapping_overrides_mode(MappingOverrideMode::Replace)
1684        .transport(Box::new(transport))
1685        .build();
1686        assert!(result.is_err());
1687    }
1688
1689    #[test]
1690    fn builder_certificate_no_transport_fails_missing_file() {
1691        let result = MqRestSession::builder(
1692            "https://host/ibmmq/rest/v2",
1693            "QM1",
1694            Credentials::Certificate {
1695                cert_path: "/nonexistent/cert.pem".into(),
1696                key_path: None,
1697            },
1698        )
1699        .build();
1700        assert!(result.is_err());
1701    }
1702
1703    #[test]
1704    fn builder_certificate_missing_key_file() {
1705        // Create a temp cert file
1706        let cert_path = std::env::temp_dir().join("test_cert_session.pem");
1707        std::fs::write(&cert_path, b"fake-cert").unwrap();
1708        let result = MqRestSession::builder(
1709            "https://host/ibmmq/rest/v2",
1710            "QM1",
1711            Credentials::Certificate {
1712                cert_path: cert_path.to_str().unwrap().into(),
1713                key_path: Some("/nonexistent/key.pem".into()),
1714            },
1715        )
1716        .build();
1717        assert!(result.is_err());
1718        let _ = std::fs::remove_file(&cert_path);
1719    }
1720
1721    #[test]
1722    fn builder_certificate_invalid_pem() {
1723        let cert_path = std::env::temp_dir().join("test_cert_session2.pem");
1724        std::fs::write(&cert_path, b"not-a-valid-pem").unwrap();
1725        let result = MqRestSession::builder(
1726            "https://host/ibmmq/rest/v2",
1727            "QM1",
1728            Credentials::Certificate {
1729                cert_path: cert_path.to_str().unwrap().into(),
1730                key_path: None,
1731            },
1732        )
1733        .build();
1734        assert!(result.is_err());
1735        let _ = std::fs::remove_file(&cert_path);
1736    }
1737
1738    #[test]
1739    fn builder_certificate_valid_pem_no_transport() {
1740        let cert_path = concat!(
1741            env!("CARGO_MANIFEST_DIR"),
1742            "/test-fixtures/test-combined.pem"
1743        );
1744        let session = MqRestSession::builder(
1745            "https://host/ibmmq/rest/v2",
1746            "QM1",
1747            Credentials::Certificate {
1748                cert_path: cert_path.into(),
1749                key_path: None,
1750            },
1751        )
1752        .build()
1753        .unwrap();
1754        assert_eq!(session.qmgr_name(), "QM1");
1755    }
1756
1757    #[test]
1758    fn builder_verify_tls_true_default_transport() {
1759        // This path creates a ReqwestTransport::new() — just verify it doesn't panic
1760        let session = MqRestSession::builder(
1761            "https://host/ibmmq/rest/v2",
1762            "QM1",
1763            Credentials::Basic {
1764                username: "u".into(),
1765                password: "p".into(),
1766            },
1767        )
1768        .verify_tls(true)
1769        .build()
1770        .unwrap();
1771        assert_eq!(session.qmgr_name(), "QM1");
1772    }
1773
1774    #[test]
1775    fn builder_verify_tls_false_default_transport() {
1776        let session = MqRestSession::builder(
1777            "https://host/ibmmq/rest/v2",
1778            "QM1",
1779            Credentials::Basic {
1780                username: "u".into(),
1781                password: "p".into(),
1782            },
1783        )
1784        .verify_tls(false)
1785        .build()
1786        .unwrap();
1787        assert_eq!(session.qmgr_name(), "QM1");
1788    }
1789
1790    // =====================================================================
1791    // map_response_parameters tests
1792    // =====================================================================
1793
1794    #[test]
1795    fn map_response_parameters_all_passthrough() {
1796        let transport = MockTransport::new(vec![]);
1797        let session = MqRestSession::builder(
1798            "https://host/ibmmq/rest/v2",
1799            "QM1",
1800            Credentials::Basic {
1801                username: "u".into(),
1802                password: "p".into(),
1803            },
1804        )
1805        .transport(Box::new(transport))
1806        .build()
1807        .unwrap();
1808        let params = vec!["all".to_owned()];
1809        let result = session
1810            .map_response_parameters("DISPLAY", "QUEUE", "queue", &params)
1811            .unwrap();
1812        assert_eq!(result, vec!["all".to_owned()]);
1813    }
1814
1815    #[test]
1816    fn map_response_parameters_maps_snake_case() {
1817        let transport = MockTransport::new(vec![]);
1818        let session = MqRestSession::builder(
1819            "https://host/ibmmq/rest/v2",
1820            "QM1",
1821            Credentials::Basic {
1822                username: "u".into(),
1823                password: "p".into(),
1824            },
1825        )
1826        .transport(Box::new(transport))
1827        .build()
1828        .unwrap();
1829        let params = vec!["description".to_owned()];
1830        let result = session
1831            .map_response_parameters("DISPLAY", "QUEUE", "queue", &params)
1832            .unwrap();
1833        assert!(result.contains(&"DESCR".to_owned()));
1834    }
1835
1836    #[test]
1837    fn map_response_parameters_unknown_qualifier_non_strict() {
1838        let transport = MockTransport::new(vec![]);
1839        let session = MqRestSession::builder(
1840            "https://host/ibmmq/rest/v2",
1841            "QM1",
1842            Credentials::Basic {
1843                username: "u".into(),
1844                password: "p".into(),
1845            },
1846        )
1847        .mapping_strict(false)
1848        .transport(Box::new(transport))
1849        .build()
1850        .unwrap();
1851        let params = vec!["foo".to_owned()];
1852        let result = session
1853            .map_response_parameters("DISPLAY", "NONEXIST", "nonexist", &params)
1854            .unwrap();
1855        assert_eq!(result, vec!["foo".to_owned()]);
1856    }
1857
1858    #[test]
1859    fn map_response_parameters_unknown_qualifier_strict() {
1860        let transport = MockTransport::new(vec![]);
1861        let session = MqRestSession::builder(
1862            "https://host/ibmmq/rest/v2",
1863            "QM1",
1864            Credentials::Basic {
1865                username: "u".into(),
1866                password: "p".into(),
1867            },
1868        )
1869        .mapping_strict(true)
1870        .transport(Box::new(transport))
1871        .build()
1872        .unwrap();
1873        let params = vec!["foo".to_owned()];
1874        let result = session.map_response_parameters("DISPLAY", "NONEXIST", "nonexist", &params);
1875        assert!(result.is_err());
1876    }
1877
1878    #[test]
1879    fn map_response_parameters_unknown_key_strict() {
1880        let transport = MockTransport::new(vec![]);
1881        let session = MqRestSession::builder(
1882            "https://host/ibmmq/rest/v2",
1883            "QM1",
1884            Credentials::Basic {
1885                username: "u".into(),
1886                password: "p".into(),
1887            },
1888        )
1889        .mapping_strict(true)
1890        .transport(Box::new(transport))
1891        .build()
1892        .unwrap();
1893        let params = vec!["unknown_snake_key".to_owned()];
1894        let result = session.map_response_parameters("DISPLAY", "QUEUE", "queue", &params);
1895        assert!(result.is_err());
1896    }
1897
1898    #[test]
1899    fn map_response_parameters_macro_name() {
1900        let transport = MockTransport::new(vec![]);
1901        let session = MqRestSession::builder(
1902            "https://host/ibmmq/rest/v2",
1903            "QM1",
1904            Credentials::Basic {
1905                username: "u".into(),
1906                password: "p".into(),
1907            },
1908        )
1909        .mapping_strict(false)
1910        .transport(Box::new(transport))
1911        .build()
1912        .unwrap();
1913        // Use a response parameter macro name if available for DISPLAY QUEUE
1914        let params = vec!["description".to_owned(), "max_queue_depth".to_owned()];
1915        let result = session
1916            .map_response_parameters("DISPLAY", "QUEUE", "queue", &params)
1917            .unwrap();
1918        assert_eq!(result.len(), 2);
1919    }
1920
1921    // =====================================================================
1922    // mqsc_command with mapping — error propagation paths
1923    // =====================================================================
1924
1925    #[test]
1926    fn mqsc_command_mapping_request_error_propagates() {
1927        let transport = MockTransport::new(vec![empty_success_response()]);
1928        let mut session = MqRestSession::builder(
1929            "https://host/ibmmq/rest/v2",
1930            "QM1",
1931            Credentials::Basic {
1932                username: "u".into(),
1933                password: "p".into(),
1934            },
1935        )
1936        .map_attributes(true)
1937        .mapping_strict(true)
1938        .transport(Box::new(transport))
1939        .build()
1940        .unwrap();
1941        let mut params = HashMap::new();
1942        params.insert("totally_unknown_key".into(), json!("val"));
1943        let result = session.mqsc_command("DISPLAY", "QUEUE", Some("*"), Some(&params), None, None);
1944        assert!(result.is_err());
1945    }
1946
1947    #[test]
1948    fn mqsc_command_mapping_response_param_error_propagates() {
1949        let transport = MockTransport::new(vec![empty_success_response()]);
1950        let mut session = MqRestSession::builder(
1951            "https://host/ibmmq/rest/v2",
1952            "QM1",
1953            Credentials::Basic {
1954                username: "u".into(),
1955                password: "p".into(),
1956            },
1957        )
1958        .map_attributes(true)
1959        .mapping_strict(true)
1960        .transport(Box::new(transport))
1961        .build()
1962        .unwrap();
1963        let resp_params: &[&str] = &["totally_unknown_snake_param"];
1964        let result =
1965            session.mqsc_command("DISPLAY", "QUEUE", Some("*"), None, Some(resp_params), None);
1966        assert!(result.is_err());
1967    }
1968
1969    #[test]
1970    fn mqsc_command_mapping_response_list_error_propagates() {
1971        // Strict mode + unknown response key should propagate the mapping error
1972        let mut params = HashMap::new();
1973        params.insert("UNKNOWN_RESP_KEY".into(), json!("val"));
1974        let transport = MockTransport::new(vec![success_response(vec![params])]);
1975        let mut session = MqRestSession::builder(
1976            "https://host/ibmmq/rest/v2",
1977            "QM1",
1978            Credentials::Basic {
1979                username: "u".into(),
1980                password: "p".into(),
1981            },
1982        )
1983        .map_attributes(true)
1984        .mapping_strict(true)
1985        .transport(Box::new(transport))
1986        .build()
1987        .unwrap();
1988        let result = session.mqsc_command("DISPLAY", "QUEUE", Some("*"), None, None, None);
1989        assert!(result.is_err());
1990    }
1991
1992    // =====================================================================
1993    // get_response_parameter_macros / build_snake_to_mqsc_map
1994    // =====================================================================
1995
1996    #[test]
1997    fn get_response_parameter_macros_existing() {
1998        let data = &*MAPPING_DATA;
1999        let macros = get_response_parameter_macros("DISPLAY", "QUEUE", data);
2000        // DISPLAY QUEUE should have some macros in the mapping data
2001        let _ = macros.len(); // just exercises the code path
2002    }
2003
2004    #[test]
2005    fn get_response_parameter_macros_missing_command() {
2006        let data = json!({"commands": {}});
2007        let macros = get_response_parameter_macros("DISPLAY", "NONEXIST", &data);
2008        assert!(macros.is_empty());
2009    }
2010
2011    #[test]
2012    fn get_response_parameter_macros_no_macros_key() {
2013        let data = json!({"commands": {"DISPLAY QUEUE": {}}});
2014        let macros = get_response_parameter_macros("DISPLAY", "QUEUE", &data);
2015        assert!(macros.is_empty());
2016    }
2017
2018    #[test]
2019    fn build_snake_to_mqsc_map_combines_both_maps() {
2020        let entry = json!({
2021            "request_key_map": {"snake_req": "MQSC_REQ"},
2022            "response_key_map": {"MQSC_RESP": "snake_resp"}
2023        });
2024        let result = build_snake_to_mqsc_map(&entry);
2025        assert_eq!(result.get("snake_req").unwrap(), "MQSC_REQ");
2026        assert_eq!(result.get("snake_resp").unwrap(), "MQSC_RESP");
2027    }
2028
2029    #[test]
2030    fn build_snake_to_mqsc_map_empty() {
2031        let entry = json!({});
2032        let result = build_snake_to_mqsc_map(&entry);
2033        assert!(result.is_empty());
2034    }
2035
2036    #[test]
2037    fn map_response_parameter_names_with_macros() {
2038        let mut macro_lookup = HashMap::new();
2039        macro_lookup.insert("events".into(), "EVENTS".into());
2040        let mut combined_map = HashMap::new();
2041        combined_map.insert("description".into(), "DESCR".into());
2042        let params = vec!["events".into(), "description".into(), "unknown".into()];
2043        let (mapped, issues) =
2044            map_response_parameter_names(&params, &macro_lookup, &combined_map, "queue");
2045        assert_eq!(mapped[0], "EVENTS");
2046        assert_eq!(mapped[1], "DESCR");
2047        assert_eq!(mapped[2], "unknown");
2048        assert_eq!(issues.len(), 1);
2049    }
2050
2051    #[test]
2052    fn get_command_map_valid() {
2053        let data = json!({"commands": {"DISPLAY QUEUE": {"qualifier": "queue"}}});
2054        let result = get_command_map(&data);
2055        assert!(result.contains_key("DISPLAY QUEUE"));
2056    }
2057
2058    #[test]
2059    fn get_command_map_missing() {
2060        let data = json!({});
2061        let result = get_command_map(&data);
2062        assert!(result.is_empty());
2063    }
2064
2065    #[test]
2066    fn get_qualifier_entry_present() {
2067        let data = json!({"qualifiers": {"queue": {"request_key_map": {}}}});
2068        assert!(get_qualifier_entry("queue", &data).is_some());
2069    }
2070
2071    #[test]
2072    fn get_qualifier_entry_missing() {
2073        let data = json!({"qualifiers": {}});
2074        assert!(get_qualifier_entry("nonexist", &data).is_none());
2075    }
2076
2077    #[test]
2078    fn normalize_response_attributes_uppercases_keys() {
2079        let mut attrs = HashMap::new();
2080        attrs.insert("descr".into(), json!("test"));
2081        let result = normalize_response_attributes(&attrs);
2082        assert!(result.contains_key("DESCR"));
2083    }
2084
2085    #[test]
2086    fn is_all_response_parameters_true() {
2087        assert!(is_all_response_parameters(&["ALL".to_owned()]));
2088        assert!(is_all_response_parameters(&["all".to_owned()]));
2089    }
2090
2091    #[test]
2092    fn is_all_response_parameters_false() {
2093        assert!(!is_all_response_parameters(&["DESCR".to_owned()]));
2094        assert!(!is_all_response_parameters(&[]));
2095    }
2096
2097    #[test]
2098    fn build_basic_auth_header_format() {
2099        let header = build_basic_auth_header("admin", "secret");
2100        assert!(header.starts_with("Basic "));
2101    }
2102
2103    #[test]
2104    fn build_unknown_qualifier_issue_format() {
2105        let issues = build_unknown_qualifier_issue("test_q");
2106        assert_eq!(issues.len(), 1);
2107        assert_eq!(issues[0].reason, "unknown_qualifier");
2108        assert_eq!(issues[0].qualifier, Some("test_q".into()));
2109    }
2110
2111    #[test]
2112    fn mqsc_command_where_clause_mapping_error_strict() {
2113        let transport = MockTransport::new(vec![empty_success_response()]);
2114        let mut session = MqRestSession::builder(
2115            "https://host/ibmmq/rest/v2",
2116            "QM1",
2117            Credentials::Basic {
2118                username: "u".into(),
2119                password: "p".into(),
2120            },
2121        )
2122        .map_attributes(true)
2123        .mapping_strict(true)
2124        .transport(Box::new(transport))
2125        .build()
2126        .unwrap();
2127        let result = session.mqsc_command(
2128            "DISPLAY",
2129            "QUEUE",
2130            Some("*"),
2131            None,
2132            None,
2133            Some("totally_bogus_key LK test*"),
2134        );
2135        assert!(result.is_err());
2136    }
2137
2138    #[test]
2139    fn flatten_nested_objects_non_object_in_array() {
2140        let item = {
2141            let mut m = HashMap::new();
2142            m.insert("shared".into(), json!("s"));
2143            m.insert("objects".into(), json!([42, {"a": 1}]));
2144            m
2145        };
2146        let result = flatten_nested_objects(vec![item]);
2147        // Only the object element is flattened, non-objects are skipped
2148        assert_eq!(result.len(), 1);
2149        assert_eq!(result[0]["a"], json!(1));
2150    }
2151
2152    #[test]
2153    fn extract_optional_i64_some() {
2154        assert_eq!(extract_optional_i64(Some(&json!(42))), Some(42));
2155    }
2156
2157    #[test]
2158    fn extract_optional_i64_none() {
2159        assert_eq!(extract_optional_i64(None), None);
2160    }
2161
2162    #[test]
2163    fn extract_optional_i64_non_number() {
2164        assert_eq!(extract_optional_i64(Some(&json!("not a number"))), None);
2165    }
2166
2167    #[test]
2168    fn has_error_codes_both_zero() {
2169        assert!(!has_error_codes(Some(0), Some(0)));
2170    }
2171
2172    #[test]
2173    fn has_error_codes_completion_nonzero() {
2174        assert!(has_error_codes(Some(2), Some(0)));
2175    }
2176
2177    #[test]
2178    fn has_error_codes_reason_nonzero() {
2179        assert!(has_error_codes(Some(0), Some(3008)));
2180    }
2181
2182    #[test]
2183    fn has_error_codes_both_none() {
2184        assert!(!has_error_codes(None, None));
2185    }
2186
2187    #[test]
2188    fn builder_replace_mode_with_complete_overrides() {
2189        let base = &*MAPPING_DATA;
2190        let base_obj = base.as_object().unwrap();
2191        let commands = base_obj.get("commands").cloned().unwrap();
2192        let qualifiers = base_obj.get("qualifiers").cloned().unwrap();
2193        let overrides = json!({"commands": commands, "qualifiers": qualifiers});
2194        let transport = MockTransport::new(vec![]);
2195        let _session = MqRestSession::builder(
2196            "https://host/ibmmq/rest/v2",
2197            "QM1",
2198            Credentials::Basic {
2199                username: "u".into(),
2200                password: "p".into(),
2201            },
2202        )
2203        .mapping_overrides(overrides)
2204        .mapping_overrides_mode(MappingOverrideMode::Replace)
2205        .transport(Box::new(transport))
2206        .build()
2207        .unwrap();
2208    }
2209
2210    #[test]
2211    fn builder_ltpa_login_fails_propagates() {
2212        // LTPA login transport error
2213        let transport = MockTransport::new(vec![]);
2214        let result = MqRestSession::builder(
2215            "https://host/ibmmq/rest/v2",
2216            "QM1",
2217            Credentials::Ltpa {
2218                username: "u".into(),
2219                password: "p".into(),
2220            },
2221        )
2222        .transport(Box::new(transport))
2223        .build();
2224        assert!(result.is_err());
2225    }
2226
2227    #[test]
2228    fn raise_for_command_errors_non_object_item_in_response() {
2229        let mut payload = HashMap::new();
2230        payload.insert("overallCompletionCode".into(), json!(0));
2231        payload.insert("overallReasonCode".into(), json!(0));
2232        payload.insert("commandResponse".into(), json!([42, "string"]));
2233        // Non-object items are skipped — no error
2234        assert!(raise_for_command_errors(&payload, 200).is_ok());
2235    }
2236
2237    #[test]
2238    fn raise_for_command_errors_with_ok_items() {
2239        let mut payload = HashMap::new();
2240        payload.insert("overallCompletionCode".into(), json!(0));
2241        payload.insert("overallReasonCode".into(), json!(0));
2242        payload.insert(
2243            "commandResponse".into(),
2244            json!([
2245                {"completionCode": 0, "reasonCode": 0, "parameters": {"key": "val"}}
2246            ]),
2247        );
2248        assert!(raise_for_command_errors(&payload, 200).is_ok());
2249    }
2250
2251    #[test]
2252    fn build_snake_to_mqsc_map_response_key_map_non_string_ignored() {
2253        let entry = json!({
2254            "response_key_map": {"MQSC": 42},
2255            "request_key_map": {}
2256        });
2257        let result = build_snake_to_mqsc_map(&entry);
2258        assert!(result.is_empty());
2259    }
2260
2261    #[test]
2262    fn build_snake_to_mqsc_map_request_key_map_non_string_ignored() {
2263        let entry = json!({
2264            "response_key_map": {},
2265            "request_key_map": {"snake": 42}
2266        });
2267        let result = build_snake_to_mqsc_map(&entry);
2268        assert!(result.is_empty());
2269    }
2270
2271    // =====================================================================
2272    // build_headers LTPA without token
2273    // =====================================================================
2274
2275    #[test]
2276    fn build_headers_ltpa_no_token_omits_cookie() {
2277        let transport = MockTransport::new(vec![]);
2278        let session = MqRestSession {
2279            rest_base_url: "https://host/ibmmq/rest/v2".into(),
2280            qmgr_name: "QM1".into(),
2281            gateway_qmgr: None,
2282            verify_tls: true,
2283            timeout_seconds: None,
2284            map_attributes: false,
2285            mapping_strict: false,
2286            csrf_token: None,
2287            credentials: Credentials::Ltpa {
2288                username: "user".into(),
2289                password: "pass".into(),
2290            },
2291            mapping_data: json!({}),
2292            transport: Box::new(transport),
2293            ltpa_cookie_name: None,
2294            ltpa_token: None,
2295            last_response_payload: None,
2296            last_response_text: None,
2297            last_http_status: None,
2298            last_command_payload: None,
2299        };
2300        let headers = session.build_headers();
2301        assert!(!headers.contains_key("Cookie"));
2302    }
2303
2304    // =====================================================================
2305    // get_qualifier_entry missing qualifier
2306    // =====================================================================
2307
2308    #[test]
2309    fn get_qualifier_entry_qualifiers_exist_but_missing_qualifier() {
2310        let data = json!({"qualifiers": {"queue": {}}});
2311        assert!(get_qualifier_entry("nonexistent", &data).is_none());
2312    }
2313}