Skip to main content

soap_server/
dispatch.rs

1// SOAP dispatch — route body element QName to registered SoapHandler
2// Also provides XSD-11 payload validation (validate_request).
3
4use crate::fault::SoapFault;
5use crate::handler::SoapHandler;
6use crate::qname::QName;
7use crate::wsdl::definitions::BindingStyle;
8use crate::wsdl::resolver::ResolvedWsdl;
9use crate::xsd::types::{ComplexContent, TypeRegistry};
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13/// A single routing entry — holds the handler plus metadata needed by the pipeline.
14pub struct DispatchEntry {
15    pub handler: Arc<dyn SoapHandler>,
16    /// Whether this operation requires authentication (controlled by auth_bypass set at startup).
17    pub auth_required: bool,
18    /// QName used for routing (body first-child element QName for document/literal,
19    /// synthesized wrapper QName for RPC).  This is the key used for dispatch lookup.
20    pub input_type: Option<QName>,
21    /// QName to look up in the TypeRegistry for structural XSD validation.
22    ///
23    /// For RPC style or inline-element ops, this equals `input_type`.
24    /// For document/literal ops whose message-part element has a named `type=` attribute,
25    /// this is the named type QName rather than the element QName.
26    /// `None` means genuinely no schema is expected (empty/omitted input) — validation passes.
27    pub validation_type: Option<QName>,
28}
29
30impl std::fmt::Debug for DispatchEntry {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("DispatchEntry")
33            .field("auth_required", &self.auth_required)
34            .field("input_type", &self.input_type)
35            .field("validation_type", &self.validation_type)
36            .finish_non_exhaustive()
37    }
38}
39
40/// The runtime routing table: O(1) by body-element QName, with SOAPAction fallback.
41/// Built once at startup from a ResolvedWsdl; never mutated per-request.
42pub struct DispatchTable {
43    by_element: HashMap<QName, DispatchEntry>,
44    by_action: HashMap<String, DispatchEntry>,
45    /// Optional catch-all handler for operations not explicitly registered.
46    pub default_handler: Option<Arc<dyn SoapHandler>>,
47}
48
49impl DispatchTable {
50    /// Create an empty dispatch table (no operations, no default handler).
51    /// Used internally in multi-service mode as a placeholder.
52    pub fn empty() -> Self {
53        DispatchTable {
54            by_element: HashMap::new(),
55            by_action: HashMap::new(),
56            default_handler: None,
57        }
58    }
59}
60
61/// Errors that can occur while building the dispatch table (startup-time validation).
62#[derive(Debug, thiserror::Error)]
63pub enum DispatchError {
64    #[error("WSDL operation '{0}' has no registered handler")]
65    UnregisteredOperation(String),
66    #[error("Registered handler '{0}' has no matching operation in WSDL")]
67    UnknownOperation(String),
68    /// The operation references schema information (element/type) that is declared in the
69    /// loaded schema but cannot be resolved to a concrete type at build time.
70    /// Fail-closed: surfacing this at startup prevents silent skip of required validation.
71    #[error("Operation '{op}' input element '{element}' references type '{type_ref}' which is not in the type registry")]
72    UnresolvableInputType {
73        op: String,
74        element: String,
75        type_ref: String,
76    },
77}
78
79/// Build the dispatch table at startup.
80///
81/// `handlers` is keyed by operation name (e.g., "GetProfiles").
82/// `auth_bypass` is the set of operation names that skip authentication.
83/// `default_handler` is an optional catch-all used when no specific handler matches.
84///   When `Some`, operations without a registered handler are silently skipped (no error).
85///   When `None`, every WSDL operation must have a handler or `UnregisteredOperation` is returned.
86///
87/// Returns `Err(DispatchError::UnregisteredOperation)` if any WSDL operation has no handler and no default.
88/// Returns `Err(DispatchError::UnknownOperation)` if any registered handler has no WSDL operation.
89pub fn build_dispatch_table(
90    resolved: &ResolvedWsdl,
91    handlers: HashMap<String, Arc<dyn SoapHandler>>,
92    auth_bypass: &HashSet<String>,
93    default_handler: Option<Arc<dyn SoapHandler>>,
94) -> Result<DispatchTable, DispatchError> {
95    let unique_ops = collect_ops_for_service(None, resolved);
96    build_dispatch_table_from_ops(unique_ops, resolved, handlers, auth_bypass, default_handler)
97}
98
99/// Build a dispatch table for a single named service.
100///
101/// Only operations reachable through `service_name`'s ports and bindings are included.
102/// Useful when a WSDL defines multiple services and each service mounts on a distinct path.
103///
104/// Semantics are otherwise identical to `build_dispatch_table`.
105pub fn build_dispatch_table_for_service(
106    service_name: &str,
107    resolved: &ResolvedWsdl,
108    handlers: HashMap<String, Arc<dyn SoapHandler>>,
109    auth_bypass: &HashSet<String>,
110    default_handler: Option<Arc<dyn SoapHandler>>,
111) -> Result<DispatchTable, DispatchError> {
112    let unique_ops = collect_ops_for_service(Some(service_name), resolved);
113    build_dispatch_table_from_ops(unique_ops, resolved, handlers, auth_bypass, default_handler)
114}
115
116/// Collect unique operations for all services (service_name = None) or a specific service.
117/// Returns Vec of (op_name, soap_action, binding_style, rpc_namespace).
118fn collect_ops_for_service(
119    service_name: Option<&str>,
120    resolved: &ResolvedWsdl,
121) -> Vec<(String, String, BindingStyle, Option<String>)> {
122    let mut all_binding_ops: Vec<(String, String, BindingStyle, Option<String>)> = Vec::new();
123
124    let service_iter: Vec<&crate::wsdl::definitions::Service> = match service_name {
125        Some(name) => resolved.definition.services.get(name).into_iter().collect(),
126        None => resolved.definition.services.values().collect(),
127    };
128
129    for service in service_iter {
130        for port in &service.ports {
131            let binding_local = &port.binding.local_name;
132            if let Some(binding) = resolved.definition.bindings.get(binding_local) {
133                let style = binding.soap_binding.style.clone();
134                for binding_op in &binding.operations {
135                    let rpc_ns = if style == BindingStyle::Rpc {
136                        binding_op
137                            .input
138                            .body
139                            .namespace
140                            .clone()
141                            .or_else(|| Some(resolved.definition.target_namespace.clone()))
142                    } else {
143                        None
144                    };
145                    all_binding_ops.push((
146                        binding_op.name.clone(),
147                        binding_op.soap_action.clone(),
148                        style.clone(),
149                        rpc_ns,
150                    ));
151                }
152            }
153        }
154    }
155
156    // If no services defined (and collecting for all), fall back to all bindings directly.
157    if all_binding_ops.is_empty() && service_name.is_none() {
158        for binding in resolved.definition.bindings.values() {
159            let style = binding.soap_binding.style.clone();
160            for binding_op in &binding.operations {
161                let rpc_ns = if style == BindingStyle::Rpc {
162                    binding_op
163                        .input
164                        .body
165                        .namespace
166                        .clone()
167                        .or_else(|| Some(resolved.definition.target_namespace.clone()))
168                } else {
169                    None
170                };
171                all_binding_ops.push((
172                    binding_op.name.clone(),
173                    binding_op.soap_action.clone(),
174                    style.clone(),
175                    rpc_ns,
176                ));
177            }
178        }
179    }
180
181    // Deduplicate (same operation may appear in multiple ports/bindings).
182    let mut seen_ops: HashSet<String> = HashSet::new();
183    let mut unique_ops: Vec<(String, String, BindingStyle, Option<String>)> = Vec::new();
184    for (op_name, soap_action, style, rpc_ns) in all_binding_ops {
185        if seen_ops.insert(op_name.clone()) {
186            unique_ops.push((op_name, soap_action, style, rpc_ns));
187        }
188    }
189
190    unique_ops
191}
192
193/// Internal helper: build a DispatchTable from a pre-collected list of unique operations.
194fn build_dispatch_table_from_ops(
195    unique_ops: Vec<(String, String, BindingStyle, Option<String>)>,
196    resolved: &ResolvedWsdl,
197    handlers: HashMap<String, Arc<dyn SoapHandler>>,
198    auth_bypass: &HashSet<String>,
199    default_handler: Option<Arc<dyn SoapHandler>>,
200) -> Result<DispatchTable, DispatchError> {
201    let mut by_element: HashMap<QName, DispatchEntry> = HashMap::new();
202    let mut by_action: HashMap<String, DispatchEntry> = HashMap::new();
203
204    // Track which handler names are consumed (to detect handlers with no WSDL operation).
205    let mut consumed_handlers: HashSet<String> = HashSet::new();
206
207    for (op_name, soap_action, style, rpc_ns) in unique_ops {
208        let handler = match handlers.get(&op_name) {
209            Some(h) => h.clone(),
210            None => match &default_handler {
211                Some(d) => d.clone(),
212                None => return Err(DispatchError::UnregisteredOperation(op_name.clone())),
213            },
214        };
215
216        consumed_handlers.insert(op_name.clone());
217
218        let auth_required = !auth_bypass.contains(&op_name);
219
220        // Determine the input dispatch QName and validation type QName.
221        //
222        // RPC style: synthesize QName from (soap:body namespace or targetNamespace, operation name).
223        //   validation_type == input_type (registry has been populated for RPC types)
224        //
225        // Document/literal style: resolve from message part element reference.
226        //   input_type = element QName (used for routing)
227        //   validation_type = resolved via element_type_map:
228        //     - element has inline complexType → element QName (now registered in TypeRegistry)
229        //     - element has type= reference → named type QName
230        //     - element not in element_type_map (genuinely no schema) → None (skip validation)
231        //     - element in map but type not in registry → fail-closed build error
232        let (input_type, validation_type) = if style == BindingStyle::Rpc {
233            let ns = rpc_ns
234                .as_deref()
235                .unwrap_or(&resolved.definition.target_namespace);
236            let qn = Some(QName::new(ns, &op_name));
237            (qn.clone(), qn)
238        } else {
239            let elem_qname = resolve_input_element(resolved, &op_name);
240            let val_type = match &elem_qname {
241                None => None, // No element reference in WSDL — no schema expected.
242                Some(elem_qn) => {
243                    match resolved.element_type_map.get(elem_qn) {
244                        None => {
245                            // Element QName is not in the element_type_map: either the element is
246                            // declared in the schema with no type (empty/any — skip validation),
247                            // or the element is not in the schema at all (external type — skip).
248                            None
249                        }
250                        Some(type_qn) => {
251                            // Element is in the schema and maps to a type.
252                            // Fail-closed: if the type is not resolvable at this point, error.
253                            if resolved.type_registry.lookup(type_qn).is_none() {
254                                return Err(DispatchError::UnresolvableInputType {
255                                    op: op_name.clone(),
256                                    element: elem_qn.to_string(),
257                                    type_ref: type_qn.to_string(),
258                                });
259                            }
260                            Some(type_qn.clone())
261                        }
262                    }
263                }
264            };
265            (elem_qname, val_type)
266        };
267
268        let entry_for_element = DispatchEntry {
269            handler: handler.clone(),
270            auth_required,
271            input_type: input_type.clone(),
272            validation_type: validation_type.clone(),
273        };
274        let entry_for_action = DispatchEntry {
275            handler,
276            auth_required,
277            input_type,
278            validation_type,
279        };
280
281        // Index by input element QName.
282        if let Some(ref qn) = entry_for_element.input_type {
283            by_element.insert(qn.clone(), entry_for_element);
284        } else {
285            // No element reference — still insert by soap action so we can route by SOAPAction.
286            drop(entry_for_element);
287        }
288
289        // Index by SOAPAction (non-empty actions only).
290        if !soap_action.is_empty() {
291            by_action.insert(soap_action, entry_for_action);
292        }
293    }
294
295    // Verify no extra handlers were registered that have no WSDL operation.
296    for handler_name in handlers.keys() {
297        if !consumed_handlers.contains(handler_name) {
298            return Err(DispatchError::UnknownOperation(handler_name.clone()));
299        }
300    }
301
302    Ok(DispatchTable {
303        by_element,
304        by_action,
305        default_handler,
306    })
307}
308
309/// Resolve the input element QName for a named operation.
310/// Searches through port_types for an operation by name, then resolves its input message's
311/// first part element reference.
312fn resolve_input_element(resolved: &ResolvedWsdl, op_name: &str) -> Option<QName> {
313    // Search all port_types for this operation.
314    for port_type in resolved.definition.port_types.values() {
315        for op in &port_type.operations {
316            if op.name != op_name {
317                continue;
318            }
319            let input_msg_ref = op.input.as_ref()?;
320            // Resolve the message QName (local_name is the key in the messages map).
321            let msg_local = &input_msg_ref.message.local_name;
322            let message = resolved.definition.messages.get(msg_local)?;
323            // Take the element attribute of the first part (document/literal style).
324            let first_part = message.parts.first()?;
325            return first_part.element.clone();
326        }
327    }
328    None
329}
330
331/// Route an incoming request to its handler.
332///
333/// Tries `by_element` lookup first (document/literal dispatch by body first-child QName).
334/// Falls back to `by_action` if `soap_action` is provided and body-QName lookup fails.
335/// Returns `Err(SoapFault)` with `FaultCode::Sender` if no match is found.
336pub fn route<'a>(
337    table: &'a DispatchTable,
338    body_first_child_qname: &QName,
339    soap_action: Option<&str>,
340) -> Result<&'a DispatchEntry, SoapFault> {
341    // Primary: by element QName.
342    if let Some(entry) = table.by_element.get(body_first_child_qname) {
343        return Ok(entry);
344    }
345
346    // Fallback: by SOAPAction header.
347    if let Some(action) = soap_action {
348        if let Some(entry) = table.by_action.get(action) {
349            return Ok(entry);
350        }
351    }
352
353    Err(SoapFault::action_not_supported(
354        &body_first_child_qname.to_string(),
355    ))
356    // Note: default_handler is not reachable via route() — it is used at build_dispatch_table()
357    // time to fill entries for unregistered operations, so by dispatch time all entries exist.
358}
359
360/// XSD-11: Structural validation of the request body against the operation's input type.
361///
362/// Parses `body_bytes` as XML and checks that all elements with `min_occurs > 0` in the
363/// resolved ComplexType are present as children of the root element.
364///
365/// Returns `Ok(())` if:
366/// - `input_type` is `None` (no type information — skip validation)
367/// - `input_type` is `Some(qname)` but `qname` is not in the registry (unknown type — skip)
368/// - All required elements are present
369///
370/// Returns `Err(SoapFault::sender(...))` if a required child element is missing.
371pub fn validate_request(
372    body_bytes: &[u8],
373    type_registry: &TypeRegistry,
374    input_type: Option<&QName>,
375) -> Result<(), SoapFault> {
376    let Some(qname) = input_type else {
377        return Ok(());
378    };
379
380    let Some(xsd_type) = type_registry.lookup(qname) else {
381        return Ok(()); // Unknown type — skip validation (not an error)
382    };
383
384    // Extract required element names from the ComplexType.
385    let required_names = collect_required_element_names(xsd_type);
386    if required_names.is_empty() {
387        return Ok(());
388    }
389
390    // Parse body_bytes and collect the local names of direct children of the root element.
391    let present_children = parse_child_element_names(body_bytes)
392        .map_err(|e| SoapFault::sender(format!("Schema validation failed: malformed XML: {e}")))?;
393
394    for required in &required_names {
395        if !present_children.contains(required) {
396            return Err(SoapFault::sender(format!(
397                "Schema validation failed: required element '{required}' is missing"
398            )));
399        }
400    }
401
402    Ok(())
403}
404
405/// Collect local names of elements with min_occurs > 0 from the top-level content model.
406fn collect_required_element_names(xsd_type: &crate::xsd::types::XsdType) -> Vec<String> {
407    use crate::xsd::types::XsdType;
408    match xsd_type {
409        XsdType::Complex(ct) => collect_required_from_content(&ct.content),
410        XsdType::Simple(_) => vec![],
411    }
412}
413
414fn collect_required_from_content(content: &ComplexContent) -> Vec<String> {
415    let elements = match content {
416        ComplexContent::Sequence(els) | ComplexContent::All(els) => els,
417        // xs:choice: individual children are NOT independently required — the choice
418        // as a whole may be required (minOccurs > 0 on the choice group itself), but
419        // a valid request only provides ONE branch. Returning an empty required list
420        // prevents the validator from demanding all branches simultaneously.
421        ComplexContent::Choice(_) => return vec![],
422        ComplexContent::ComplexExtension { content, .. } => {
423            return collect_required_from_content(content)
424        }
425        ComplexContent::ComplexRestriction { content, .. } => {
426            return collect_required_from_content(content)
427        }
428        ComplexContent::Empty | ComplexContent::SimpleContent(_) => return vec![],
429    };
430
431    elements
432        .iter()
433        .filter_map(|el| {
434            if el.min_occurs > 0 {
435                el.name.clone()
436            } else {
437                None
438            }
439        })
440        .collect()
441}
442
443/// Use quick-xml to parse body_bytes and return the local names of direct children
444/// of the root element (one level deep).
445fn parse_child_element_names(body_bytes: &[u8]) -> Result<HashSet<String>, String> {
446    use quick_xml::events::Event;
447    use quick_xml::Reader;
448
449    let mut reader = Reader::from_reader(body_bytes);
450    reader.config_mut().trim_text(true);
451
452    let mut depth: u32 = 0;
453    let mut children: HashSet<String> = HashSet::new();
454
455    loop {
456        match reader.read_event() {
457            Ok(Event::Start(e)) => {
458                depth += 1;
459                if depth == 2 {
460                    // Direct child of root element
461                    let local_name = e.local_name();
462                    let name = std::str::from_utf8(local_name.as_ref())
463                        .map_err(|e| e.to_string())?
464                        .to_string();
465                    children.insert(name);
466                }
467            }
468            Ok(Event::Empty(e)) => {
469                if depth == 1 {
470                    // Self-closing direct child of root
471                    let local_name = e.local_name();
472                    let name = std::str::from_utf8(local_name.as_ref())
473                        .map_err(|e| e.to_string())?
474                        .to_string();
475                    children.insert(name);
476                }
477            }
478            Ok(Event::End(_)) => {
479                if depth == 0 {
480                    break;
481                }
482                depth -= 1;
483            }
484            Ok(Event::Eof) => break,
485            Err(e) => return Err(e.to_string()),
486            _ => {}
487        }
488    }
489
490    Ok(children)
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::fault::{FaultCode, SoapFault};
497    use crate::handler::SoapHandler;
498    use crate::qname::QName;
499    use crate::wsdl::definitions::*;
500    use crate::wsdl::resolver::ResolvedWsdl;
501    use crate::xsd::elements::XsdElement;
502    use crate::xsd::types::{ComplexContent, ComplexType, TypeRegistry, XsdType};
503    use async_trait::async_trait;
504    use bytes::Bytes;
505    use std::sync::Arc;
506
507    // ── Test handler ──────────────────────────────────────────────────────────
508
509    struct MockHandler {
510        name: &'static str,
511    }
512
513    #[async_trait]
514    impl SoapHandler for MockHandler {
515        async fn handle(&self, _body: Bytes) -> Result<Bytes, SoapFault> {
516            Ok(Bytes::from(format!("<response>{}</response>", self.name)))
517        }
518    }
519
520    fn mock_handler(name: &'static str) -> Arc<dyn SoapHandler> {
521        Arc::new(MockHandler { name })
522    }
523
524    // ── Minimal ResolvedWsdl builder ──────────────────────────────────────────
525
526    /// Build a minimal ResolvedWsdl with one service, one port, one binding, one operation.
527    fn make_resolved_wsdl(
528        op_name: &str,
529        soap_action: &str,
530        input_element_qname: Option<QName>,
531    ) -> ResolvedWsdl {
532        let target_ns = "http://example.com/service".to_string();
533        let msg_name = format!("{op_name}Request");
534
535        // Message: one part with the element reference
536        let part = MessagePart {
537            name: "parameters".to_string(),
538            element: input_element_qname,
539            type_ref: None,
540        };
541        let message = Message {
542            name: msg_name.clone(),
543            parts: vec![part],
544        };
545
546        // PortType operation
547        let pt_op = Operation {
548            name: op_name.to_string(),
549            input: Some(OperationMessage {
550                name: None,
551                message: QName::local(&msg_name),
552            }),
553            output: None,
554            faults: vec![],
555            style: OperationStyle::RequestResponse,
556        };
557        let port_type = PortType {
558            name: "TestPortType".to_string(),
559            operations: vec![pt_op],
560        };
561
562        // Binding operation
563        let binding_op = BindingOperation {
564            name: op_name.to_string(),
565            soap_action: soap_action.to_string(),
566            input: BindingMessage {
567                body: SoapBody {
568                    use_attr: UseStyle::Literal,
569                    namespace: None,
570                    encoding_style: None,
571                },
572                headers: vec![],
573            },
574            output: BindingMessage {
575                body: SoapBody {
576                    use_attr: UseStyle::Literal,
577                    namespace: None,
578                    encoding_style: None,
579                },
580                headers: vec![],
581            },
582        };
583        let binding = Binding {
584            name: "TestBinding".to_string(),
585            port_type: QName::local("TestPortType"),
586            soap_binding: SoapBinding {
587                style: BindingStyle::Document,
588                transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
589                soap_version: SoapVersion::Soap12,
590            },
591            operations: vec![binding_op],
592        };
593
594        // Service + Port
595        let port = Port {
596            name: "TestPort".to_string(),
597            binding: QName::local("TestBinding"),
598            address: "http://localhost/service".to_string(),
599        };
600        let service = Service {
601            name: "TestService".to_string(),
602            ports: vec![port],
603        };
604
605        let mut messages = HashMap::new();
606        messages.insert(msg_name, message);
607        let mut port_types = HashMap::new();
608        port_types.insert("TestPortType".to_string(), port_type);
609        let mut bindings = HashMap::new();
610        bindings.insert("TestBinding".to_string(), binding);
611        let mut services = HashMap::new();
612        services.insert("TestService".to_string(), service);
613
614        let definition = WsdlDefinition {
615            target_namespace: target_ns,
616            imports: vec![],
617            types: TypesSection::default(),
618            messages,
619            port_types,
620            bindings,
621            services,
622        };
623
624        ResolvedWsdl {
625            definition,
626            type_registry: TypeRegistry::new(),
627            element_type_map: HashMap::new(),
628            raw_bytes: vec![],
629        }
630    }
631
632    // ── build_dispatch_table tests ────────────────────────────────────────────
633
634    #[test]
635    fn dispatch_table_routes_by_element_qname() {
636        let elem_qname = QName::new("http://example.com", "GetProfiles");
637        let resolved =
638            make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
639
640        let mut handlers = HashMap::new();
641        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
642
643        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
644        let entry = route(&table, &elem_qname, None).unwrap();
645
646        // Handler is present (we can verify by running it synchronously via block_on equivalent)
647        assert!(entry.auth_required); // default: auth required
648    }
649
650    #[test]
651    fn dispatch_table_unregistered_operation_fails_at_build() {
652        let elem_qname = QName::new("http://example.com", "GetProfiles");
653        let resolved = make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname));
654
655        // No handlers provided at all — should fail at startup
656        let handlers: HashMap<String, Arc<dyn SoapHandler>> = HashMap::new();
657        let result = build_dispatch_table(&resolved, handlers, &HashSet::new(), None);
658
659        assert!(
660            matches!(result, Err(DispatchError::UnregisteredOperation(ref name)) if name == "GetProfiles")
661        );
662    }
663
664    #[test]
665    fn dispatch_table_unknown_handler_name_fails_at_build() {
666        let elem_qname = QName::new("http://example.com", "GetProfiles");
667        let resolved = make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname));
668
669        let mut handlers = HashMap::new();
670        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
671        handlers.insert("NonExistentOp".to_string(), mock_handler("ghost")); // no WSDL op
672
673        let result = build_dispatch_table(&resolved, handlers, &HashSet::new(), None);
674        assert!(
675            matches!(result, Err(DispatchError::UnknownOperation(ref name)) if name == "NonExistentOp")
676        );
677    }
678
679    #[test]
680    fn route_unknown_qname_no_soap_action_returns_sender_fault() {
681        let elem_qname = QName::new("http://example.com", "GetProfiles");
682        let resolved =
683            make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
684
685        let mut handlers = HashMap::new();
686        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
687        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
688
689        let unknown = QName::new("http://example.com", "UnknownOperation");
690        let result = route(&table, &unknown, None);
691
692        assert!(result.is_err());
693        let fault = result.unwrap_err();
694        assert_eq!(fault.code, FaultCode::Sender);
695        assert!(
696            fault.reason.to_lowercase().contains("action")
697                || fault.reason.to_lowercase().contains("supported")
698        );
699    }
700
701    #[test]
702    fn route_falls_back_to_soap_action_on_unknown_qname() {
703        let elem_qname = QName::new("http://example.com", "GetProfiles");
704        let resolved =
705            make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
706
707        let mut handlers = HashMap::new();
708        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
709        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
710
711        // Unknown body QName but known SOAPAction
712        let unknown = QName::local("SomethingElse");
713        let result = route(&table, &unknown, Some("urn:GetProfiles"));
714        assert!(result.is_ok(), "Expected fallback to SOAPAction to succeed");
715    }
716
717    #[test]
718    fn route_unknown_qname_unknown_soap_action_returns_fault() {
719        let elem_qname = QName::new("http://example.com", "GetProfiles");
720        let resolved =
721            make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
722
723        let mut handlers = HashMap::new();
724        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
725        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
726
727        let unknown = QName::local("SomethingElse");
728        let result = route(&table, &unknown, Some("urn:UnknownAction"));
729        assert!(result.is_err());
730        assert_eq!(result.unwrap_err().code, FaultCode::Sender);
731    }
732
733    #[test]
734    fn auth_bypass_marks_entry_auth_not_required() {
735        let elem_qname = QName::new("http://example.com", "GetProfiles");
736        let resolved =
737            make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
738
739        let mut handlers = HashMap::new();
740        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
741
742        let mut bypass = HashSet::new();
743        bypass.insert("GetProfiles".to_string());
744
745        let table = build_dispatch_table(&resolved, handlers, &bypass, None).unwrap();
746        let entry = route(&table, &elem_qname, None).unwrap();
747        assert!(!entry.auth_required); // bypassed — no auth required
748    }
749
750    #[test]
751    fn auth_required_by_default_for_non_bypass_op() {
752        let elem_qname = QName::new("http://example.com", "SetSomething");
753        let resolved =
754            make_resolved_wsdl("SetSomething", "urn:SetSomething", Some(elem_qname.clone()));
755
756        let mut handlers = HashMap::new();
757        handlers.insert("SetSomething".to_string(), mock_handler("SetSomething"));
758
759        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
760        let entry = route(&table, &elem_qname, None).unwrap();
761        assert!(entry.auth_required);
762    }
763
764    // ── validate_request tests ────────────────────────────────────────────────
765
766    fn make_type_registry_with_required_field(
767        type_qname: &QName,
768        required_field: &str,
769    ) -> TypeRegistry {
770        let element = XsdElement {
771            name: Some(required_field.to_string()),
772            min_occurs: 1,
773            ..Default::default()
774        };
775        let ct = ComplexType {
776            name: Some(type_qname.local_name.clone()),
777            content: ComplexContent::Sequence(vec![element]),
778            attributes: vec![],
779        };
780        let mut reg = TypeRegistry::new();
781        reg.insert(type_qname.clone(), XsdType::Complex(Box::new(ct)));
782        reg
783    }
784
785    #[test]
786    fn validate_request_no_input_type_returns_ok() {
787        let reg = TypeRegistry::new();
788        let result = validate_request(b"<GetProfiles/>", &reg, None);
789        assert!(result.is_ok());
790    }
791
792    #[test]
793    fn validate_request_unknown_type_in_registry_returns_ok() {
794        let reg = TypeRegistry::new(); // empty — unknown type
795        let qname = QName::new("http://example.com", "GetProfilesRequest");
796        let result = validate_request(b"<GetProfiles/>", &reg, Some(&qname));
797        assert!(result.is_ok());
798    }
799
800    #[test]
801    fn validate_request_valid_xml_with_required_element_returns_ok() {
802        let qname = QName::new("http://example.com", "GetProfilesRequest");
803        let reg = make_type_registry_with_required_field(&qname, "Token");
804
805        let body = b"<GetProfiles xmlns=\"http://example.com\"><Token>1234</Token></GetProfiles>";
806        let result = validate_request(body, &reg, Some(&qname));
807        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
808    }
809
810    #[test]
811    fn validate_request_missing_required_element_returns_sender_fault() {
812        let qname = QName::new("http://example.com", "GetProfilesRequest");
813        let reg = make_type_registry_with_required_field(&qname, "Token");
814
815        // Body has no <Token> child
816        let body = b"<GetProfiles xmlns=\"http://example.com\"></GetProfiles>";
817        let result = validate_request(body, &reg, Some(&qname));
818        assert!(result.is_err());
819        let fault = result.unwrap_err();
820        assert_eq!(fault.code, FaultCode::Sender);
821        assert!(fault.reason.contains("Schema validation failed"));
822        assert!(fault.reason.contains("Token"));
823    }
824
825    #[test]
826    fn validate_request_optional_element_missing_returns_ok() {
827        let qname = QName::new("http://example.com", "GetProfilesRequest");
828        let optional_el = XsdElement {
829            name: Some("OptionalField".to_string()),
830            min_occurs: 0,
831            ..Default::default()
832        };
833        let ct = ComplexType {
834            name: Some("GetProfilesRequest".to_string()),
835            content: ComplexContent::Sequence(vec![optional_el]),
836            attributes: vec![],
837        };
838        let mut reg = TypeRegistry::new();
839        reg.insert(qname.clone(), XsdType::Complex(Box::new(ct)));
840
841        let body = b"<GetProfiles xmlns=\"http://example.com\"></GetProfiles>";
842        let result = validate_request(body, &reg, Some(&qname));
843        assert!(result.is_ok());
844    }
845
846    #[test]
847    fn handler_name_matches_wsdl_operation_name_get_profiles() {
848        // Regression: "GetProfiles" key in handlers must match "GetProfiles" operation in WSDL
849        let elem_qname = QName::new("http://onvif.org/ver10/media/wsdl", "GetProfiles");
850        let resolved = make_resolved_wsdl(
851            "GetProfiles",
852            "http://www.onvif.org/ver10/media/wsdl/GetProfiles",
853            Some(elem_qname.clone()),
854        );
855
856        let mut handlers = HashMap::new();
857        handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
858
859        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
860        let entry = route(&table, &elem_qname, None).unwrap();
861        assert!(entry.auth_required);
862    }
863
864    // ── RPC binding tests ─────────────────────────────────────────────────────
865
866    /// Build a minimal ResolvedWsdl with RPC-style binding.
867    fn make_resolved_wsdl_rpc(
868        op_name: &str,
869        soap_action: &str,
870        rpc_namespace: Option<&str>,
871    ) -> ResolvedWsdl {
872        let target_ns = "http://example.com/service".to_string();
873        let msg_name = format!("{op_name}Request");
874
875        // Message: one part with type_ref (RPC style — no element ref)
876        let part = MessagePart {
877            name: "parameters".to_string(),
878            element: None,
879            type_ref: Some(QName::new("http://www.w3.org/2001/XMLSchema", "string")),
880        };
881        let message = Message {
882            name: msg_name.clone(),
883            parts: vec![part],
884        };
885
886        // PortType operation
887        let pt_op = Operation {
888            name: op_name.to_string(),
889            input: Some(OperationMessage {
890                name: None,
891                message: QName::local(&msg_name),
892            }),
893            output: None,
894            faults: vec![],
895            style: OperationStyle::RequestResponse,
896        };
897        let port_type = PortType {
898            name: "TestPortType".to_string(),
899            operations: vec![pt_op],
900        };
901
902        // Binding operation with RPC-specific namespace
903        let binding_op = BindingOperation {
904            name: op_name.to_string(),
905            soap_action: soap_action.to_string(),
906            input: BindingMessage {
907                body: SoapBody {
908                    use_attr: UseStyle::Encoded,
909                    namespace: rpc_namespace.map(|s| s.to_string()),
910                    encoding_style: None,
911                },
912                headers: vec![],
913            },
914            output: BindingMessage {
915                body: SoapBody {
916                    use_attr: UseStyle::Encoded,
917                    namespace: rpc_namespace.map(|s| s.to_string()),
918                    encoding_style: None,
919                },
920                headers: vec![],
921            },
922        };
923        let binding = Binding {
924            name: "TestBinding".to_string(),
925            port_type: QName::local("TestPortType"),
926            soap_binding: SoapBinding {
927                style: BindingStyle::Rpc,
928                transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
929                soap_version: SoapVersion::Soap12,
930            },
931            operations: vec![binding_op],
932        };
933
934        // Service + Port
935        let port = Port {
936            name: "TestPort".to_string(),
937            binding: QName::local("TestBinding"),
938            address: "http://localhost/service".to_string(),
939        };
940        let service = Service {
941            name: "TestService".to_string(),
942            ports: vec![port],
943        };
944
945        let mut messages = HashMap::new();
946        messages.insert(msg_name, message);
947        let mut port_types = HashMap::new();
948        port_types.insert("TestPortType".to_string(), port_type);
949        let mut bindings = HashMap::new();
950        bindings.insert("TestBinding".to_string(), binding);
951        let mut services = HashMap::new();
952        services.insert("TestService".to_string(), service);
953
954        let definition = WsdlDefinition {
955            target_namespace: target_ns,
956            imports: vec![],
957            types: TypesSection::default(),
958            messages,
959            port_types,
960            bindings,
961            services,
962        };
963
964        ResolvedWsdl {
965            definition,
966            type_registry: crate::xsd::types::TypeRegistry::new(),
967            element_type_map: HashMap::new(),
968            raw_bytes: vec![],
969        }
970    }
971
972    /// Build a ResolvedWsdl with two services (ServiceA and ServiceB), each with their own
973    /// binding and operations.
974    fn make_resolved_wsdl_two_services() -> ResolvedWsdl {
975        let target_ns = "http://example.com/multi".to_string();
976
977        // Messages
978        let msg_a = Message {
979            name: "OpARequest".to_string(),
980            parts: vec![MessagePart {
981                name: "parameters".to_string(),
982                element: Some(QName::new("http://example.com/multi", "OpA")),
983                type_ref: None,
984            }],
985        };
986        let msg_b = Message {
987            name: "OpBRequest".to_string(),
988            parts: vec![MessagePart {
989                name: "parameters".to_string(),
990                element: Some(QName::new("http://example.com/multi", "OpB")),
991                type_ref: None,
992            }],
993        };
994
995        // PortTypes
996        let pt_a = PortType {
997            name: "PortTypeA".to_string(),
998            operations: vec![Operation {
999                name: "OpA".to_string(),
1000                input: Some(OperationMessage {
1001                    name: None,
1002                    message: QName::local("OpARequest"),
1003                }),
1004                output: None,
1005                faults: vec![],
1006                style: OperationStyle::RequestResponse,
1007            }],
1008        };
1009        let pt_b = PortType {
1010            name: "PortTypeB".to_string(),
1011            operations: vec![Operation {
1012                name: "OpB".to_string(),
1013                input: Some(OperationMessage {
1014                    name: None,
1015                    message: QName::local("OpBRequest"),
1016                }),
1017                output: None,
1018                faults: vec![],
1019                style: OperationStyle::RequestResponse,
1020            }],
1021        };
1022
1023        // Bindings
1024        let binding_a = Binding {
1025            name: "BindingA".to_string(),
1026            port_type: QName::local("PortTypeA"),
1027            soap_binding: SoapBinding {
1028                style: BindingStyle::Document,
1029                transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
1030                soap_version: SoapVersion::Soap12,
1031            },
1032            operations: vec![BindingOperation {
1033                name: "OpA".to_string(),
1034                soap_action: "urn:OpA".to_string(),
1035                input: BindingMessage {
1036                    body: SoapBody {
1037                        use_attr: UseStyle::Literal,
1038                        namespace: None,
1039                        encoding_style: None,
1040                    },
1041                    headers: vec![],
1042                },
1043                output: BindingMessage {
1044                    body: SoapBody {
1045                        use_attr: UseStyle::Literal,
1046                        namespace: None,
1047                        encoding_style: None,
1048                    },
1049                    headers: vec![],
1050                },
1051            }],
1052        };
1053        let binding_b = Binding {
1054            name: "BindingB".to_string(),
1055            port_type: QName::local("PortTypeB"),
1056            soap_binding: SoapBinding {
1057                style: BindingStyle::Document,
1058                transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
1059                soap_version: SoapVersion::Soap12,
1060            },
1061            operations: vec![BindingOperation {
1062                name: "OpB".to_string(),
1063                soap_action: "urn:OpB".to_string(),
1064                input: BindingMessage {
1065                    body: SoapBody {
1066                        use_attr: UseStyle::Literal,
1067                        namespace: None,
1068                        encoding_style: None,
1069                    },
1070                    headers: vec![],
1071                },
1072                output: BindingMessage {
1073                    body: SoapBody {
1074                        use_attr: UseStyle::Literal,
1075                        namespace: None,
1076                        encoding_style: None,
1077                    },
1078                    headers: vec![],
1079                },
1080            }],
1081        };
1082
1083        // Services
1084        let service_a = Service {
1085            name: "ServiceA".to_string(),
1086            ports: vec![Port {
1087                name: "PortA".to_string(),
1088                binding: QName::local("BindingA"),
1089                address: "http://localhost/soap/a".to_string(),
1090            }],
1091        };
1092        let service_b = Service {
1093            name: "ServiceB".to_string(),
1094            ports: vec![Port {
1095                name: "PortB".to_string(),
1096                binding: QName::local("BindingB"),
1097                address: "http://localhost/soap/b".to_string(),
1098            }],
1099        };
1100
1101        let mut messages = HashMap::new();
1102        messages.insert("OpARequest".to_string(), msg_a);
1103        messages.insert("OpBRequest".to_string(), msg_b);
1104        let mut port_types = HashMap::new();
1105        port_types.insert("PortTypeA".to_string(), pt_a);
1106        port_types.insert("PortTypeB".to_string(), pt_b);
1107        let mut bindings = HashMap::new();
1108        bindings.insert("BindingA".to_string(), binding_a);
1109        bindings.insert("BindingB".to_string(), binding_b);
1110        let mut services = HashMap::new();
1111        services.insert("ServiceA".to_string(), service_a);
1112        services.insert("ServiceB".to_string(), service_b);
1113
1114        let definition = WsdlDefinition {
1115            target_namespace: target_ns,
1116            imports: vec![],
1117            types: TypesSection::default(),
1118            messages,
1119            port_types,
1120            bindings,
1121            services,
1122        };
1123
1124        ResolvedWsdl {
1125            definition,
1126            type_registry: crate::xsd::types::TypeRegistry::new(),
1127            element_type_map: HashMap::new(),
1128            raw_bytes: vec![],
1129        }
1130    }
1131
1132    #[test]
1133    fn build_dispatch_table_rpc_binding() {
1134        let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", Some("http://example.com/svc"));
1135
1136        let mut handlers = HashMap::new();
1137        handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
1138
1139        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
1140
1141        // RPC dispatch key: (soap:body namespace, operation name)
1142        let rpc_qname = QName::new("http://example.com/svc", "GetOp");
1143        let result = route(&table, &rpc_qname, None);
1144        assert!(
1145            result.is_ok(),
1146            "Expected RPC QName to route successfully, got: {result:?}"
1147        );
1148    }
1149
1150    #[test]
1151    fn rpc_dispatch_by_wrapper_element() {
1152        let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", Some("http://example.com/svc"));
1153
1154        let mut handlers = HashMap::new();
1155        handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
1156
1157        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
1158
1159        // Confirm the entry is in by_element keyed on the synthesized QName
1160        let rpc_qname = QName::new("http://example.com/svc", "GetOp");
1161        let result = route(&table, &rpc_qname, None);
1162        assert!(
1163            result.is_ok(),
1164            "route by synthesized RPC QName should return Ok"
1165        );
1166    }
1167
1168    #[test]
1169    fn build_dispatch_table_rpc_missing_namespace_falls_back_to_target_ns() {
1170        // When soap:body.namespace is None for an RPC binding, fall back to WSDL targetNamespace
1171        let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", None);
1172        let target_ns = resolved.definition.target_namespace.clone();
1173
1174        let mut handlers = HashMap::new();
1175        handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
1176
1177        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
1178
1179        // Should be keyed by (targetNamespace, opName)
1180        let fallback_qname = QName::new(&target_ns, "GetOp");
1181        let result = route(&table, &fallback_qname, None);
1182        assert!(
1183            result.is_ok(),
1184            "RPC without namespace should fall back to targetNamespace, got: {result:?}"
1185        );
1186    }
1187
1188    #[test]
1189    fn build_dispatch_table_for_service_isolates_operations() {
1190        let resolved = make_resolved_wsdl_two_services();
1191
1192        let op_a_qname = QName::new("http://example.com/multi", "OpA");
1193        let op_b_qname = QName::new("http://example.com/multi", "OpB");
1194
1195        let mut handlers_a = HashMap::new();
1196        handlers_a.insert("OpA".to_string(), mock_handler("OpA"));
1197
1198        let mut handlers_b = HashMap::new();
1199        handlers_b.insert("OpB".to_string(), mock_handler("OpB"));
1200
1201        let table_a = build_dispatch_table_for_service(
1202            "ServiceA",
1203            &resolved,
1204            handlers_a,
1205            &HashSet::new(),
1206            None,
1207        )
1208        .unwrap();
1209        let table_b = build_dispatch_table_for_service(
1210            "ServiceB",
1211            &resolved,
1212            handlers_b,
1213            &HashSet::new(),
1214            None,
1215        )
1216        .unwrap();
1217
1218        // ServiceA table contains OpA
1219        assert!(
1220            route(&table_a, &op_a_qname, None).is_ok(),
1221            "ServiceA should route OpA"
1222        );
1223        // ServiceB table contains OpB
1224        assert!(
1225            route(&table_b, &op_b_qname, None).is_ok(),
1226            "ServiceB should route OpB"
1227        );
1228        // ServiceA table does NOT contain OpB
1229        assert!(
1230            route(&table_a, &op_b_qname, None).is_err(),
1231            "ServiceA should NOT route OpB"
1232        );
1233    }
1234
1235    // ── Finding #3: document/literal element validation tests ────────────────
1236
1237    /// WSDL with a document/literal operation whose input element has an INLINE complexType
1238    /// containing a REQUIRED child element.
1239    const INLINE_ELEMENT_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1240<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
1241  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
1242  xmlns:tns="http://example.com/svc"
1243  xmlns:xs="http://www.w3.org/2001/XMLSchema"
1244  targetNamespace="http://example.com/svc">
1245  <types>
1246    <xs:schema targetNamespace="http://example.com/svc" xmlns:xs="http://www.w3.org/2001/XMLSchema">
1247      <xs:element name="GetFoo">
1248        <xs:complexType>
1249          <xs:sequence>
1250            <xs:element name="RequiredField" type="xs:string" minOccurs="1"/>
1251            <xs:element name="OptionalField" type="xs:string" minOccurs="0"/>
1252          </xs:sequence>
1253        </xs:complexType>
1254      </xs:element>
1255      <xs:element name="GetFooResponse">
1256        <xs:complexType><xs:sequence/></xs:complexType>
1257      </xs:element>
1258    </xs:schema>
1259  </types>
1260  <message name="GetFooRequest"><part name="parameters" element="tns:GetFoo"/></message>
1261  <message name="GetFooResponse"><part name="parameters" element="tns:GetFooResponse"/></message>
1262  <portType name="SvcPortType">
1263    <operation name="GetFoo">
1264      <input message="tns:GetFooRequest"/>
1265      <output message="tns:GetFooResponse"/>
1266    </operation>
1267  </portType>
1268  <binding name="SvcBinding" type="tns:SvcPortType">
1269    <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
1270    <operation name="GetFoo">
1271      <soap12:operation soapAction="urn:GetFoo"/>
1272      <input><soap12:body use="literal"/></input>
1273      <output><soap12:body use="literal"/></output>
1274    </operation>
1275  </binding>
1276  <service name="SvcService">
1277    <port name="SvcPort" binding="tns:SvcBinding">
1278      <soap12:address location="http://localhost/svc"/>
1279    </port>
1280  </service>
1281</definitions>"#;
1282
1283    /// WSDL with a document/literal operation whose input element uses a named type reference.
1284    const NAMED_TYPE_REF_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1285<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
1286  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
1287  xmlns:tns="http://example.com/svc"
1288  xmlns:xs="http://www.w3.org/2001/XMLSchema"
1289  targetNamespace="http://example.com/svc">
1290  <types>
1291    <xs:schema targetNamespace="http://example.com/svc" xmlns:xs="http://www.w3.org/2001/XMLSchema">
1292      <xs:complexType name="GetBarType">
1293        <xs:sequence>
1294          <xs:element name="BarRequired" type="xs:string" minOccurs="1"/>
1295        </xs:sequence>
1296      </xs:complexType>
1297      <xs:element name="GetBar" type="tns:GetBarType"/>
1298      <xs:element name="GetBarResponse">
1299        <xs:complexType><xs:sequence/></xs:complexType>
1300      </xs:element>
1301    </xs:schema>
1302  </types>
1303  <message name="GetBarRequest"><part name="parameters" element="tns:GetBar"/></message>
1304  <message name="GetBarResponse"><part name="parameters" element="tns:GetBarResponse"/></message>
1305  <portType name="SvcPortType">
1306    <operation name="GetBar">
1307      <input message="tns:GetBarRequest"/>
1308      <output message="tns:GetBarResponse"/>
1309    </operation>
1310  </portType>
1311  <binding name="SvcBinding" type="tns:SvcPortType">
1312    <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
1313    <operation name="GetBar">
1314      <soap12:operation soapAction="urn:GetBar"/>
1315      <input><soap12:body use="literal"/></input>
1316      <output><soap12:body use="literal"/></output>
1317    </operation>
1318  </binding>
1319  <service name="SvcService">
1320    <port name="SvcPort" binding="tns:SvcBinding">
1321      <soap12:address location="http://localhost/svc"/>
1322    </port>
1323  </service>
1324</definitions>"#;
1325
1326    fn resolve_test_wsdl(wsdl: &str) -> ResolvedWsdl {
1327        use crate::wsdl::parser::WsdlError;
1328        use crate::wsdl::resolver::{resolve_wsdl, WsdlLoader};
1329        struct NullLoader;
1330        impl WsdlLoader for NullLoader {
1331            fn load(&self, loc: &str) -> Result<Vec<u8>, WsdlError> {
1332                Err(WsdlError::MalformedXml(format!("NullLoader: {loc}")))
1333            }
1334        }
1335        let mut visited = HashSet::new();
1336        resolve_wsdl(wsdl.as_bytes(), &NullLoader, &mut visited)
1337            .expect("WSDL resolution should succeed")
1338    }
1339
1340    /// Document/literal op with inline complexType: request missing required field must be REJECTED.
1341    #[test]
1342    fn doc_literal_inline_type_missing_required_field_rejected() {
1343        let resolved = resolve_test_wsdl(INLINE_ELEMENT_WSDL);
1344
1345        let elem_qname = QName::new("http://example.com/svc", "GetFoo");
1346
1347        // The validation_type for GetFoo should now be the element QName itself
1348        // (because we register inline element types under element QName in TypeRegistry).
1349        assert!(
1350            resolved.type_registry.lookup(&elem_qname).is_some(),
1351            "Inline element type must be in TypeRegistry after resolve_wsdl"
1352        );
1353
1354        // Build a minimal table and check validation rejects missing required field.
1355        let mut handlers = HashMap::new();
1356        handlers.insert("GetFoo".to_string(), mock_handler("GetFoo"));
1357        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
1358
1359        let entry = route(&table, &elem_qname, None).unwrap();
1360        // validation_type must be set (not None) for validation to work
1361        assert!(
1362            entry.validation_type.is_some(),
1363            "validation_type must be Some for document/literal op with inline type"
1364        );
1365
1366        // Request omitting RequiredField must be rejected
1367        let body_missing = br#"<tns:GetFoo xmlns:tns="http://example.com/svc"/>"#;
1368        let result = validate_request(
1369            body_missing,
1370            &resolved.type_registry,
1371            entry.validation_type.as_ref(),
1372        );
1373        assert!(
1374            result.is_err(),
1375            "Request missing RequiredField must produce a validation error"
1376        );
1377        let fault = result.unwrap_err();
1378        assert_eq!(fault.code, crate::fault::FaultCode::Sender);
1379        assert!(
1380            fault.reason.contains("RequiredField"),
1381            "Fault reason must mention the missing field, got: {}",
1382            fault.reason
1383        );
1384
1385        // Request including RequiredField must pass
1386        let body_ok = br#"<tns:GetFoo xmlns:tns="http://example.com/svc"><tns:RequiredField>val</tns:RequiredField></tns:GetFoo>"#;
1387        let result_ok = validate_request(
1388            body_ok,
1389            &resolved.type_registry,
1390            entry.validation_type.as_ref(),
1391        );
1392        assert!(
1393            result_ok.is_ok(),
1394            "Request with RequiredField must pass validation, got: {result_ok:?}"
1395        );
1396    }
1397
1398    /// Document/literal op with named type reference: missing required field must also be REJECTED.
1399    #[test]
1400    fn doc_literal_named_type_ref_missing_required_field_rejected() {
1401        let resolved = resolve_test_wsdl(NAMED_TYPE_REF_WSDL);
1402
1403        let elem_qname = QName::new("http://example.com/svc", "GetBar");
1404        let type_qname = QName::new("http://example.com/svc", "GetBarType");
1405
1406        // The named type must be in registry.
1407        assert!(
1408            resolved.type_registry.lookup(&type_qname).is_some(),
1409            "Named type GetBarType must be in TypeRegistry"
1410        );
1411
1412        // Build table — element_type_map for GetBar should point to GetBarType.
1413        let mut handlers = HashMap::new();
1414        handlers.insert("GetBar".to_string(), mock_handler("GetBar"));
1415        let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
1416
1417        let entry = route(&table, &elem_qname, None).unwrap();
1418        assert!(
1419            entry.validation_type.is_some(),
1420            "validation_type must be Some for document/literal op with named type ref"
1421        );
1422        // validation_type should be GetBarType, not GetBar
1423        assert_eq!(
1424            entry.validation_type.as_ref().unwrap(),
1425            &type_qname,
1426            "validation_type must be the named type QName, not the element QName"
1427        );
1428
1429        // Request omitting BarRequired must be rejected
1430        let body_missing = br#"<tns:GetBar xmlns:tns="http://example.com/svc"/>"#;
1431        let result = validate_request(
1432            body_missing,
1433            &resolved.type_registry,
1434            entry.validation_type.as_ref(),
1435        );
1436        assert!(
1437            result.is_err(),
1438            "Request missing BarRequired must produce a validation error"
1439        );
1440        let fault = result.unwrap_err();
1441        assert_eq!(fault.code, crate::fault::FaultCode::Sender);
1442        assert!(
1443            fault.reason.contains("BarRequired"),
1444            "Fault reason must mention the missing field, got: {}",
1445            fault.reason
1446        );
1447
1448        // Request including BarRequired must pass
1449        let body_ok = br#"<tns:GetBar xmlns:tns="http://example.com/svc"><tns:BarRequired>val</tns:BarRequired></tns:GetBar>"#;
1450        let result_ok = validate_request(
1451            body_ok,
1452            &resolved.type_registry,
1453            entry.validation_type.as_ref(),
1454        );
1455        assert!(
1456            result_ok.is_ok(),
1457            "Request with BarRequired must pass validation, got: {result_ok:?}"
1458        );
1459    }
1460}