1use 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
19pub const DEFAULT_RESPONSE_PARAMETERS: &[&str] = &["all"];
21
22pub 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
32fn 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
47pub 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 #[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 #[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 #[must_use]
96 pub const fn verify_tls(mut self, verify: bool) -> Self {
97 self.verify_tls = verify;
98 self
99 }
100
101 #[must_use]
103 pub const fn timeout_seconds(mut self, timeout: Option<f64>) -> Self {
104 self.timeout_seconds = timeout;
105 self
106 }
107
108 #[must_use]
110 pub const fn map_attributes(mut self, enabled: bool) -> Self {
111 self.map_attributes = enabled;
112 self
113 }
114
115 #[must_use]
117 pub const fn mapping_strict(mut self, strict: bool) -> Self {
118 self.mapping_strict = strict;
119 self
120 }
121
122 #[must_use]
124 pub fn mapping_overrides(mut self, overrides: Value) -> Self {
125 self.mapping_overrides = Some(overrides);
126 self
127 }
128
129 #[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 #[must_use]
138 pub fn csrf_token(mut self, token: Option<String>) -> Self {
139 self.csrf_token = token;
140 self
141 }
142
143 #[must_use]
145 pub fn transport(mut self, transport: Box<dyn MqRestTransport>) -> Self {
146 self.transport = Some(transport);
147 self
148 }
149
150 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
250pub 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 pub last_response_payload: Option<HashMap<String, Value>>,
268 pub last_response_text: Option<String>,
270 pub last_http_status: Option<u16>,
272 pub last_command_payload: Option<HashMap<String, Value>>,
274}
275
276impl MqRestSession {
277 #[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 #[must_use]
289 pub fn qmgr_name(&self) -> &str {
290 &self.qmgr_name
291 }
292
293 #[must_use]
295 pub fn gateway_qmgr(&self) -> Option<&str> {
296 self.gateway_qmgr.as_deref()
297 }
298
299 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 ¯o_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 #[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 #[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 #[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"), ¶ms, &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, ¶ms, &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(""), ¶ms, &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, ¶ms, &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 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 #[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 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 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 #[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", ¶ms)
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", ¶ms)
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", ¶ms)
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", ¶ms);
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", ¶ms);
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 let params = vec!["description".to_owned(), "max_queue_depth".to_owned()];
1915 let result = session
1916 .map_response_parameters("DISPLAY", "QUEUE", "queue", ¶ms)
1917 .unwrap();
1918 assert_eq!(result.len(), 2);
1919 }
1920
1921 #[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(¶ms), 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 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 #[test]
1997 fn get_response_parameter_macros_existing() {
1998 let data = &*MAPPING_DATA;
1999 let macros = get_response_parameter_macros("DISPLAY", "QUEUE", data);
2000 let _ = macros.len(); }
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(¶ms, ¯o_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 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 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 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 #[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 #[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}