From 64a52ac15db5fbc72287cc72c99e35355f76dcda Mon Sep 17 00:00:00 2001 From: "Laurence A. Lee" Date: Wed, 15 Jul 2026 11:46:41 -1000 Subject: [PATCH] SOAP 1.2 support: envelope, header role/relay, fault model, HTTP transport, WSDL binding Adds SOAP::SOAPVersion, a small bundle class for the constants that differ between SOAP 1.1 and 1.2 (envelope namespace, header role/relay/ mustUnderstand attributes, media type, fault codes). Driver/Proxy/Router gain a soap_version accessor (default SOAPVersion1_1, zero behavior change unless explicitly set to SOAPVersion1_2). Envelope/header: SOAPEnvelope/SOAPHeader/SOAPBody derive their element namespace from the configured version instead of hardcoded 1.1 constants. SOAP 1.1's "actor" header attribute is renamed "role" in 1.2, which also adds a "relay" attribute 1.1 has no equivalent for. Fault model: SOAP 1.2 replaces 1.1's flat faultcode/faultstring/ faultactor/detail with a nested Code{Value,Subcode?}+Reason{Text}+Node?+ Role?+Detail? shape (SOAP12Fault). FaultError reads either shape into the same reader names; Router#fault branches on soap_version to build the right one. HTTP transport: SOAP 1.2 folds the SOAPAction value into the Content-Type's "action" parameter and drops the separate SOAPAction header entirely (1.1 keeps both). StreamHandler/Proxy/Router/Soaplet all thread soap_version through to build/parse the right form. WSDL binding: recognizes the SOAP 1.2 WSDL binding namespace alongside the 1.1 one at all six dispatch points that previously only recognized 1.1, and configures generated/runtime-built drivers' soap_version accordingly based on which binding namespace actually matched. SOAP-with-Attachments (MIME multipart) also now honors soap_version: MIMEMessage previously hardcoded "text/xml" for both the root part and the outer multipart's own "type" parameter regardless of version. The legacy SOAPAction header is still sent for attachment requests even under 1.2 (where it's otherwise dropped) since action placement for attachments+1.2 combined isn't settled by any spec this was checked against, and real-world use of that combination is rare -- the same pragmatic fallback Savon's own client uses unconditionally. Verified with real live round trips throughout, not just unit tests: a hand-built SOAP 1.2 envelope round-tripping through Processor.marshal/ unmarshal; a real WEBrick StandaloneServer and Driver both configured for SOAP 1.2 completing an actual RPC call over real HTTP, proving the Content-Type-only action dispatch genuinely works; a WSDL2Ruby-generated driver from a SOAP 1.2-bound WSDL fixture completing a real RPC call end to end. Full test:deep clean across all 5 parsers on both ruby:3.4.10 and soap4r-legacy:1.9.3-p551 throughout development. --- lib/soap/element.rb | 175 ++++++++++++++++-- lib/soap/mimemessage.rb | 13 +- lib/soap/ns.rb | 4 +- lib/soap/parser.rb | 19 +- lib/soap/rpc/driver.rb | 3 +- lib/soap/rpc/httpserver.rb | 1 + lib/soap/rpc/proxy.rb | 15 +- lib/soap/rpc/router.rb | 46 +++-- lib/soap/rpc/soaplet.rb | 8 + lib/soap/soap.rb | 25 ++- lib/soap/soapversion.rb | 87 +++++++++ lib/soap/streamHandler.rb | 57 +++++- lib/soap/wsdlDriver.rb | 3 + lib/wsdl/binding.rb | 3 +- lib/wsdl/data.rb | 13 ++ lib/wsdl/operationBinding.rb | 2 +- lib/wsdl/param.rb | 6 +- lib/wsdl/port.rb | 2 +- lib/wsdl/service.rb | 2 +- lib/wsdl/soap/binding.rb | 7 + lib/wsdl/soap/data.rb | 1 + lib/wsdl/soap/driverCreator.rb | 3 + lib/wsdl/soap/header.rb | 2 +- lib/wsdl/wsdl.rb | 4 + test/soap/fault/test_soap12_fault_client.rb | 56 ++++++ test/soap/fault/test_soap12_fault_router.rb | 55 ++++++ test/soap/fault/test_soap12_fault_shape.rb | 92 +++++++++ test/soap/swa/test_soap12_swa.rb | 85 +++++++++ .../version/test_soap12_envelope_roundtrip.rb | 59 ++++++ test/soap/version/test_soap12_http_headers.rb | 63 +++++++ .../version/test_soap12_http_roundtrip.rb | 58 ++++++ test/soap/version/test_soap12_role_relay.rb | 52 ++++++ .../soap/version/test_soap_version_default.rb | 62 +++++++ test/wsdl/soap/soap12/soap12.wsdl | 49 +++++ .../wsdl/soap/soap12/test_soap12_roundtrip.rb | 69 +++++++ test/wsdl/soap/test_soap12_binding_parse.rb | 38 ++++ 36 files changed, 1176 insertions(+), 63 deletions(-) create mode 100644 lib/soap/soapversion.rb create mode 100644 test/soap/fault/test_soap12_fault_client.rb create mode 100644 test/soap/fault/test_soap12_fault_router.rb create mode 100644 test/soap/fault/test_soap12_fault_shape.rb create mode 100644 test/soap/swa/test_soap12_swa.rb create mode 100644 test/soap/version/test_soap12_envelope_roundtrip.rb create mode 100644 test/soap/version/test_soap12_http_headers.rb create mode 100644 test/soap/version/test_soap12_http_roundtrip.rb create mode 100644 test/soap/version/test_soap12_role_relay.rb create mode 100644 test/soap/version/test_soap_version_default.rb create mode 100644 test/wsdl/soap/soap12/soap12.wsdl create mode 100644 test/wsdl/soap/soap12/test_soap12_roundtrip.rb create mode 100644 test/wsdl/soap/test_soap12_binding_parse.rb diff --git a/lib/soap/element.rb b/lib/soap/element.rb index 9e6a6a9cd..ca1c6dccf 100644 --- a/lib/soap/element.rb +++ b/lib/soap/element.rb @@ -9,6 +9,7 @@ require 'xsd/qname' require 'soap/baseData' +require 'soap/soapversion' module SOAP @@ -92,14 +93,148 @@ def encode(generator, ns, attrs = {}) end +# The SOAP 1.2 fault shape (env:Fault > Code{Value,Subcode?} + +# Reason{Text}+ + Node? + Role? + Detail?) is structurally different +# enough from SOAP 1.1's flat faultcode/faultstring/faultactor/detail +# (SOAPFault above, left completely untouched) that it gets its own +# class rather than trying to make one class cover both shapes. +# +# Code/Subcode/Reason and their leaf children (Value/Text/Role) are built +# as SOAPElement instances rather than SOAPStruct/SOAPQName: SOAPElement +# defaults its own encodingstyle to LiteralNamespace, which routes +# encoding through LiteralHandler instead of the RPC/Encoded-style +# SOAPHandler -- avoiding a pile of xsi:type/encodingStyle attributes and +# stray namespace declarations that SOAP 1.2 fault content, being plain +# structural XML rather than SOAP-encoded data, has no business carrying. +# SOAPElement's text-content handling also already special-cases QName +# values (used for Value's content, e.g. FaultCode12::Sender) by +# resolving them against the current namespace-to-prefix mapping at +# encode time -- exactly what's needed here, and not something +# SOAPQName's generic path does on its own. +# +# No new decode-dispatch wiring needed despite the nesting: both +# SOAPHandler's and LiteralHandler's decode_parent key struct/element +# children purely by local name (node.elename.name), never by namespace, +# so Code/Subcode/Reason/etc. decode through the exact same generic +# machinery already used for all other body content. +class SOAP12Fault < SOAPStruct + include SOAPEnvelopeElement + include SOAPCompoundtype + +public + + # Standard fault-code Values (Sender/Receiver/MustUnderstand/ + # VersionMismatch) always live in the SOAP 1.2 envelope namespace per + # spec, so those resolve back to a real XSD::QName even after a decode + # round-trip, where the wire value is just a prefixed string ("env: + # Sender") with no automatic QName-content resolution in the generic + # literal-style decode path this reuses (a real, pre-existing gap in + # this codebase, not something specific to fault codes -- no QName- + # typed element content resolves its prefix back to a namespace + # anywhere today without an explicit type-driven decode path). Custom + # Subcodes can live in arbitrary app namespaces, so code_subcode can't + # apply the same trick; it returns the resolved QName only when never + # serialized (same-process construction), and the raw wire string + # ("n1:BadArgument") after a real decode. + STANDARD_FAULT_CODE_LOCAL_NAMES = { + 'Sender' => FaultCode12::Sender, + 'Receiver' => FaultCode12::Receiver, + 'MustUnderstand' => FaultCode12::MustUnderstand, + 'VersionMismatch' => FaultCode12::VersionMismatch + } + + def code_value + v = self['Code'] && self['Code']['Value'] + return nil unless v + data = v.data + return data unless data.is_a?(String) + STANDARD_FAULT_CODE_LOCAL_NAMES[data.sub(/\A[^:]+:/, '')] || data + end + + def code_subcode + v = self['Code'] && self['Code']['Subcode'] && self['Code']['Subcode']['Value'] + v && v.data + end + + def reason_text + v = self['Reason'] && self['Reason']['Text'] + v && v.data + end + + def node + v = self['Node'] + v && v.data + end + + def role + v = self['Role'] + v && v.data + end + + def detail + self['Detail'] + end + + def detail=(rhs) + self['Detail'] = rhs + end + + # code_value/code_subcode are XSD::QName (see FaultCode12::Sender etc); + # reason_text/node/role are plain strings; detail is any SOAP/OM node. + def initialize(code_value = nil, reason_text = nil, code_subcode = nil, + role = nil, detail = nil, reason_lang = 'en') + ns_uri = SOAPVersion1_2.envelope_namespace + super(XSD::QName.new(ns_uri, EleFault)) + @elename = XSD::QName.new(ns_uri, EleFault) + @encodingstyle = LiteralNamespace + return unless code_value + + code = SOAPElement.new(XSD::QName.new(ns_uri, 'Code')) + code.add(SOAPElement.new(XSD::QName.new(ns_uri, 'Value'), code_value)) + if code_subcode + subcode = SOAPElement.new(XSD::QName.new(ns_uri, 'Subcode')) + subcode.add(SOAPElement.new(XSD::QName.new(ns_uri, 'Value'), code_subcode)) + code.add(subcode) + end + self.add('Code', code) + + reason = SOAPElement.new(XSD::QName.new(ns_uri, 'Reason')) + text = SOAPElement.new(XSD::QName.new(ns_uri, 'Text'), reason_text) + text.extraattr[XSD::QName.new(XSD::NS::Namespace, 'lang')] = reason_lang + reason.add(text) + self.add('Reason', reason) + + if role + self.add('Role', SOAPElement.new(XSD::QName.new(ns_uri, 'Role'), role)) + end + if detail + detail.elename = XSD::QName.new(ns_uri, 'Detail') if detail.respond_to?(:elename=) + self.add('Detail', detail) + end + end + + def encode(generator, ns, attrs = {}) + Generator.assign_ns(attrs, ns, SOAPVersion1_2.envelope_namespace) + name = ns.name(@elename) + generator.encode_tag(name, attrs) + yield(self['Code']) + yield(self['Reason']) + yield(self['Node']) if self['Node'] + yield(self['Role']) if self['Role'] + yield(self['Detail']) if self['Detail'] + generator.encode_tag_end(name, true) + end +end + + class SOAPBody < SOAPStruct include SOAPEnvelopeElement attr_reader :is_fault - def initialize(data = nil, is_fault = false) + def initialize(data = nil, is_fault = false, soap_version = SOAPVersion1_1) super(nil) - @elename = EleBodyName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleBody) @encodingstyle = nil if data if data.respond_to?(:to_xmlpart) @@ -154,14 +289,18 @@ class SOAPHeaderItem < XSD::NSDBase attr_accessor :mustunderstand attr_accessor :encodingstyle attr_accessor :actor + attr_accessor :relay - def initialize(element, mustunderstand = true, encodingstyle = nil, actor = nil) + def initialize(element, mustunderstand = true, encodingstyle = nil, actor = nil, + relay = nil, soap_version = SOAPVersion1_1) super() @type = nil @element = element @mustunderstand = mustunderstand @encodingstyle = encodingstyle @actor = actor + @relay = relay + @soap_version = soap_version element.parent = self if element element.qualified = true end @@ -172,7 +311,8 @@ def encode(generator, ns, attrs = {}) end # to remove mustUnderstand attribute, set it to nil unless @mustunderstand.nil? - @element.extraattr[AttrMustUnderstandName] = (@mustunderstand ? '1' : '0') + @element.extraattr[@soap_version.mustunderstand_attr_name] = + (@mustunderstand ? '1' : '0') end if @encodingstyle @element.extraattr[AttrEncodingStyleName] = @encodingstyle @@ -181,7 +321,12 @@ def encode(generator, ns, attrs = {}) @element.encodingstyle = @encodingstyle end if @actor - @element.extraattr[AttrActorName] = @actor + @element.extraattr[@soap_version.role_attr_name] = @actor + end + # relay has no SOAP 1.1 equivalent -- @soap_version.relay_attr_name is + # nil there, so this is a no-op unless actually running under 1.2. + unless @relay.nil? || @soap_version.relay_attr_name.nil? + @element.extraattr[@soap_version.relay_attr_name] = (@relay ? 'true' : 'false') end yield(@element) end @@ -192,12 +337,14 @@ class SOAPHeader < SOAPStruct include SOAPEnvelopeElement attr_writer :force_encode + attr_reader :soap_version - def initialize + def initialize(soap_version = SOAPVersion1_1) super(nil) - @elename = EleHeaderName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleHeader) @encodingstyle = nil @force_encode = false + @soap_version = soap_version end def encode(generator, ns, attrs = {}) @@ -210,12 +357,14 @@ def encode(generator, ns, attrs = {}) end def add(name, value) - actor = value.extraattr[AttrActorName] - mu = value.extraattr[AttrMustUnderstandName] + actor = value.extraattr[@soap_version.role_attr_name] + mu = value.extraattr[@soap_version.mustunderstand_attr_name] encstyle = value.extraattr[AttrEncodingStyleName] - mu_value = mu.nil? ? nil : (mu == '1') + mu_value = mu.nil? ? nil : (mu == '1' || mu == 'true') + relay = @soap_version.relay_attr_name && value.extraattr[@soap_version.relay_attr_name] + relay_value = relay.nil? ? nil : (relay == 'true' || relay == '1') # to remove mustUnderstand attribute, set it to nil - item = SOAPHeaderItem.new(value, mu_value, encstyle, actor) + item = SOAPHeaderItem.new(value, mu_value, encstyle, actor, relay_value, @soap_version) super(name, item) end @@ -238,10 +387,10 @@ class SOAPEnvelope < XSD::NSDBase attr_reader :body attr_reader :external_content - def initialize(header = nil, body = nil) + def initialize(header = nil, body = nil, soap_version = SOAPVersion1_1) super() @type = nil - @elename = EleEnvelopeName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleEnvelope) @encodingstyle = nil @header = header @body = body diff --git a/lib/soap/mimemessage.rb b/lib/soap/mimemessage.rb index 79516b4c3..c128ae7b5 100644 --- a/lib/soap/mimemessage.rb +++ b/lib/soap/mimemessage.rb @@ -8,6 +8,7 @@ require 'soap/attachment' +require 'soap/soapversion' module SOAP @@ -147,11 +148,17 @@ def to_s end end - def initialize + # soap_version governs the media type this message's parts declare + # (root part and the outer multipart's own "type" parameter) -- defaults + # to SOAPVersion1_1 so parsing an incoming message (which doesn't care + # what media type the parts claim; see #root) and any other caller that + # never had a version to give aren't affected. + def initialize(soap_version = SOAPVersion1_1) @parts = [] @headers = Headers.new @root = nil @boundary = nil + @soap_version = soap_version end def self.parse(head, str) @@ -163,7 +170,7 @@ def self.parse(head, str) def close @headers.add( "Content-Type", - "multipart/related; type=\"text/xml\"; boundary=\"#{boundary}\"; start=\"#{@parts[0].contentid}\"" + "multipart/related; type=\"#{@soap_version.media_type}\"; boundary=\"#{boundary}\"; start=\"#{@parts[0].contentid}\"" ) end @@ -202,7 +209,7 @@ def boundary def add_part(content) part = Part.new part.headers.add("Content-Type", - "text/xml; charset=" + XSD::Charset.xml_encoding_label) + "#{@soap_version.media_type}; charset=" + XSD::Charset.xml_encoding_label) part.headers.add("Content-ID", Attachment.contentid(part)) part.body = content @parts.unshift(part) diff --git a/lib/soap/ns.rb b/lib/soap/ns.rb index 3b1db4e39..f80e595db 100644 --- a/lib/soap/ns.rb +++ b/lib/soap/ns.rb @@ -10,6 +10,7 @@ require 'xsd/datatypes' require 'xsd/ns' require 'soap/soap' +require 'soap/soapversion' module SOAP @@ -21,7 +22,8 @@ module SOAP class NS < XSD::NS KNOWN_TAG = XSD::NS::KNOWN_TAG.dup.update( - SOAP::EnvelopeNamespace => 'env' + SOAP::EnvelopeNamespace => 'env', + SOAP::SOAPVersion1_2.envelope_namespace => 'env' ) def initialize(tag2ns = nil) diff --git a/lib/soap/parser.rb b/lib/soap/parser.rb index e635a7323..5c6684430 100644 --- a/lib/soap/parser.rb +++ b/lib/soap/parser.rb @@ -9,6 +9,7 @@ require 'xsd/xmlparser' require 'soap/soap' +require 'soap/soapversion' require 'soap/ns' require 'soap/baseData' require 'soap/encodingstyle/handler' @@ -74,6 +75,7 @@ def update(ns, name, node, encodingstyle, handler) attr_accessor :default_encodingstyle attr_accessor :decode_typemap attr_accessor :allow_unqualified_element + attr_accessor :soap_version def initialize(opt = {}) @opt = opt @@ -82,7 +84,9 @@ def initialize(opt = {}) @recycleframe = nil @lastnode = nil @handlers = {} - @envelopenamespace = opt[:envelopenamespace] || EnvelopeNamespace + @soap_version = opt[:soap_version] || SOAPVersion1_1 + @envelopenamespace = + opt[:envelopenamespace] || @soap_version.envelope_namespace @default_encodingstyle = opt[:default_encodingstyle] || EncodingNamespace @decode_typemap = opt[:decode_typemap] || nil @allow_unqualified_element = opt[:allow_unqualified_element] || false @@ -202,7 +206,7 @@ def decode_text(ns, text, handler) def decode_soap_envelope(ns, ele, attrs, parent) o = nil if ele.name == EleEnvelope - o = SOAPEnvelope.new + o = SOAPEnvelope.new(nil, nil, @soap_version) if ext = @opt[:external_content] ext.each do |k, v| o.external_content[k] = v @@ -210,23 +214,24 @@ def decode_soap_envelope(ns, ele, attrs, parent) end elsif ele.name == EleHeader return nil unless parent.node.is_a?(SOAPEnvelope) - o = SOAPHeader.new + o = SOAPHeader.new(@soap_version) parent.node.header = o elsif ele.name == EleBody return nil unless parent.node.is_a?(SOAPEnvelope) - o = SOAPBody.new + o = SOAPBody.new(nil, false, @soap_version) parent.node.body = o elsif ele.name == EleFault + fault_class = (@soap_version == SOAPVersion1_2) ? SOAP12Fault : SOAPFault if parent.node.is_a?(SOAPBody) - o = SOAPFault.new + o = fault_class.new parent.node.fault = o elsif parent.node.is_a?(SOAPEnvelope) # live.com server returns SOAPFault as a direct child of SOAPEnvelope. # support it even if it's not spec compliant. warn("Fault must be a child of Body.") - body = SOAPBody.new + body = SOAPBody.new(nil, false, @soap_version) parent.node.body = body - o = SOAPFault.new + o = fault_class.new body.fault = o else return nil diff --git a/lib/soap/rpc/driver.rb b/lib/soap/rpc/driver.rb index dabd8c20c..30e1b46f0 100644 --- a/lib/soap/rpc/driver.rb +++ b/lib/soap/rpc/driver.rb @@ -30,6 +30,7 @@ class Driver attr_proxy :literal_mapping_registry, true attr_proxy :allow_unqualified_element, true attr_proxy :default_encodingstyle, true + attr_proxy :soap_version, true attr_proxy :generate_explicit_type, true attr_proxy :use_default_namespace, true attr_proxy :return_response_as_xml, true @@ -165,7 +166,7 @@ def set_wiredump_file_base(name) end def create_header(headers) - header = SOAPHeader.new() + header = SOAPHeader.new(@proxy.soap_version) headers.each do |content, mustunderstand, encodingstyle| header.add(SOAPHeaderItem.new(content, mustunderstand, encodingstyle)) end diff --git a/lib/soap/rpc/httpserver.rb b/lib/soap/rpc/httpserver.rb index b5123a38e..9df38723a 100644 --- a/lib/soap/rpc/httpserver.rb +++ b/lib/soap/rpc/httpserver.rb @@ -29,6 +29,7 @@ class HTTPServer < Logger::Application attr_proxy :literal_mapping_registry, true attr_proxy :generate_explicit_type, true attr_proxy :use_default_namespace, true + attr_proxy :soap_version, true def initialize(config) actor = config[:SOAPHTTPServerApplicationName] || self.class.name diff --git a/lib/soap/rpc/proxy.rb b/lib/soap/rpc/proxy.rb index 4d14cd2b7..6a13d7d47 100644 --- a/lib/soap/rpc/proxy.rb +++ b/lib/soap/rpc/proxy.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/processor' require 'soap/mapping' require 'soap/mapping/literalregistry' @@ -32,6 +33,7 @@ class Proxy attr_accessor :mandatorycharset attr_accessor :allow_unqualified_element attr_accessor :default_encodingstyle + attr_accessor :soap_version attr_accessor :generate_explicit_type attr_accessor :use_default_namespace attr_accessor :return_response_as_xml @@ -57,6 +59,7 @@ def initialize(endpoint_url, soapaction, options) # TODO: set to false by default or drop thie option in 1.6.0 @allow_unqualified_element = true @default_encodingstyle = nil + @soap_version = SOAPVersion1_1 @generate_explicit_type = nil @use_default_namespace = false @return_response_as_xml = false @@ -126,7 +129,8 @@ def call(name, *params) req_header = create_request_header req_body = SOAPBody.new( op_info.request_body(params, @mapping_registry, - @literal_mapping_registry, mapping_opt) + @literal_mapping_registry, mapping_opt), + false, @soap_version ) reqopt = create_encoding_opt( :soapaction => op_info.soapaction || @soapaction, @@ -167,14 +171,14 @@ def call(name, *params) end def route(req_header, req_body, reqopt, resopt) - req_env = ::SOAP::SOAPEnvelope.new(req_header, req_body) + req_env = ::SOAP::SOAPEnvelope.new(req_header, req_body, @soap_version) unless reqopt[:envelopenamespace].nil? set_envelopenamespace(req_env, reqopt[:envelopenamespace]) end reqopt[:external_content] = nil conn_data = marshal(req_env, reqopt) if ext = reqopt[:external_content] - mime = MIMEMessage.new + mime = MIMEMessage.new(@soap_version) ext.each do |k, v| mime.add_attachment(v.data) end @@ -182,8 +186,10 @@ def route(req_header, req_body, reqopt, resopt) mime.close conn_data.send_string = mime.content_str conn_data.send_contenttype = mime.headers['content-type'].str + conn_data.multipart = true end conn_data.soapaction = reqopt[:soapaction] + conn_data.soap_version = @soap_version conn_data = @streamhandler.send(@endpoint_url, conn_data) if conn_data.receive_string.empty? return nil @@ -243,7 +249,7 @@ def set_envelopenamespace(env, namespace) end def create_request_header - header = ::SOAP::SOAPHeader.new + header = ::SOAP::SOAPHeader.new(@soap_version) items = @headerhandler.on_outbound(header) items.each do |item| header.add(item.elename.name, item) @@ -304,6 +310,7 @@ def create_encoding_opt(hash = nil) opt[:default_encodingstyle] = @default_encodingstyle opt[:allow_unqualified_element] = @allow_unqualified_element opt[:generate_explicit_type] = @generate_explicit_type + opt[:soap_version] = @soap_version opt[:no_indent] = @options["soap.envelope.no_indent"] opt[:use_numeric_character_reference] = @options["soap.envelope.use_numeric_character_reference"] diff --git a/lib/soap/rpc/router.rb b/lib/soap/rpc/router.rb index 404472858..b310a831d 100644 --- a/lib/soap/rpc/router.rb +++ b/lib/soap/rpc/router.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/processor' require 'soap/mapping' require 'soap/mapping/literalregistry' @@ -33,6 +34,7 @@ class Router attr_accessor :generate_explicit_type attr_accessor :use_default_namespace attr_accessor :external_ces + attr_accessor :soap_version attr_reader :filterchain def initialize(actor) @@ -43,6 +45,7 @@ def initialize(actor) @generate_explicit_type = true @use_default_namespace = false @external_ces = nil + @soap_version = SOAPVersion1_1 @operation_by_soapaction = {} @operation_by_qname = {} @headerhandlerfactory = [] @@ -191,15 +194,16 @@ def route(conn_data) conn_data.is_nocontent = true conn_data else - body = SOAPBody.new(soap_response, conn_data.is_fault) - env = SOAPEnvelope.new(header, body) + body = SOAPBody.new(soap_response, conn_data.is_fault, @soap_version) + env = SOAPEnvelope.new(header, body, @soap_version) marshal(conn_data, env, default_encodingstyle) end end # Create fault response string. def create_fault_response(e) - env = SOAPEnvelope.new(SOAPHeader.new, SOAPBody.new(fault(e, nil), true)) + env = SOAPEnvelope.new(SOAPHeader.new(@soap_version), + SOAPBody.new(fault(e, nil), true, @soap_version), @soap_version) opt = {} opt[:external_content] = nil @filterchain.reverse_each do |filter| @@ -277,7 +281,7 @@ def lookup_operation(soapaction, body) end def call_headers(headerhandler) - header = ::SOAP::SOAPHeader.new + header = ::SOAP::SOAPHeader.new(@soap_version) items = headerhandler.on_outbound(header) items.each do |item| header.add(item.elename.name, item) @@ -292,6 +296,7 @@ def receive_headers(headerhandler, header) def unmarshal(conn_data) xml = nil opt = {} + opt[:soap_version] = @soap_version contenttype = conn_data.receive_contenttype if /#{MIMEMessage::MultipartContentType}/i =~ contenttype opt[:external_content] = {} @@ -316,7 +321,7 @@ def unmarshal(conn_data) end env = Processor.unmarshal(xml, opt) charset = opt[:charset] - conn_data.send_contenttype = "text/xml; charset=\"#{charset}\"" + conn_data.send_contenttype = @soap_version.build_content_type(charset) env end @@ -324,6 +329,7 @@ def marshal(conn_data, env, default_encodingstyle = nil) opt = {} opt[:external_content] = nil opt[:default_encodingstyle] = default_encodingstyle + opt[:soap_version] = @soap_version opt[:generate_explicit_type] = @generate_explicit_type opt[:use_default_namespace] = @use_default_namespace @filterchain.reverse_each do |filter| @@ -339,7 +345,7 @@ def marshal(conn_data, env, default_encodingstyle = nil) end def mimeize(conn_data, ext) - mime = MIMEMessage.new + mime = MIMEMessage.new(@soap_version) ext.each do |k, v| mime.add_attachment(v.data) end @@ -352,10 +358,18 @@ def mimeize(conn_data, ext) # Create fault response. def fault(e, wsdl_fault_details) - if e.is_a?(UnhandledMustUnderstandHeaderError) - faultcode = FaultCode::MustUnderstand + if @soap_version == SOAPVersion1_2 + if e.is_a?(UnhandledMustUnderstandHeaderError) + faultcode = FaultCode12::MustUnderstand + else + faultcode = FaultCode12::Receiver + end else - faultcode = FaultCode::Server + if e.is_a?(UnhandledMustUnderstandHeaderError) + faultcode = FaultCode::MustUnderstand + else + faultcode = FaultCode::Server + end end # If the exception represents a WSDL fault, the fault element should @@ -385,11 +399,15 @@ def fault(e, wsdl_fault_details) detail = SOAPString.new("failed to serialize detail object: #{$!}") end - SOAPFault.new( - SOAPElement.new(nil, faultcode), - SOAPString.new(e.to_s), - SOAPString.new(@actor), - detail) + if @soap_version == SOAPVersion1_2 + SOAP12Fault.new(faultcode, e.to_s, nil, @actor, detail) + else + SOAPFault.new( + SOAPElement.new(nil, faultcode), + SOAPString.new(e.to_s), + SOAPString.new(@actor), + detail) + end end def create_mapping_opt diff --git a/lib/soap/rpc/soaplet.rb b/lib/soap/rpc/soaplet.rb index 0a5deb21c..a0ce6b061 100644 --- a/lib/soap/rpc/soaplet.rb +++ b/lib/soap/rpc/soaplet.rb @@ -141,6 +141,14 @@ def setup_req(conn_data, req) conn_data.receive_string = req.body conn_data.receive_contenttype = req['content-type'] conn_data.soapaction = parse_soapaction(req.meta_vars['HTTP_SOAPACTION']) + # SOAP 1.2 has no separate SOAPAction header -- the action travels as + # a Content-Type parameter instead. Only consulted when the header is + # absent, so this is a pure fallback: 1.1 traffic (which always sends + # the header) is completely unaffected. + if conn_data.soapaction.nil? + conn_data.soapaction = + StreamHandler.parse_action_from_content_type(conn_data.receive_contenttype) + end end def setup_res(conn_data, req, res) diff --git a/lib/soap/soap.rb b/lib/soap/soap.rb index 4bf3c7fc9..5d63f81f0 100644 --- a/lib/soap/soap.rb +++ b/lib/soap/soap.rb @@ -99,17 +99,34 @@ class FaultError < Error attr_reader :faultactor attr_accessor :detail + # fault is either a SOAPFault (1.1's flat faultcode/faultstring/ + # faultactor/detail) or a SOAP12Fault (1.2's nested Code/Reason/Role/ + # Detail) -- exposed through this same set of reader names either way, + # since callers already rely on this shape and the two protocols' + # underlying fault semantics don't need identical Ruby types to be + # usable, just a reasonable value for each. code_value/reason_text/role + # are already the natural Ruby values (QName-or-string/string/string); + # faultcode/faultactor for 1.1 stay exactly as before (whatever + # SOAPFault#faultcode/#faultactor already returned). def initialize(fault) - @faultcode = fault.faultcode - @faultstring = fault.faultstring - @faultactor = fault.faultactor + if fault.is_a?(SOAP12Fault) + @faultcode = fault.code_value + @faultstring = fault.reason_text + @faultactor = fault.role + else + @faultcode = fault.faultcode + @faultstring = fault.faultstring + @faultactor = fault.faultactor + end @detail = fault.detail super(self.to_s) end def to_s str = nil - if @faultstring and @faultstring.respond_to?('data') + if @faultstring.is_a?(String) + str = @faultstring + elsif @faultstring and @faultstring.respond_to?('data') str = @faultstring.data end str || '(No faultstring)' diff --git a/lib/soap/soapversion.rb b/lib/soap/soapversion.rb new file mode 100644 index 000000000..d6f037786 --- /dev/null +++ b/lib/soap/soapversion.rb @@ -0,0 +1,87 @@ +# encoding: UTF-8 +# SOAP4R - SOAP protocol version bundle (1.1 vs 1.2). +# Copyright (C) 2000-2007 NAKAMURA, Hiroshi . + +# This program is copyrighted free software by NAKAMURA, Hiroshi. You can +# redistribute it and/or modify it under the same terms of Ruby's license; +# either the dual license version in 2003, or any later version. + + +require 'soap/soap' + + +module SOAP + + +# A SOAPVersion bundles the constants that differ between SOAP 1.1 and 1.2 +# (envelope namespace, and -- as later phases land -- encoding namespace, +# media type, fault codes, header role attribute, etc). It's resolved once +# per call, the same place @default_encodingstyle already is (Proxy/Router's +# opt hashes), never inferred from parsed content: SOAP version is a +# whole-message, driver-configured-in-advance choice, not a per-element, +# reactively-discovered one like encodingStyle. +class SOAPVersion + attr_reader :envelope_namespace + # The attribute a header block uses to address its intended recipient: + # SOAP 1.1 calls this "actor", SOAP 1.2 renamed it "role" and gave it + # slightly different semantics (a URI identifying a SOAP role rather than + # a specific node) -- both are exposed through this one field name since + # from the header-encoding machinery's point of view they serve the same + # structural purpose. + attr_reader :role_attr_name + # SOAP 1.2-only; nil under 1.1, since 1.1 has no equivalent concept. + attr_reader :relay_attr_name + attr_reader :mustunderstand_attr_name + attr_reader :media_type + # SOAP 1.2's HTTP binding folds the SOAPAction value into a Content-Type + # "action" parameter and drops the separate SOAPAction header entirely; + # 1.1 keeps them as two separate things. false under 1.1, true under 1.2. + attr_reader :action_in_content_type + + # Plain positional args with defaults, not Ruby keyword args (`x:`/`x: 1`) + # -- this project supports Ruby back to 1.8.7/1.9.3, which predate + # keyword-argument syntax entirely. + def initialize(envelope_namespace, role_attr_local_name, relay_attr_local_name = nil, + media_type = MediaType, action_in_content_type = false) + @envelope_namespace = envelope_namespace + @role_attr_name = XSD::QName.new(envelope_namespace, role_attr_local_name) + @relay_attr_name = + if relay_attr_local_name + XSD::QName.new(envelope_namespace, relay_attr_local_name) + end + @mustunderstand_attr_name = XSD::QName.new(envelope_namespace, 'mustUnderstand') + @media_type = media_type + @action_in_content_type = action_in_content_type + end + + # soapaction is only ever embedded when action_in_content_type is true + # (1.2) AND a soapaction is actually given -- under 1.1 (or when no + # soapaction applies, e.g. a response) this is just "; + # charset=", identical to what StreamHandler.create_media_type + # already built before this existed. + def build_content_type(charset, soapaction = nil) + ct = "#{@media_type}; charset=#{charset}" + if @action_in_content_type && soapaction + ct += %Q{; action="#{soapaction}"} + end + ct + end +end + +SOAPVersion1_1 = SOAPVersion.new(EnvelopeNamespace, 'actor') +SOAPVersion1_2 = SOAPVersion.new('http://www.w3.org/2003/05/soap-envelope', 'role', 'relay', + 'application/soap+xml', true) + +# SOAP 1.2's env:Fault/env:Code/env:Value content is a QName identifying +# the fault code; 1.1's Client/Server (SOAP::FaultCode in soap.rb) are +# renamed Sender/Receiver here, same VersionMismatch/MustUnderstand +# concepts otherwise. +module FaultCode12 + VersionMismatch = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'VersionMismatch').freeze + MustUnderstand = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'MustUnderstand').freeze + Sender = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'Sender').freeze + Receiver = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'Receiver').freeze +end + + +end diff --git a/lib/soap/streamHandler.rb b/lib/soap/streamHandler.rb index 7684e3c87..92c7c9428 100644 --- a/lib/soap/streamHandler.rb +++ b/lib/soap/streamHandler.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/httpconfigloader' require 'soap/httpbackend' require 'soap/filter/filterchain' @@ -35,6 +36,18 @@ class ConnectionData attr_accessor :is_fault attr_accessor :is_nocontent attr_accessor :soapaction + attr_accessor :soap_version + # True when send_string/send_contenttype are a MIME multipart message + # (SOAP-with-Attachments) -- send_post uses this to still emit the + # legacy SOAPAction header even under SOAP 1.2, where it's otherwise + # folded into the Content-Type's action parameter instead. That + # folding isn't attempted for the multipart case at all (see + # MIMEMessage): action placement for attachments+1.2 combined isn't + # settled by any spec this was checked against, and real-world use of + # that combination is rare enough that falling back to the + # widely-supported legacy header is the pragmatic choice, not a gap + # left to fix later. + attr_accessor :multipart def initialize(send_string = nil) @send_string = send_string @@ -44,6 +57,8 @@ def initialize(send_string = nil) @is_fault = false @is_nocontent = false @soapaction = nil + @soap_version = nil + @multipart = false end end @@ -51,17 +66,36 @@ def initialize @filterchain = Filter::FilterChain.new end + # Recognizes both SOAP 1.1's media type (text/xml) and 1.2's + # (application/soap+xml). Unlike the original 1.1-only version, this + # isn't fully anchored to the end of the string: a 1.2 Content-Type can + # carry an additional action="..." parameter alongside charset, in + # either order, which parse_action_from_content_type below extracts + # separately. def self.parse_media_type(str) - if /^#{ MediaType }(?:\s*;\s*charset=([^"]+|"[^"]+"))?$/i !~ str - return nil + return nil unless str + return nil unless /^(?:#{Regexp.escape(MediaType)}|application\/soap\+xml)\b/i =~ str + if /charset=("[^"]*"|[^;\s]+)/i =~ str + charset = $1 + charset = charset.gsub(/"/, '') end - charset = $1 - charset.gsub!(/"/, '') if charset charset || 'utf-8' end - def self.create_media_type(charset) - "#{ MediaType }; charset=#{ charset }" + # SOAP 1.2 only: the Content-Type "action" parameter that replaces the + # 1.1 SOAPAction header. nil if absent (1.1 traffic, or a 1.2 request + # with no action). + def self.parse_action_from_content_type(str) + return nil unless str + if /action=("[^"]*"|[^;\s]+)/i =~ str + $1.gsub(/"/, '') + end + end + + def self.create_media_type(charset, media_type = MediaType, action = nil) + ct = "#{ media_type }; charset=#{ charset }" + ct += %Q{; action="#{ action }"} if action + ct end def send(url, conn_data, soapaction = nil, charset = nil) @@ -201,7 +235,10 @@ def set_cookie_store_file(value) end def send_post(url, conn_data, charset) - conn_data.send_contenttype ||= StreamHandler.create_media_type(charset) + soap_version = conn_data.soap_version || SOAPVersion1_1 + conn_data.send_contenttype ||= + StreamHandler.create_media_type(charset, soap_version.media_type, + soap_version.action_in_content_type ? conn_data.soapaction : nil) if @wiredump_file_base filename = @wiredump_file_base + '_request.xml' @@ -212,7 +249,11 @@ def send_post(url, conn_data, charset) extheader = {} extheader['Content-Type'] = conn_data.send_contenttype - extheader['SOAPAction'] = "\"#{ conn_data.soapaction }\"" + # See ConnectionData#multipart's comment -- attachments always get the + # legacy header too, regardless of version. + if !soap_version.action_in_content_type || conn_data.multipart + extheader['SOAPAction'] = "\"#{ conn_data.soapaction }\"" + end extheader['Accept-Encoding'] = 'gzip' if send_accept_encoding_gzip? send_string = conn_data.send_string @wiredump_dev << "Wire dump:\n\n" if @wiredump_dev diff --git a/lib/soap/wsdlDriver.rb b/lib/soap/wsdlDriver.rb index be91e8572..2c21ed4fa 100644 --- a/lib/soap/wsdlDriver.rb +++ b/lib/soap/wsdlDriver.rb @@ -113,6 +113,9 @@ def find_port(servicename = nil, portname = nil) end def init_driver(drv, binding) + if binding.soapbinding && binding.soapbinding.soap12 + drv.soap_version = ::SOAP::SOAPVersion1_2 + end wsdl_elements = @wsdl.collect_elements wsdl_types = @wsdl.collect_complextypes + @wsdl.collect_simpletypes rpc_decode_typemap = wsdl_types + diff --git a/lib/wsdl/binding.rb b/lib/wsdl/binding.rb index b502641f3..8f2f94e31 100644 --- a/lib/wsdl/binding.rb +++ b/lib/wsdl/binding.rb @@ -38,8 +38,9 @@ def parse_element(element) o = OperationBinding.new @operations << o o - when SOAPBindingName + when SOAPBindingName, SOAP12BindingName o = WSDL::SOAP::Binding.new + o.soap12 = (element == SOAP12BindingName) @soapbinding = o o when DocumentationName diff --git a/lib/wsdl/data.rb b/lib/wsdl/data.rb index c7cc9eda9..829010d16 100644 --- a/lib/wsdl/data.rb +++ b/lib/wsdl/data.rb @@ -51,6 +51,19 @@ module WSDL SOAPFaultName = XSD::QName.new(SOAPBindingNamespace, 'fault') SOAPOperationName = XSD::QName.new(SOAPBindingNamespace, 'operation') +# SOAP 1.2 WSDL binding extension elements -- same local names, different +# namespace. Routed to the exact same WSDL::SOAP::* classes as their 1.1 +# counterparts at every dispatch point (binding.rb, operationBinding.rb, +# param.rb, port.rb, service.rb, soap/header.rb): only the recognized +# input namespace differs, the object model doesn't need to know or care +# which SOAP version bound a given WSDL operation. +SOAP12AddressName = XSD::QName.new(SOAP12BindingNamespace, 'address') +SOAP12BindingName = XSD::QName.new(SOAP12BindingNamespace, 'binding') +SOAP12HeaderName = XSD::QName.new(SOAP12BindingNamespace, 'header') +SOAP12BodyName = XSD::QName.new(SOAP12BindingNamespace, 'body') +SOAP12FaultName = XSD::QName.new(SOAP12BindingNamespace, 'fault') +SOAP12OperationName = XSD::QName.new(SOAP12BindingNamespace, 'operation') + BindingAttrName = XSD::QName.new(nil, 'binding') ElementAttrName = XSD::QName.new(nil, 'element') LocationAttrName = XSD::QName.new(nil, 'location') diff --git a/lib/wsdl/operationBinding.rb b/lib/wsdl/operationBinding.rb index fe2df5f03..11da42ff8 100644 --- a/lib/wsdl/operationBinding.rb +++ b/lib/wsdl/operationBinding.rb @@ -177,7 +177,7 @@ def parse_element(element) o = Param.new @fault << o o - when SOAPOperationName + when SOAPOperationName, SOAP12OperationName o = WSDL::SOAP::Operation.new @soapoperation = o o diff --git a/lib/wsdl/param.rb b/lib/wsdl/param.rb index 4736cd4f5..a98208b9b 100644 --- a/lib/wsdl/param.rb +++ b/lib/wsdl/param.rb @@ -55,15 +55,15 @@ def soapbody_encodingstyle def parse_element(element) case element - when SOAPBodyName + when SOAPBodyName, SOAP12BodyName o = WSDL::SOAP::Body.new @soapbody = o o - when SOAPHeaderName + when SOAPHeaderName, SOAP12HeaderName o = WSDL::SOAP::Header.new @soapheader << o o - when SOAPFaultName + when SOAPFaultName, SOAP12FaultName o = WSDL::SOAP::Fault.new @soapfault = o o diff --git a/lib/wsdl/port.rb b/lib/wsdl/port.rb index 79ca02c74..a6e2fe3e8 100644 --- a/lib/wsdl/port.rb +++ b/lib/wsdl/port.rb @@ -39,7 +39,7 @@ def find_binding def parse_element(element) case element - when SOAPAddressName + when SOAPAddressName, SOAP12AddressName o = WSDL::SOAP::Address.new @soap_address = o o diff --git a/lib/wsdl/service.rb b/lib/wsdl/service.rb index d89026efa..044da964a 100644 --- a/lib/wsdl/service.rb +++ b/lib/wsdl/service.rb @@ -36,7 +36,7 @@ def parse_element(element) o = Port.new @ports << o o - when SOAPAddressName + when SOAPAddressName, SOAP12AddressName o = WSDL::SOAP::Address.new @soap_address = o o diff --git a/lib/wsdl/soap/binding.rb b/lib/wsdl/soap/binding.rb index 1f679debe..250ba855d 100644 --- a/lib/wsdl/soap/binding.rb +++ b/lib/wsdl/soap/binding.rb @@ -17,11 +17,18 @@ module SOAP class Binding < Info attr_reader :style attr_reader :transport + # Set by WSDL::Binding#parse_element (lib/wsdl/binding.rb) based on + # whether the or element matched -- + # this class itself is identical either way, only the WSDL author's + # chosen namespace differs, so that's the one place that actually knows + # which one it was. + attr_accessor :soap12 def initialize super @style = nil @transport = nil + @soap12 = false end def parse_element(element) diff --git a/lib/wsdl/soap/data.rb b/lib/wsdl/soap/data.rb index 9bcdedd09..b45e6b5d6 100644 --- a/lib/wsdl/soap/data.rb +++ b/lib/wsdl/soap/data.rb @@ -25,6 +25,7 @@ module SOAP HeaderFaultName = XSD::QName.new(SOAPBindingNamespace, 'headerfault') +SOAP12HeaderFaultName = XSD::QName.new(SOAP12BindingNamespace, 'headerfault') LocationAttrName = XSD::QName.new(nil, 'location') StyleAttrName = XSD::QName.new(nil, 'style') diff --git a/lib/wsdl/soap/driverCreator.rb b/lib/wsdl/soap/driverCreator.rb index ca300bc4c..16ecb60eb 100644 --- a/lib/wsdl/soap/driverCreator.rb +++ b/lib/wsdl/soap/driverCreator.rb @@ -83,9 +83,12 @@ def dump_porttype(porttype) EOD wsdl_name = @definitions.name ? @definitions.name.name : 'default' mrname = safeconstname(wsdl_name + 'MappingRegistry') + soap_version_line = + binding.soapbinding.soap12 ? %Q[self.soap_version = ::SOAP::SOAPVersion1_2\n] : '' c.def_method("initialize", "endpoint_url = nil") do %Q[endpoint_url ||= DefaultEndpointUrl\n] + %Q[super(endpoint_url, nil)\n] + + soap_version_line + %Q[self.mapping_registry = #{mrname}::EncodedRegistry\n] + %Q[self.literal_mapping_registry = #{mrname}::LiteralRegistry\n] + %Q[init_methods] diff --git a/lib/wsdl/soap/header.rb b/lib/wsdl/soap/header.rb index 550dc12a0..3bf9b89af 100644 --- a/lib/wsdl/soap/header.rb +++ b/lib/wsdl/soap/header.rb @@ -52,7 +52,7 @@ def find_part def parse_element(element) case element - when HeaderFaultName + when HeaderFaultName, SOAP12HeaderFaultName o = WSDL::SOAP::HeaderFault.new @headerfault = o o diff --git a/lib/wsdl/wsdl.rb b/lib/wsdl/wsdl.rb index 7e293751b..2b32e0a45 100644 --- a/lib/wsdl/wsdl.rb +++ b/lib/wsdl/wsdl.rb @@ -18,6 +18,10 @@ module WSDL Namespace = 'http://schemas.xmlsoap.org/wsdl/' SOAPBindingNamespace ='http://schemas.xmlsoap.org/wsdl/soap/' +# From the "SOAP 1.2 Binding for WSDL 1.1" W3C submission, S2.1: the +# namespace implementations MUST use for soap12:binding/operation/body/ +# header/fault/address WSDL extension elements. +SOAP12BindingNamespace = 'http://schemas.xmlsoap.org/wsdl/soap12/' class Error < StandardError; include ::SOAP::NestedException; end diff --git a/test/soap/fault/test_soap12_fault_client.rb b/test/soap/fault/test_soap12_fault_client.rb new file mode 100644 index 000000000..394ae37d1 --- /dev/null +++ b/test/soap/fault/test_soap12_fault_client.rb @@ -0,0 +1,56 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 2 (continued): full client-side +# canned-envelope test mirroring test_fault.rb, but feeding a hand-built +# SOAP 1.2 fault envelope through a driver configured with +# soap_version = SOAPVersion1_2, confirming the raised FaultError carries +# the right message end to end (parser -> SOAP12Fault -> FaultError). +class TestSoap12FaultClient < Test::Unit::TestCase + def setup + @client = SOAP::RPC::Driver.new(nil, 'urn:fault') + @client.wiredump_dev = STDERR if $DEBUG + @client.soap_version = SOAP::SOAPVersion1_2 + @client.add_method("hello", "msg") + end + + def teardown + @client.reset_stream if @client + end + + def test_soap12_fault + @client.mapping_registry = SOAP::Mapping::EncodedRegistry.new + @client.test_loopback_response << <<__XML__ + + + + + env:Sender + + + DN cannot be empty + + + + +__XML__ + begin + @client.hello("world") + assert(false) + rescue ::SOAP::FaultError => e + assert_equal("DN cannot be empty", e.message) + assert_equal(SOAP::FaultCode12::Sender, e.faultcode) + end + end +end + + +end +end diff --git a/test/soap/fault/test_soap12_fault_router.rb b/test/soap/fault/test_soap12_fault_router.rb new file mode 100644 index 000000000..c2483ebc1 --- /dev/null +++ b/test/soap/fault/test_soap12_fault_router.rb @@ -0,0 +1,55 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/rpc/router' +require 'soap/mapping/mapping' +require 'soap/processor' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 2: wire SOAP12Fault into +# Router#create_fault_response/#fault and FaultError. Direct port of +# test_soaparray.rb's pattern (router.create_fault_response($!) -> +# Processor.unmarshal -> FaultError.new -> Mapping.fault2exception), +# with router.soap_version = SOAPVersion1_2 -- asserting Sender/Receiver +# fault codes appear, not Client/Server, and that the whole chain still +# ends up re-raising the original exception correctly. +class TestSoap12FaultRouter < Test::Unit::TestCase + def test_parse_fault_soap12 + router = SOAP::RPC::Router.new('parse_soap12_error') + router.soap_version = SOAP::SOAPVersion1_2 + soap_fault = pump_stack rescue router.create_fault_response($!) + env = SOAP::Processor.unmarshal(soap_fault.send_string, :soap_version => SOAP::SOAPVersion1_2) + assert_kind_of(SOAP::SOAP12Fault, env.body.fault) + assert_equal(SOAP::FaultCode12::Receiver, env.body.fault.code_value) + + soap_fault = SOAP::FaultError.new(env.body.fault) + assert_raises(RuntimeError) do + registry = SOAP::Mapping::LiteralRegistry.new + SOAP::Mapping.fault2exception(soap_fault, registry) + end + end + + def test_soap11_fault_router_unaffected + router = SOAP::RPC::Router.new('parse_soap11_error') + soap_fault = pump_stack rescue router.create_fault_response($!) + env = SOAP::Processor.unmarshal(soap_fault.send_string) + assert_kind_of(SOAP::SOAPFault, env.body.fault) + # Same pre-existing decode limitation documented on SOAP12Fault#code_value + # applies here too (not new): the wire value is a raw prefixed string, + # not resolved back to a QName. Confirms 1.1's own fault path (and its + # existing Client/Server codes) is untouched by the 1.2 work. + assert_equal("Server", env.body.fault.faultcode.data.split(':').last) + end + + def pump_stack(max = 0) + raise ArgumentError if max > 10 + pump_stack(max+1) + end +end + + +end +end diff --git a/test/soap/fault/test_soap12_fault_shape.rb b/test/soap/fault/test_soap12_fault_shape.rb new file mode 100644 index 000000000..d44d3fe75 --- /dev/null +++ b/test/soap/fault/test_soap12_fault_shape.rb @@ -0,0 +1,92 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' +require 'soap/rpc/element' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 1: prove the nested/qualified/ +# repeatable SOAP12Fault shape marshals and unmarshals correctly in +# isolation, before wiring it into router/exception machinery (that's +# test_soap12_fault_router.rb / test_soap12_fault_client.rb). SOAPFault +# itself is completely untouched by this -- confirmed separately by the +# existing test_fault.rb/test_customfault.rb/test_soaparray.rb suite +# still passing unmodified. +class TestSoap12FaultShape < Test::Unit::TestCase + def build_envelope(fault) + # Matches how router.rb actually builds a fault response + # (SOAPBody.new(fault_obj, true, soap_version)) rather than the + # decode-side body.fault= setter -- the latter forces the struct key + # (and, via dup_name, the wire tag) to the literal lowercase string + # "fault" regardless of the fault object's own elename, which is only + # fine for values that get read back out via accessors, not + # re-marshaled as-is. + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + body = SOAP::SOAPBody.new(fault, true, SOAP::SOAPVersion1_2) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_fault_with_subcode_and_role_round_trips + fault = SOAP::SOAP12Fault.new( + SOAP::FaultCode12::Sender, "Invalid request", XSD::QName.new("urn:myapp", "BadArgument"), + "http://example.com/myrole" + ) + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + + assert_match(/env:Code/, xml) + assert_match(/env:Value/, xml) + assert_match(/env:Subcode/, xml) + assert_match(/env:Reason/, xml) + assert_match(/env:Text xml:lang="en"/, xml) + assert_match(/env:Role/, xml) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + decoded = env.body.fault + # Standard fault codes (Sender/Receiver/etc.) always live in the 1.2 + # envelope namespace per spec, so code_value resolves back to a real + # QName even post-decode. Custom Subcodes can live in arbitrary + # app namespaces, which the generic literal-style decode path has no + # automatic QName-content resolution for (a pre-existing gap, not + # specific to fault codes) -- so code_subcode is the raw wire string + # ("prefix:localname") after a real decode, not a resolved QName. + assert_equal(SOAP::FaultCode12::Sender, decoded.code_value) + assert_equal("BadArgument", decoded.code_subcode.split(':').last) + assert_equal("Invalid request", decoded.reason_text) + assert_equal("http://example.com/myrole", decoded.role) + end + + def test_fault_without_subcode_or_role + fault = SOAP::SOAP12Fault.new(SOAP::FaultCode12::Receiver, "Server broke") + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + refute_match(/env:Subcode/, xml) + refute_match(/env:Role/, xml) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + decoded = env.body.fault + assert_equal(SOAP::FaultCode12::Receiver, decoded.code_value) + assert_nil(decoded.code_subcode) + assert_equal("Server broke", decoded.reason_text) + assert_nil(decoded.role) + end + + def test_fault_uses_sender_receiver_not_client_server + fault = SOAP::SOAP12Fault.new(SOAP::FaultCode12::Sender, "bad input") + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + assert_match(/Sender/, xml) + refute_match(/Client/, xml) + refute_match(/Server/, xml) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match #{string.inspect}") + end +end + + +end +end diff --git a/test/soap/swa/test_soap12_swa.rb b/test/soap/swa/test_soap12_swa.rb new file mode 100644 index 000000000..6b0be3b93 --- /dev/null +++ b/test/soap/swa/test_soap12_swa.rb @@ -0,0 +1,85 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' +require 'soap/attachment' +require 'stringio' + + +module SOAP +module SWA + + +# Confirms SOAP-with-Attachments (MIME multipart) honors soap_version -- a +# real gap found alongside SOAP 1.2 support: MIMEMessage previously +# hardcoded "text/xml" for both the root part and the outer multipart's +# own "type" parameter regardless of soap_version, clobbering whatever +# Content-Type the non-attachment path would have built (see +# lib/soap/mimemessage.rb). Also confirms the legacy SOAPAction header is +# still sent for attachment requests even under 1.2, where it's otherwise +# folded into the Content-Type's action parameter instead -- action +# placement for attachments+1.2 combined isn't settled by any spec this +# was checked against, so the widely-supported legacy header is kept as a +# pragmatic fallback rather than guessed at (see +# StreamHandler::ConnectionData#multipart's comment). +class TestSoap12Swa < Test::Unit::TestCase + Port = 17171 + + class SwAService + def put_file(name, file) + "File '#{name}' was received ok." + end + end + + def setup + @server = SOAP::RPC::StandaloneServer.new('SwAServer12', + 'http://www.acmetron.com/soap', '0.0.0.0', Port) + @server.add_servant(SwAService.new) + @server.level = Logger::Severity::ERROR + @server.soap_version = SOAP::SOAPVersion1_2 + @t = TestUtil.start_server_thread(@server) + @endpoint = "http://localhost:#{Port}/" + @client = SOAP::RPC::Driver.new(@endpoint, 'http://www.acmetron.com/soap') + @client.soap_version = SOAP::SOAPVersion1_2 + @client.add_method('put_file', 'name', 'file') + end + + def teardown + @server.shutdown if @server + if @t + unless @t.join(10) + @t.kill + @t.join + end + end + @client.reset_stream if @client + end + + def test_attachment_request_uses_soap12_media_type_and_keeps_soapaction_header + wire = StringIO.new + @client.wiredump_dev = wire + # A synthetic payload, not this test's own source -- attaching the + # latter would make refute_match's "text/xml" check below a false + # positive against this very file's own comments. + attachment = SOAP::Attachment.new(StringIO.new("attachment payload, unrelated to any media type string")) + result = @client.put_file('foo', attachment) + assert_equal("File 'foo' was received ok.", result) + + dump = wire.string + assert_match(/Content-Type:.*multipart\/related.*type="application\/soap\+xml"/i, dump) + assert_match(/SOAPAction:/i, dump) + assert_match(/Content-Type:\s*application\/soap\+xml;\s*charset/i, dump) + refute_match(/text\/xml/i, dump) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match #{string.inspect}") + end +end + + +end +end diff --git a/test/soap/version/test_soap12_envelope_roundtrip.rb b/test/soap/version/test_soap12_envelope_roundtrip.rb new file mode 100644 index 000000000..d605af099 --- /dev/null +++ b/test/soap/version/test_soap12_envelope_roundtrip.rb @@ -0,0 +1,59 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' + + +module SOAP + + +# Phase 1 of SOAP 1.2 support: envelope/header/body namespace switching via +# SOAPVersion1_2, no fault shape yet (that's Phase 2) and no HTTP transport +# changes yet (Phase 3). Fault-less SOAP 1.2 messages should already +# round-trip correctly through Processor.marshal/unmarshal at this point. +class TestSoap12EnvelopeRoundtrip < Test::Unit::TestCase + def build_envelope + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + body = SOAP::SOAPBody.new( + SOAP::SOAPElement.new(XSD::QName.new("my:foo", "bodyitem"), 'bi'), + false, SOAP::SOAPVersion1_2 + ) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_marshal_uses_soap12_namespace_only + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + assert_match(/xmlns:env="http:\/\/www\.w3\.org\/2003\/05\/soap-envelope"/, xml) + refute_match(/#{Regexp.escape(SOAP::EnvelopeNamespace)}/, xml) + end + + def test_unmarshal_roundtrip + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + assert_equal('http://www.w3.org/2003/05/soap-envelope', env.elename.namespace) + assert_equal('http://www.w3.org/2003/05/soap-envelope', env.body.elename.namespace) + assert_equal('bi', env.body.root_node.data) + end + + # Documents the existing, pre-SOAP-1.2 contract: an envelope in a + # namespace the Parser wasn't configured to recognize was never a clean + # error (this predates soap_version entirely -- any unrecognized + # envelope namespace falls through to decode_tag's generic + # encoding-style handler instead of decode_soap_envelope). soap_version + # being "explicit opt-in only" rides on that existing behavior rather + # than needing anything new: a 1.2 envelope fed to a default (1.1) + # Parser is simply never recognized as an envelope at all. + def test_soap12_envelope_not_recognized_by_default_1_1_parser + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + result = SOAP::Processor.unmarshal(xml, {}) + assert_not_equal(SOAP::SOAPEnvelope, result.class) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match") + end +end + + +end diff --git a/test/soap/version/test_soap12_http_headers.rb b/test/soap/version/test_soap12_http_headers.rb new file mode 100644 index 000000000..28e99f199 --- /dev/null +++ b/test/soap/version/test_soap12_http_headers.rb @@ -0,0 +1,63 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/streamHandler' + + +module SOAP + + +# Phase 3 of SOAP 1.2 support: HTTP binding. SOAP 1.2 replaces the +# separate SOAPAction header with an action="..." parameter folded into +# Content-Type (application/soap+xml instead of text/xml). Unit-level +# coverage for the building blocks; test_soap12_http_roundtrip.rb covers +# a real client -> WEBrick server -> back round trip. +class TestSoap12HttpHeaders < Test::Unit::TestCase + def test_soap11_content_type_never_embeds_action + ct = SOAP::SOAPVersion1_1.build_content_type("utf-8", "urn:foo:bar") + assert_equal("text/xml; charset=utf-8", ct) + end + + def test_soap12_content_type_embeds_action + ct = SOAP::SOAPVersion1_2.build_content_type("utf-8", "urn:foo:bar") + assert_equal('application/soap+xml; charset=utf-8; action="urn:foo:bar"', ct) + end + + def test_soap12_content_type_omits_action_when_absent + ct = SOAP::SOAPVersion1_2.build_content_type("utf-8") + assert_equal("application/soap+xml; charset=utf-8", ct) + end + + def test_parse_media_type_handles_both_versions + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("text/xml; charset=utf-8")) + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("application/soap+xml; charset=utf-8")) + # action can appear before or after charset + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type( + 'application/soap+xml; action="urn:foo"; charset=utf-8')) + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type( + 'application/soap+xml; charset=utf-8; action="urn:foo"')) + end + + def test_parse_media_type_defaults_charset_when_absent + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("text/xml")) + end + + def test_parse_media_type_rejects_unrelated_content_type + assert_nil(SOAP::StreamHandler.parse_media_type("application/json")) + end + + def test_parse_action_from_content_type + assert_equal("urn:foo:bar", SOAP::StreamHandler.parse_action_from_content_type( + 'application/soap+xml; charset=utf-8; action="urn:foo:bar"')) + assert_nil(SOAP::StreamHandler.parse_action_from_content_type("text/xml; charset=utf-8")) + assert_nil(SOAP::StreamHandler.parse_action_from_content_type(nil)) + end + + def test_create_media_type_backward_compatible_two_arg_form + # the pre-1.2 call site (charset only) must keep producing exactly + # what it always did. + assert_equal("text/xml; charset=utf-8", SOAP::StreamHandler.create_media_type("utf-8")) + end +end + + +end diff --git a/test/soap/version/test_soap12_http_roundtrip.rb b/test/soap/version/test_soap12_http_roundtrip.rb new file mode 100644 index 000000000..279822200 --- /dev/null +++ b/test/soap/version/test_soap12_http_roundtrip.rb @@ -0,0 +1,58 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' + + +module SOAP + + +# Phase 3 of SOAP 1.2 support: full live HTTP round trip (client driver +# -> real WEBrick standalone server -> back) under soap_version = +# SOAPVersion1_2 on both ends -- proves setup_req's Content-Type action +# fallback (soaplet.rb) actually dispatches correctly with a real HTTP +# stack involved, not just that the pieces compile. +class TestSoap12HttpRoundtrip < Test::Unit::TestCase + Port = 17171 + + class EchoServer < SOAP::RPC::StandaloneServer + def on_init + add_method(self, 'echo', 'msg') + end + + def echo(msg) + msg + end + end + + def setup + @server = EchoServer.new('soap12echo', 'urn:soap12echo', '0.0.0.0', Port) + @server.level = Logger::Severity::ERROR + @server.soap_version = SOAP::SOAPVersion1_2 + @t = TestUtil.start_server_thread(@server) + @endpoint = "http://localhost:#{Port}/" + @client = SOAP::RPC::Driver.new(@endpoint, 'urn:soap12echo') + @client.soap_version = SOAP::SOAPVersion1_2 + @client.wiredump_dev = STDERR if $DEBUG + @client.add_method("echo", "msg") + end + + def teardown + @server.shutdown if @server + if @t + unless @t.join(10) + @t.kill + @t.join + end + end + @client.reset_stream if @client + end + + def test_soap12_round_trip_dispatches_via_content_type_action + assert_equal("hello", @client.echo("hello")) + end +end + + +end diff --git a/test/soap/version/test_soap12_role_relay.rb b/test/soap/version/test_soap12_role_relay.rb new file mode 100644 index 000000000..80b6afc36 --- /dev/null +++ b/test/soap/version/test_soap12_role_relay.rb @@ -0,0 +1,52 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' + + +module SOAP + + +class TestSoap12RoleRelay < Test::Unit::TestCase + ROLE = 'http://www.w3.org/2003/05/soap-envelope/role/next' + + def build_envelope_with_header_item + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + item_element = SOAP::SOAPElement.new(XSD::QName.new("my:foo", "auth"), 'secret') + header.add("auth", item_element) + header_item = header.instance_variable_get(:@data).last + header_item.actor = ROLE + header_item.relay = true + body = SOAP::SOAPBody.new(nil, false, SOAP::SOAPVersion1_2) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_role_and_relay_round_trip + xml = SOAP::Processor.marshal(build_envelope_with_header_item, :soap_version => SOAP::SOAPVersion1_2) + assert_match(/env:role="#{Regexp.escape(ROLE)}"/, xml) + assert_match(/env:relay="true"/, xml) + assert_nil(/actor=/.match(xml)) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + item = env.header.instance_variable_get(:@data).first + assert_equal(ROLE, item.actor) + assert_equal(true, item.relay) + end + + def test_soap11_header_item_never_emits_role_or_relay + header = SOAP::SOAPHeader.new + item_element = SOAP::SOAPElement.new(XSD::QName.new("my:foo", "auth"), 'secret') + header.add("auth", item_element) + header_item = header.instance_variable_get(:@data).last + header_item.actor = 'http://example.com/actor' + header_item.relay = true # should be silently ignored -- no such concept under 1.1 + body = SOAP::SOAPBody.new + env = SOAP::SOAPEnvelope.new(header, body) + + xml = SOAP::Processor.marshal(env, {}) + assert_match(/env:actor="http:\/\/example\.com\/actor"/, xml) + assert_nil(/relay=/.match(xml)) + end +end + + +end diff --git a/test/soap/version/test_soap_version_default.rb b/test/soap/version/test_soap_version_default.rb new file mode 100644 index 000000000..2de82ad76 --- /dev/null +++ b/test/soap/version/test_soap_version_default.rb @@ -0,0 +1,62 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' +require 'soap/rpc/driver' +require 'soap/rpc/router' + + +module SOAP + + +# Phase 0 of SOAP 1.2 support: SOAPVersion plumbing only, no visible +# behavior change yet. This is the regression pin for that guarantee -- +# with soap_version left untouched anywhere, marshal/unmarshal must behave +# exactly as it did before SOAPVersion existed, since SOAPVersion1_1's +# envelope_namespace is built from the very same EnvelopeNamespace constant +# Parser/Proxy/Router already defaulted to. +class TestSoapVersionDefault < Test::Unit::TestCase + def build_envelope + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(SOAP::SOAPElement.new(XSD::QName.new("my:foo", "bodyitem"), 'bi')) + SOAP::SOAPEnvelope.new(header, body) + end + + def test_marshal_identical_with_or_without_explicit_soap_version_1_1 + implicit = SOAP::Processor.marshal(build_envelope, {}) + explicit = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_1) + assert_equal(implicit, explicit) + assert_match(/xmlns:env="#{Regexp.escape(SOAP::EnvelopeNamespace)}"/, implicit) + end + + def test_unmarshal_identical_with_or_without_explicit_soap_version_1_1 + xml = SOAP::Processor.marshal(build_envelope, {}) + implicit = SOAP::Processor.unmarshal(xml, {}) + explicit = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_1) + assert_equal(SOAP::EnvelopeNamespace, implicit.elename.namespace) + assert_equal(SOAP::EnvelopeNamespace, explicit.elename.namespace) + end + + def test_parser_default_soap_version_is_1_1 + parser = SOAP::Parser.new + assert_equal(SOAP::SOAPVersion1_1, parser.soap_version) + assert_equal(SOAP::EnvelopeNamespace, parser.envelopenamespace) + end + + def test_driver_proxy_soap_version_accessor_defaults_and_is_settable + driver = SOAP::RPC::Driver.new("http://localhost:17171/") + assert_equal(SOAP::SOAPVersion1_1, driver.soap_version) + driver.soap_version = SOAP::SOAPVersion1_2 + assert_equal(SOAP::SOAPVersion1_2, driver.soap_version) + assert_equal(SOAP::SOAPVersion1_2, driver.proxy.soap_version) + end + + def test_router_soap_version_accessor_defaults_and_is_settable + router = SOAP::RPC::Router.new('test_router') + assert_equal(SOAP::SOAPVersion1_1, router.soap_version) + router.soap_version = SOAP::SOAPVersion1_2 + assert_equal(SOAP::SOAPVersion1_2, router.soap_version) + end +end + + +end diff --git a/test/wsdl/soap/soap12/soap12.wsdl b/test/wsdl/soap/soap12/soap12.wsdl new file mode 100644 index 000000000..82c74a65a --- /dev/null +++ b/test/wsdl/soap/soap12/soap12.wsdl @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/wsdl/soap/soap12/test_soap12_roundtrip.rb b/test/wsdl/soap/soap12/test_soap12_roundtrip.rb new file mode 100644 index 000000000..404c3fd07 --- /dev/null +++ b/test/wsdl/soap/soap12/test_soap12_roundtrip.rb @@ -0,0 +1,69 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'wsdl/parser' +require 'soap/rpc/standaloneServer' +require 'soap/wsdlDriver' + + +module WSDL; module SOAP; module T_Soap12Roundtrip + + +# Phase 4 of SOAP 1.2 support, the strongest test in the whole plan: +# following test_soapenc.rb's template, proves a SOAP 1.2-bound WSDL +# doesn't just parse (test_soap12_binding_parse.rb already covers that) +# but produces a driver that completes a real, wire-correct SOAP 1.2 RPC +# call end to end -- WSDLDriverFactory#create_rpc_driver must actually +# detect binding.soapbinding.soap12 and configure the resulting driver's +# soap_version accordingly (lib/soap/wsdlDriver.rb#init_driver), or this +# fails despite the WSDL parsing perfectly fine. +class TestSoap12Roundtrip < Test::Unit::TestCase + class Server < ::SOAP::RPC::StandaloneServer + def on_init + add_rpc_method(self, 'echo', 'msg') + self.soap_version = ::SOAP::SOAPVersion1_2 + end + + def echo(msg) + msg + end + end + + DIR = File.dirname(File.expand_path(__FILE__)) + Port = 17171 + + def setup + @server = Server.new('Test', 'urn:soap12echo', '0.0.0.0', Port) + @server.level = Logger::Severity::ERROR + @server_thread = TestUtil.start_server_thread(@server) + @client = nil + end + + def teardown + @server.shutdown if @server + unless @server_thread.join(10) + @server_thread.kill + @server_thread.join + end + @client.reset_stream if @client + end + + def test_soap12_bound_wsdl_drives_a_real_call + wsdl = File.join(DIR, 'soap12.wsdl') + factory = ::SOAP::WSDLDriverFactory.new(wsdl) + @client = factory.create_rpc_driver + assert_equal(::SOAP::SOAPVersion1_2, @client.soap_version) + @client.endpoint_url = "http://localhost:#{Port}/" + @client.wiredump_dev = STDOUT if $DEBUG + ret = @client.echo("hello") + # RPC-style single-part response wrapping is an existing, orthogonal + # WSDL/RPC message-mapping behavior, not a SOAP 1.2 concern -- the + # actual point of this test is everything upstream: the request made + # it over the wire framed as SOAP 1.2 and the server correctly + # dispatched and responded. + assert_equal(["hello"], ret) + end +end + + +end; end; end diff --git a/test/wsdl/soap/test_soap12_binding_parse.rb b/test/wsdl/soap/test_soap12_binding_parse.rb new file mode 100644 index 000000000..40779d9bc --- /dev/null +++ b/test/wsdl/soap/test_soap12_binding_parse.rb @@ -0,0 +1,38 @@ +# encoding: UTF-8 +require 'helper' +require 'wsdl/importer' + + +module WSDL +module SOAP + + +# Phase 4 of SOAP 1.2 support: WSDL binding-namespace recognition. Proves +# the six case/when dispatch points (binding.rb, operationBinding.rb, +# param.rb, port.rb, service.rb, soap/header.rb) actually recognize +# etc -- the fixture (soap12/soap12.wsdl) uses the SOAP +# 1.2 WSDL binding namespace exclusively, no at all, so +# this only passes if every one of those six arms works. +class TestSoap12BindingParse < Test::Unit::TestCase + DIR = File.dirname(File.expand_path(__FILE__)) + + def test_soap12_binding_recognized + wsdl = WSDL::Importer.import(File.join(DIR, 'soap12', 'soap12.wsdl')) + binding = wsdl.bindings.find { |b| b.name.name == 'soap12echo_binding' } + assert_not_nil(binding) + assert_not_nil(binding.soapbinding) + assert_equal(true, binding.soapbinding.soap12) + assert_equal(:rpc, binding.soapbinding.style) + + operation = binding.operations.find { |op| op.name == 'echo' } + assert_not_nil(operation.soapoperation) + + port = wsdl.services[0].ports[0] + assert_not_nil(port.soap_address) + assert_equal('http://localhost:17171/', port.soap_address.location) + end +end + + +end +end