use crate::fault::SoapFault;
use crate::handler::SoapHandler;
use crate::qname::QName;
use crate::wsdl::definitions::BindingStyle;
use crate::wsdl::resolver::ResolvedWsdl;
use crate::xsd::types::{ComplexContent, TypeRegistry};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
pub struct DispatchEntry {
pub handler: Arc<dyn SoapHandler>,
pub auth_required: bool,
pub input_type: Option<QName>,
pub validation_type: Option<QName>,
}
impl std::fmt::Debug for DispatchEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DispatchEntry")
.field("auth_required", &self.auth_required)
.field("input_type", &self.input_type)
.field("validation_type", &self.validation_type)
.finish_non_exhaustive()
}
}
pub struct DispatchTable {
by_element: HashMap<QName, DispatchEntry>,
by_action: HashMap<String, DispatchEntry>,
pub default_handler: Option<Arc<dyn SoapHandler>>,
}
impl DispatchTable {
pub fn empty() -> Self {
DispatchTable {
by_element: HashMap::new(),
by_action: HashMap::new(),
default_handler: None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum DispatchError {
#[error("WSDL operation '{0}' has no registered handler")]
UnregisteredOperation(String),
#[error("Registered handler '{0}' has no matching operation in WSDL")]
UnknownOperation(String),
#[error("Operation '{op}' input element '{element}' references type '{type_ref}' which is not in the type registry")]
UnresolvableInputType {
op: String,
element: String,
type_ref: String,
},
}
pub fn build_dispatch_table(
resolved: &ResolvedWsdl,
handlers: HashMap<String, Arc<dyn SoapHandler>>,
auth_bypass: &HashSet<String>,
default_handler: Option<Arc<dyn SoapHandler>>,
) -> Result<DispatchTable, DispatchError> {
let unique_ops = collect_ops_for_service(None, resolved);
build_dispatch_table_from_ops(unique_ops, resolved, handlers, auth_bypass, default_handler)
}
pub fn build_dispatch_table_for_service(
service_name: &str,
resolved: &ResolvedWsdl,
handlers: HashMap<String, Arc<dyn SoapHandler>>,
auth_bypass: &HashSet<String>,
default_handler: Option<Arc<dyn SoapHandler>>,
) -> Result<DispatchTable, DispatchError> {
let unique_ops = collect_ops_for_service(Some(service_name), resolved);
build_dispatch_table_from_ops(unique_ops, resolved, handlers, auth_bypass, default_handler)
}
fn collect_ops_for_service(
service_name: Option<&str>,
resolved: &ResolvedWsdl,
) -> Vec<(String, String, BindingStyle, Option<String>)> {
let mut all_binding_ops: Vec<(String, String, BindingStyle, Option<String>)> = Vec::new();
let service_iter: Vec<&crate::wsdl::definitions::Service> = match service_name {
Some(name) => resolved.definition.services.get(name).into_iter().collect(),
None => resolved.definition.services.values().collect(),
};
for service in service_iter {
for port in &service.ports {
let binding_local = &port.binding.local_name;
if let Some(binding) = resolved.definition.bindings.get(binding_local) {
let style = binding.soap_binding.style.clone();
for binding_op in &binding.operations {
let rpc_ns = if style == BindingStyle::Rpc {
binding_op
.input
.body
.namespace
.clone()
.or_else(|| Some(resolved.definition.target_namespace.clone()))
} else {
None
};
all_binding_ops.push((
binding_op.name.clone(),
binding_op.soap_action.clone(),
style.clone(),
rpc_ns,
));
}
}
}
}
if all_binding_ops.is_empty() && service_name.is_none() {
for binding in resolved.definition.bindings.values() {
let style = binding.soap_binding.style.clone();
for binding_op in &binding.operations {
let rpc_ns = if style == BindingStyle::Rpc {
binding_op
.input
.body
.namespace
.clone()
.or_else(|| Some(resolved.definition.target_namespace.clone()))
} else {
None
};
all_binding_ops.push((
binding_op.name.clone(),
binding_op.soap_action.clone(),
style.clone(),
rpc_ns,
));
}
}
}
let mut seen_ops: HashSet<String> = HashSet::new();
let mut unique_ops: Vec<(String, String, BindingStyle, Option<String>)> = Vec::new();
for (op_name, soap_action, style, rpc_ns) in all_binding_ops {
if seen_ops.insert(op_name.clone()) {
unique_ops.push((op_name, soap_action, style, rpc_ns));
}
}
unique_ops
}
fn build_dispatch_table_from_ops(
unique_ops: Vec<(String, String, BindingStyle, Option<String>)>,
resolved: &ResolvedWsdl,
handlers: HashMap<String, Arc<dyn SoapHandler>>,
auth_bypass: &HashSet<String>,
default_handler: Option<Arc<dyn SoapHandler>>,
) -> Result<DispatchTable, DispatchError> {
let mut by_element: HashMap<QName, DispatchEntry> = HashMap::new();
let mut by_action: HashMap<String, DispatchEntry> = HashMap::new();
let mut consumed_handlers: HashSet<String> = HashSet::new();
for (op_name, soap_action, style, rpc_ns) in unique_ops {
let handler = match handlers.get(&op_name) {
Some(h) => h.clone(),
None => match &default_handler {
Some(d) => d.clone(),
None => return Err(DispatchError::UnregisteredOperation(op_name.clone())),
},
};
consumed_handlers.insert(op_name.clone());
let auth_required = !auth_bypass.contains(&op_name);
let (input_type, validation_type) = if style == BindingStyle::Rpc {
let ns = rpc_ns
.as_deref()
.unwrap_or(&resolved.definition.target_namespace);
let qn = Some(QName::new(ns, &op_name));
(qn.clone(), qn)
} else {
let elem_qname = resolve_input_element(resolved, &op_name);
let val_type = match &elem_qname {
None => None, Some(elem_qn) => {
match resolved.element_type_map.get(elem_qn) {
None => {
None
}
Some(type_qn) => {
if resolved.type_registry.lookup(type_qn).is_none() {
return Err(DispatchError::UnresolvableInputType {
op: op_name.clone(),
element: elem_qn.to_string(),
type_ref: type_qn.to_string(),
});
}
Some(type_qn.clone())
}
}
}
};
(elem_qname, val_type)
};
let entry_for_element = DispatchEntry {
handler: handler.clone(),
auth_required,
input_type: input_type.clone(),
validation_type: validation_type.clone(),
};
let entry_for_action = DispatchEntry {
handler,
auth_required,
input_type,
validation_type,
};
if let Some(ref qn) = entry_for_element.input_type {
by_element.insert(qn.clone(), entry_for_element);
} else {
drop(entry_for_element);
}
if !soap_action.is_empty() {
by_action.insert(soap_action, entry_for_action);
}
}
for handler_name in handlers.keys() {
if !consumed_handlers.contains(handler_name) {
return Err(DispatchError::UnknownOperation(handler_name.clone()));
}
}
Ok(DispatchTable {
by_element,
by_action,
default_handler,
})
}
fn resolve_input_element(resolved: &ResolvedWsdl, op_name: &str) -> Option<QName> {
for port_type in resolved.definition.port_types.values() {
for op in &port_type.operations {
if op.name != op_name {
continue;
}
let input_msg_ref = op.input.as_ref()?;
let msg_local = &input_msg_ref.message.local_name;
let message = resolved.definition.messages.get(msg_local)?;
let first_part = message.parts.first()?;
return first_part.element.clone();
}
}
None
}
pub fn route<'a>(
table: &'a DispatchTable,
body_first_child_qname: &QName,
soap_action: Option<&str>,
) -> Result<&'a DispatchEntry, SoapFault> {
if let Some(entry) = table.by_element.get(body_first_child_qname) {
return Ok(entry);
}
if let Some(action) = soap_action {
if let Some(entry) = table.by_action.get(action) {
return Ok(entry);
}
}
Err(SoapFault::action_not_supported(
&body_first_child_qname.to_string(),
))
}
pub fn validate_request(
body_bytes: &[u8],
type_registry: &TypeRegistry,
input_type: Option<&QName>,
) -> Result<(), SoapFault> {
let Some(qname) = input_type else {
return Ok(());
};
let Some(xsd_type) = type_registry.lookup(qname) else {
return Ok(()); };
let required_names = collect_required_element_names(xsd_type);
if required_names.is_empty() {
return Ok(());
}
let present_children = parse_child_element_names(body_bytes)
.map_err(|e| SoapFault::sender(format!("Schema validation failed: malformed XML: {e}")))?;
for required in &required_names {
if !present_children.contains(required) {
return Err(SoapFault::sender(format!(
"Schema validation failed: required element '{required}' is missing"
)));
}
}
Ok(())
}
fn collect_required_element_names(xsd_type: &crate::xsd::types::XsdType) -> Vec<String> {
use crate::xsd::types::XsdType;
match xsd_type {
XsdType::Complex(ct) => collect_required_from_content(&ct.content),
XsdType::Simple(_) => vec![],
}
}
fn collect_required_from_content(content: &ComplexContent) -> Vec<String> {
let elements = match content {
ComplexContent::Sequence(els) | ComplexContent::All(els) => els,
ComplexContent::Choice(_) => return vec![],
ComplexContent::ComplexExtension { content, .. } => {
return collect_required_from_content(content)
}
ComplexContent::ComplexRestriction { content, .. } => {
return collect_required_from_content(content)
}
ComplexContent::Empty | ComplexContent::SimpleContent(_) => return vec![],
};
elements
.iter()
.filter_map(|el| {
if el.min_occurs > 0 {
el.name.clone()
} else {
None
}
})
.collect()
}
fn parse_child_element_names(body_bytes: &[u8]) -> Result<HashSet<String>, String> {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_reader(body_bytes);
reader.config_mut().trim_text(true);
let mut depth: u32 = 0;
let mut children: HashSet<String> = HashSet::new();
loop {
match reader.read_event() {
Ok(Event::Start(e)) => {
depth += 1;
if depth == 2 {
let local_name = e.local_name();
let name = std::str::from_utf8(local_name.as_ref())
.map_err(|e| e.to_string())?
.to_string();
children.insert(name);
}
}
Ok(Event::Empty(e)) => {
if depth == 1 {
let local_name = e.local_name();
let name = std::str::from_utf8(local_name.as_ref())
.map_err(|e| e.to_string())?
.to_string();
children.insert(name);
}
}
Ok(Event::End(_)) => {
if depth == 0 {
break;
}
depth -= 1;
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.to_string()),
_ => {}
}
}
Ok(children)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fault::{FaultCode, SoapFault};
use crate::handler::SoapHandler;
use crate::qname::QName;
use crate::wsdl::definitions::*;
use crate::wsdl::resolver::ResolvedWsdl;
use crate::xsd::elements::XsdElement;
use crate::xsd::types::{ComplexContent, ComplexType, TypeRegistry, XsdType};
use async_trait::async_trait;
use bytes::Bytes;
use std::sync::Arc;
struct MockHandler {
name: &'static str,
}
#[async_trait]
impl SoapHandler for MockHandler {
async fn handle(&self, _body: Bytes) -> Result<Bytes, SoapFault> {
Ok(Bytes::from(format!("<response>{}</response>", self.name)))
}
}
fn mock_handler(name: &'static str) -> Arc<dyn SoapHandler> {
Arc::new(MockHandler { name })
}
fn make_resolved_wsdl(
op_name: &str,
soap_action: &str,
input_element_qname: Option<QName>,
) -> ResolvedWsdl {
let target_ns = "http://example.com/service".to_string();
let msg_name = format!("{op_name}Request");
let part = MessagePart {
name: "parameters".to_string(),
element: input_element_qname,
type_ref: None,
};
let message = Message {
name: msg_name.clone(),
parts: vec![part],
};
let pt_op = Operation {
name: op_name.to_string(),
input: Some(OperationMessage {
name: None,
message: QName::local(&msg_name),
}),
output: None,
faults: vec![],
style: OperationStyle::RequestResponse,
};
let port_type = PortType {
name: "TestPortType".to_string(),
operations: vec![pt_op],
};
let binding_op = BindingOperation {
name: op_name.to_string(),
soap_action: soap_action.to_string(),
input: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
output: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
};
let binding = Binding {
name: "TestBinding".to_string(),
port_type: QName::local("TestPortType"),
soap_binding: SoapBinding {
style: BindingStyle::Document,
transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
soap_version: SoapVersion::Soap12,
},
operations: vec![binding_op],
};
let port = Port {
name: "TestPort".to_string(),
binding: QName::local("TestBinding"),
address: "http://localhost/service".to_string(),
};
let service = Service {
name: "TestService".to_string(),
ports: vec![port],
};
let mut messages = HashMap::new();
messages.insert(msg_name, message);
let mut port_types = HashMap::new();
port_types.insert("TestPortType".to_string(), port_type);
let mut bindings = HashMap::new();
bindings.insert("TestBinding".to_string(), binding);
let mut services = HashMap::new();
services.insert("TestService".to_string(), service);
let definition = WsdlDefinition {
target_namespace: target_ns,
imports: vec![],
types: TypesSection::default(),
messages,
port_types,
bindings,
services,
};
ResolvedWsdl {
definition,
type_registry: TypeRegistry::new(),
element_type_map: HashMap::new(),
raw_bytes: vec![],
}
}
#[test]
fn dispatch_table_routes_by_element_qname() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved =
make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(entry.auth_required); }
#[test]
fn dispatch_table_unregistered_operation_fails_at_build() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved = make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname));
let handlers: HashMap<String, Arc<dyn SoapHandler>> = HashMap::new();
let result = build_dispatch_table(&resolved, handlers, &HashSet::new(), None);
assert!(
matches!(result, Err(DispatchError::UnregisteredOperation(ref name)) if name == "GetProfiles")
);
}
#[test]
fn dispatch_table_unknown_handler_name_fails_at_build() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved = make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
handlers.insert("NonExistentOp".to_string(), mock_handler("ghost"));
let result = build_dispatch_table(&resolved, handlers, &HashSet::new(), None);
assert!(
matches!(result, Err(DispatchError::UnknownOperation(ref name)) if name == "NonExistentOp")
);
}
#[test]
fn route_unknown_qname_no_soap_action_returns_sender_fault() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved =
make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let unknown = QName::new("http://example.com", "UnknownOperation");
let result = route(&table, &unknown, None);
assert!(result.is_err());
let fault = result.unwrap_err();
assert_eq!(fault.code, FaultCode::Sender);
assert!(
fault.reason.to_lowercase().contains("action")
|| fault.reason.to_lowercase().contains("supported")
);
}
#[test]
fn route_falls_back_to_soap_action_on_unknown_qname() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved =
make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let unknown = QName::local("SomethingElse");
let result = route(&table, &unknown, Some("urn:GetProfiles"));
assert!(result.is_ok(), "Expected fallback to SOAPAction to succeed");
}
#[test]
fn route_unknown_qname_unknown_soap_action_returns_fault() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved =
make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let unknown = QName::local("SomethingElse");
let result = route(&table, &unknown, Some("urn:UnknownAction"));
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, FaultCode::Sender);
}
#[test]
fn auth_bypass_marks_entry_auth_not_required() {
let elem_qname = QName::new("http://example.com", "GetProfiles");
let resolved =
make_resolved_wsdl("GetProfiles", "urn:GetProfiles", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let mut bypass = HashSet::new();
bypass.insert("GetProfiles".to_string());
let table = build_dispatch_table(&resolved, handlers, &bypass, None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(!entry.auth_required); }
#[test]
fn auth_required_by_default_for_non_bypass_op() {
let elem_qname = QName::new("http://example.com", "SetSomething");
let resolved =
make_resolved_wsdl("SetSomething", "urn:SetSomething", Some(elem_qname.clone()));
let mut handlers = HashMap::new();
handlers.insert("SetSomething".to_string(), mock_handler("SetSomething"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(entry.auth_required);
}
fn make_type_registry_with_required_field(
type_qname: &QName,
required_field: &str,
) -> TypeRegistry {
let element = XsdElement {
name: Some(required_field.to_string()),
min_occurs: 1,
..Default::default()
};
let ct = ComplexType {
name: Some(type_qname.local_name.clone()),
content: ComplexContent::Sequence(vec![element]),
attributes: vec![],
};
let mut reg = TypeRegistry::new();
reg.insert(type_qname.clone(), XsdType::Complex(Box::new(ct)));
reg
}
#[test]
fn validate_request_no_input_type_returns_ok() {
let reg = TypeRegistry::new();
let result = validate_request(b"<GetProfiles/>", ®, None);
assert!(result.is_ok());
}
#[test]
fn validate_request_unknown_type_in_registry_returns_ok() {
let reg = TypeRegistry::new(); let qname = QName::new("http://example.com", "GetProfilesRequest");
let result = validate_request(b"<GetProfiles/>", ®, Some(&qname));
assert!(result.is_ok());
}
#[test]
fn validate_request_valid_xml_with_required_element_returns_ok() {
let qname = QName::new("http://example.com", "GetProfilesRequest");
let reg = make_type_registry_with_required_field(&qname, "Token");
let body = b"<GetProfiles xmlns=\"http://example.com\"><Token>1234</Token></GetProfiles>";
let result = validate_request(body, ®, Some(&qname));
assert!(result.is_ok(), "Expected Ok, got: {result:?}");
}
#[test]
fn validate_request_missing_required_element_returns_sender_fault() {
let qname = QName::new("http://example.com", "GetProfilesRequest");
let reg = make_type_registry_with_required_field(&qname, "Token");
let body = b"<GetProfiles xmlns=\"http://example.com\"></GetProfiles>";
let result = validate_request(body, ®, Some(&qname));
assert!(result.is_err());
let fault = result.unwrap_err();
assert_eq!(fault.code, FaultCode::Sender);
assert!(fault.reason.contains("Schema validation failed"));
assert!(fault.reason.contains("Token"));
}
#[test]
fn validate_request_optional_element_missing_returns_ok() {
let qname = QName::new("http://example.com", "GetProfilesRequest");
let optional_el = XsdElement {
name: Some("OptionalField".to_string()),
min_occurs: 0,
..Default::default()
};
let ct = ComplexType {
name: Some("GetProfilesRequest".to_string()),
content: ComplexContent::Sequence(vec![optional_el]),
attributes: vec![],
};
let mut reg = TypeRegistry::new();
reg.insert(qname.clone(), XsdType::Complex(Box::new(ct)));
let body = b"<GetProfiles xmlns=\"http://example.com\"></GetProfiles>";
let result = validate_request(body, ®, Some(&qname));
assert!(result.is_ok());
}
#[test]
fn handler_name_matches_wsdl_operation_name_get_profiles() {
let elem_qname = QName::new("http://onvif.org/ver10/media/wsdl", "GetProfiles");
let resolved = make_resolved_wsdl(
"GetProfiles",
"http://www.onvif.org/ver10/media/wsdl/GetProfiles",
Some(elem_qname.clone()),
);
let mut handlers = HashMap::new();
handlers.insert("GetProfiles".to_string(), mock_handler("GetProfiles"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(entry.auth_required);
}
fn make_resolved_wsdl_rpc(
op_name: &str,
soap_action: &str,
rpc_namespace: Option<&str>,
) -> ResolvedWsdl {
let target_ns = "http://example.com/service".to_string();
let msg_name = format!("{op_name}Request");
let part = MessagePart {
name: "parameters".to_string(),
element: None,
type_ref: Some(QName::new("http://www.w3.org/2001/XMLSchema", "string")),
};
let message = Message {
name: msg_name.clone(),
parts: vec![part],
};
let pt_op = Operation {
name: op_name.to_string(),
input: Some(OperationMessage {
name: None,
message: QName::local(&msg_name),
}),
output: None,
faults: vec![],
style: OperationStyle::RequestResponse,
};
let port_type = PortType {
name: "TestPortType".to_string(),
operations: vec![pt_op],
};
let binding_op = BindingOperation {
name: op_name.to_string(),
soap_action: soap_action.to_string(),
input: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Encoded,
namespace: rpc_namespace.map(|s| s.to_string()),
encoding_style: None,
},
headers: vec![],
},
output: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Encoded,
namespace: rpc_namespace.map(|s| s.to_string()),
encoding_style: None,
},
headers: vec![],
},
};
let binding = Binding {
name: "TestBinding".to_string(),
port_type: QName::local("TestPortType"),
soap_binding: SoapBinding {
style: BindingStyle::Rpc,
transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
soap_version: SoapVersion::Soap12,
},
operations: vec![binding_op],
};
let port = Port {
name: "TestPort".to_string(),
binding: QName::local("TestBinding"),
address: "http://localhost/service".to_string(),
};
let service = Service {
name: "TestService".to_string(),
ports: vec![port],
};
let mut messages = HashMap::new();
messages.insert(msg_name, message);
let mut port_types = HashMap::new();
port_types.insert("TestPortType".to_string(), port_type);
let mut bindings = HashMap::new();
bindings.insert("TestBinding".to_string(), binding);
let mut services = HashMap::new();
services.insert("TestService".to_string(), service);
let definition = WsdlDefinition {
target_namespace: target_ns,
imports: vec![],
types: TypesSection::default(),
messages,
port_types,
bindings,
services,
};
ResolvedWsdl {
definition,
type_registry: crate::xsd::types::TypeRegistry::new(),
element_type_map: HashMap::new(),
raw_bytes: vec![],
}
}
fn make_resolved_wsdl_two_services() -> ResolvedWsdl {
let target_ns = "http://example.com/multi".to_string();
let msg_a = Message {
name: "OpARequest".to_string(),
parts: vec![MessagePart {
name: "parameters".to_string(),
element: Some(QName::new("http://example.com/multi", "OpA")),
type_ref: None,
}],
};
let msg_b = Message {
name: "OpBRequest".to_string(),
parts: vec![MessagePart {
name: "parameters".to_string(),
element: Some(QName::new("http://example.com/multi", "OpB")),
type_ref: None,
}],
};
let pt_a = PortType {
name: "PortTypeA".to_string(),
operations: vec![Operation {
name: "OpA".to_string(),
input: Some(OperationMessage {
name: None,
message: QName::local("OpARequest"),
}),
output: None,
faults: vec![],
style: OperationStyle::RequestResponse,
}],
};
let pt_b = PortType {
name: "PortTypeB".to_string(),
operations: vec![Operation {
name: "OpB".to_string(),
input: Some(OperationMessage {
name: None,
message: QName::local("OpBRequest"),
}),
output: None,
faults: vec![],
style: OperationStyle::RequestResponse,
}],
};
let binding_a = Binding {
name: "BindingA".to_string(),
port_type: QName::local("PortTypeA"),
soap_binding: SoapBinding {
style: BindingStyle::Document,
transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
soap_version: SoapVersion::Soap12,
},
operations: vec![BindingOperation {
name: "OpA".to_string(),
soap_action: "urn:OpA".to_string(),
input: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
output: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
}],
};
let binding_b = Binding {
name: "BindingB".to_string(),
port_type: QName::local("PortTypeB"),
soap_binding: SoapBinding {
style: BindingStyle::Document,
transport: "http://schemas.xmlsoap.org/soap/http".to_string(),
soap_version: SoapVersion::Soap12,
},
operations: vec![BindingOperation {
name: "OpB".to_string(),
soap_action: "urn:OpB".to_string(),
input: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
output: BindingMessage {
body: SoapBody {
use_attr: UseStyle::Literal,
namespace: None,
encoding_style: None,
},
headers: vec![],
},
}],
};
let service_a = Service {
name: "ServiceA".to_string(),
ports: vec![Port {
name: "PortA".to_string(),
binding: QName::local("BindingA"),
address: "http://localhost/soap/a".to_string(),
}],
};
let service_b = Service {
name: "ServiceB".to_string(),
ports: vec![Port {
name: "PortB".to_string(),
binding: QName::local("BindingB"),
address: "http://localhost/soap/b".to_string(),
}],
};
let mut messages = HashMap::new();
messages.insert("OpARequest".to_string(), msg_a);
messages.insert("OpBRequest".to_string(), msg_b);
let mut port_types = HashMap::new();
port_types.insert("PortTypeA".to_string(), pt_a);
port_types.insert("PortTypeB".to_string(), pt_b);
let mut bindings = HashMap::new();
bindings.insert("BindingA".to_string(), binding_a);
bindings.insert("BindingB".to_string(), binding_b);
let mut services = HashMap::new();
services.insert("ServiceA".to_string(), service_a);
services.insert("ServiceB".to_string(), service_b);
let definition = WsdlDefinition {
target_namespace: target_ns,
imports: vec![],
types: TypesSection::default(),
messages,
port_types,
bindings,
services,
};
ResolvedWsdl {
definition,
type_registry: crate::xsd::types::TypeRegistry::new(),
element_type_map: HashMap::new(),
raw_bytes: vec![],
}
}
#[test]
fn build_dispatch_table_rpc_binding() {
let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", Some("http://example.com/svc"));
let mut handlers = HashMap::new();
handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let rpc_qname = QName::new("http://example.com/svc", "GetOp");
let result = route(&table, &rpc_qname, None);
assert!(
result.is_ok(),
"Expected RPC QName to route successfully, got: {result:?}"
);
}
#[test]
fn rpc_dispatch_by_wrapper_element() {
let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", Some("http://example.com/svc"));
let mut handlers = HashMap::new();
handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let rpc_qname = QName::new("http://example.com/svc", "GetOp");
let result = route(&table, &rpc_qname, None);
assert!(
result.is_ok(),
"route by synthesized RPC QName should return Ok"
);
}
#[test]
fn build_dispatch_table_rpc_missing_namespace_falls_back_to_target_ns() {
let resolved = make_resolved_wsdl_rpc("GetOp", "urn:GetOp", None);
let target_ns = resolved.definition.target_namespace.clone();
let mut handlers = HashMap::new();
handlers.insert("GetOp".to_string(), mock_handler("GetOp"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let fallback_qname = QName::new(&target_ns, "GetOp");
let result = route(&table, &fallback_qname, None);
assert!(
result.is_ok(),
"RPC without namespace should fall back to targetNamespace, got: {result:?}"
);
}
#[test]
fn build_dispatch_table_for_service_isolates_operations() {
let resolved = make_resolved_wsdl_two_services();
let op_a_qname = QName::new("http://example.com/multi", "OpA");
let op_b_qname = QName::new("http://example.com/multi", "OpB");
let mut handlers_a = HashMap::new();
handlers_a.insert("OpA".to_string(), mock_handler("OpA"));
let mut handlers_b = HashMap::new();
handlers_b.insert("OpB".to_string(), mock_handler("OpB"));
let table_a = build_dispatch_table_for_service(
"ServiceA",
&resolved,
handlers_a,
&HashSet::new(),
None,
)
.unwrap();
let table_b = build_dispatch_table_for_service(
"ServiceB",
&resolved,
handlers_b,
&HashSet::new(),
None,
)
.unwrap();
assert!(
route(&table_a, &op_a_qname, None).is_ok(),
"ServiceA should route OpA"
);
assert!(
route(&table_b, &op_b_qname, None).is_ok(),
"ServiceB should route OpB"
);
assert!(
route(&table_a, &op_b_qname, None).is_err(),
"ServiceA should NOT route OpB"
);
}
const INLINE_ELEMENT_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:tns="http://example.com/svc"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/svc">
<types>
<xs:schema targetNamespace="http://example.com/svc" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="GetFoo">
<xs:complexType>
<xs:sequence>
<xs:element name="RequiredField" type="xs:string" minOccurs="1"/>
<xs:element name="OptionalField" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetFooResponse">
<xs:complexType><xs:sequence/></xs:complexType>
</xs:element>
</xs:schema>
</types>
<message name="GetFooRequest"><part name="parameters" element="tns:GetFoo"/></message>
<message name="GetFooResponse"><part name="parameters" element="tns:GetFooResponse"/></message>
<portType name="SvcPortType">
<operation name="GetFoo">
<input message="tns:GetFooRequest"/>
<output message="tns:GetFooResponse"/>
</operation>
</portType>
<binding name="SvcBinding" type="tns:SvcPortType">
<soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetFoo">
<soap12:operation soapAction="urn:GetFoo"/>
<input><soap12:body use="literal"/></input>
<output><soap12:body use="literal"/></output>
</operation>
</binding>
<service name="SvcService">
<port name="SvcPort" binding="tns:SvcBinding">
<soap12:address location="http://localhost/svc"/>
</port>
</service>
</definitions>"#;
const NAMED_TYPE_REF_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:tns="http://example.com/svc"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/svc">
<types>
<xs:schema targetNamespace="http://example.com/svc" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="GetBarType">
<xs:sequence>
<xs:element name="BarRequired" type="xs:string" minOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:element name="GetBar" type="tns:GetBarType"/>
<xs:element name="GetBarResponse">
<xs:complexType><xs:sequence/></xs:complexType>
</xs:element>
</xs:schema>
</types>
<message name="GetBarRequest"><part name="parameters" element="tns:GetBar"/></message>
<message name="GetBarResponse"><part name="parameters" element="tns:GetBarResponse"/></message>
<portType name="SvcPortType">
<operation name="GetBar">
<input message="tns:GetBarRequest"/>
<output message="tns:GetBarResponse"/>
</operation>
</portType>
<binding name="SvcBinding" type="tns:SvcPortType">
<soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetBar">
<soap12:operation soapAction="urn:GetBar"/>
<input><soap12:body use="literal"/></input>
<output><soap12:body use="literal"/></output>
</operation>
</binding>
<service name="SvcService">
<port name="SvcPort" binding="tns:SvcBinding">
<soap12:address location="http://localhost/svc"/>
</port>
</service>
</definitions>"#;
fn resolve_test_wsdl(wsdl: &str) -> ResolvedWsdl {
use crate::wsdl::parser::WsdlError;
use crate::wsdl::resolver::{resolve_wsdl, WsdlLoader};
struct NullLoader;
impl WsdlLoader for NullLoader {
fn load(&self, loc: &str) -> Result<Vec<u8>, WsdlError> {
Err(WsdlError::MalformedXml(format!("NullLoader: {loc}")))
}
}
let mut visited = HashSet::new();
resolve_wsdl(wsdl.as_bytes(), &NullLoader, &mut visited)
.expect("WSDL resolution should succeed")
}
#[test]
fn doc_literal_inline_type_missing_required_field_rejected() {
let resolved = resolve_test_wsdl(INLINE_ELEMENT_WSDL);
let elem_qname = QName::new("http://example.com/svc", "GetFoo");
assert!(
resolved.type_registry.lookup(&elem_qname).is_some(),
"Inline element type must be in TypeRegistry after resolve_wsdl"
);
let mut handlers = HashMap::new();
handlers.insert("GetFoo".to_string(), mock_handler("GetFoo"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(
entry.validation_type.is_some(),
"validation_type must be Some for document/literal op with inline type"
);
let body_missing = br#"<tns:GetFoo xmlns:tns="http://example.com/svc"/>"#;
let result = validate_request(
body_missing,
&resolved.type_registry,
entry.validation_type.as_ref(),
);
assert!(
result.is_err(),
"Request missing RequiredField must produce a validation error"
);
let fault = result.unwrap_err();
assert_eq!(fault.code, crate::fault::FaultCode::Sender);
assert!(
fault.reason.contains("RequiredField"),
"Fault reason must mention the missing field, got: {}",
fault.reason
);
let body_ok = br#"<tns:GetFoo xmlns:tns="http://example.com/svc"><tns:RequiredField>val</tns:RequiredField></tns:GetFoo>"#;
let result_ok = validate_request(
body_ok,
&resolved.type_registry,
entry.validation_type.as_ref(),
);
assert!(
result_ok.is_ok(),
"Request with RequiredField must pass validation, got: {result_ok:?}"
);
}
#[test]
fn doc_literal_named_type_ref_missing_required_field_rejected() {
let resolved = resolve_test_wsdl(NAMED_TYPE_REF_WSDL);
let elem_qname = QName::new("http://example.com/svc", "GetBar");
let type_qname = QName::new("http://example.com/svc", "GetBarType");
assert!(
resolved.type_registry.lookup(&type_qname).is_some(),
"Named type GetBarType must be in TypeRegistry"
);
let mut handlers = HashMap::new();
handlers.insert("GetBar".to_string(), mock_handler("GetBar"));
let table = build_dispatch_table(&resolved, handlers, &HashSet::new(), None).unwrap();
let entry = route(&table, &elem_qname, None).unwrap();
assert!(
entry.validation_type.is_some(),
"validation_type must be Some for document/literal op with named type ref"
);
assert_eq!(
entry.validation_type.as_ref().unwrap(),
&type_qname,
"validation_type must be the named type QName, not the element QName"
);
let body_missing = br#"<tns:GetBar xmlns:tns="http://example.com/svc"/>"#;
let result = validate_request(
body_missing,
&resolved.type_registry,
entry.validation_type.as_ref(),
);
assert!(
result.is_err(),
"Request missing BarRequired must produce a validation error"
);
let fault = result.unwrap_err();
assert_eq!(fault.code, crate::fault::FaultCode::Sender);
assert!(
fault.reason.contains("BarRequired"),
"Fault reason must mention the missing field, got: {}",
fault.reason
);
let body_ok = br#"<tns:GetBar xmlns:tns="http://example.com/svc"><tns:BarRequired>val</tns:BarRequired></tns:GetBar>"#;
let result_ok = validate_request(
body_ok,
&resolved.type_registry,
entry.validation_type.as_ref(),
);
assert!(
result_ok.is_ok(),
"Request with BarRequired must pass validation, got: {result_ok:?}"
);
}
}