Talking with a SOAP service using Savon gem in Ruby - ruby

I'm trying to communicate with a soap service and I know that I should send a SOAP Envelope like this:
POST /webpay_test/SveaWebPay.asmx HTTP/1.1
Host: webservices.sveaekonomi.se
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "https://webservices.sveaekonomi.se/webpay/CreateOrder"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<CreateOrder xmlns="https://webservices.sveaekonomi.se/webpay">
<request>
<Order>
<ClientOrderNr>string</ClientOrderNr>
<CustomerReference>string</CustomerReference>
<OrderDate>dateTime</OrderDate>
<CountryCode>string</CountryCode>
<SecurityNumber>string</SecurityNumber>
<CustomerEmail>string</CustomerEmail>
<IsCompany>boolean</IsCompany>
<PreApprovedCustomerId>long</PreApprovedCustomerId>
<AddressSelector>string</AddressSelector>
</Order>
<InvoiceRows>
<ClientInvoiceRowInfo>
<ArticleNr>string</ArticleNr>
<Description>string</Description>
<PricePerUnit>double</PricePerUnit>
<NrOfUnits>double</NrOfUnits>
<Unit>string</Unit>
<VatPercent>int</VatPercent>
<DiscountPercent>int</DiscountPercent>
<ClientOrderRowNr>int</ClientOrderRowNr>
</ClientInvoiceRowInfo>
<ClientInvoiceRowInfo>
<ArticleNr>string</ArticleNr>
<Description>string</Description>
<PricePerUnit>double</PricePerUnit>
<NrOfUnits>double</NrOfUnits>
<Unit>string</Unit>
<VatPercent>int</VatPercent>
<DiscountPercent>int</DiscountPercent>
<ClientOrderRowNr>int</ClientOrderRowNr>
</ClientInvoiceRowInfo>
</InvoiceRows>
</request>
</CreateOrder>
</soap:Body>
</soap:Envelope>
here is the code I've wrote:
client = Savon::Client.new("https://webservices.sveaekonomi.se/webpay_test/SveaWebPay.asmx?wsdl")
res = client.create_order do |soap|
soap.namespace = "https://webservices.sveaekonomi.se/webpay_test/CreateOrder.asmx"
soap.body = { :auth => { :username => "username", :password => "pass", :client_number => "1111" },
:order => { :client_order_nr => "1000000", :customer_reference => "4212", :order_date => Date.today,
:country_code => "SE", :security_number => "1111111111", :is_company => false,
:customer_email => "me#gmail.com", :pre_approved_customer_id => 0 },
:invoice_rows => { :client_invoice_row_info => { :article_nr => "x100", :description => "something cool -- description",
:price_per_unit => 100, :nr_of_units => 3, :unit => "SEK", :vat_percent => 25,
:discount_percent => 0, :client_order_row_nr => "1"},
:client_invoice_row_info => { :article_nr => "x200", :description => "something cooler -- description",
:price_per_unit => 200, :nr_of_units => 2, :unit => "SEK", :vat_percent => 25,
:discount_percent => 0, :client_order_row_nr => "1" }
}
}
end
and it generates this, which is different from what I have as the template and that's why I'm getting an error:
<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:wsdl="https://webservices.sveaekonomi.se/webpay_test/CreateOrder.asmx" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Body>
<wsdl:CreateOrder>
<invoiceRows>
<clientInvoiceRowInfo>
<clientOrderRowNr>1</clientOrderRowNr>
<pricePerUnit>200</pricePerUnit>
<nrOfUnits>2</nrOfUnits>
<unit>SEK</unit>
<vatPercent>25</vatPercent>
<articleNr>x200</articleNr>
<discountPercent>0</discountPercent>
<description>something cooler -- description</description>
</clientInvoiceRowInfo>
</invoiceRows>
<order>
<customerEmail>me#gmail.com</customerEmail>
<preApprovedCustomerId>0</preApprovedCustomerId>
<countryCode>SE</countryCode>
<clientOrderNr>1000000</clientOrderNr>
<securityNumber>11111111</securityNumber>
<customerReference>4212</customerReference>
<isCompany>false</isCompany>
<orderDate>2010-06-28</orderDate>
</order>
<auth>
<password>pass</password>
<clientNumber>1111</clientNumber>
<username>username</username>
</auth>
</wsdl:CreateOrder>
</env:Body>
</env:Envelope>
and here is the response:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<CreateOrderResponse xmlns="https://webservices.sveaekonomi.se/webpay">
<CreateOrderResult>
<Accepted>false</Accepted>
<ErrorMessage>Failed to create or authorize order</ErrorMessage>
<SveaOrderNr>0</SveaOrderNr>
<RejectionCode>Error</RejectionCode>
<WillBuyInvoices xsi:nil="true" />
<AuthorizeId>0</AuthorizeId>
<AuthorizedAmount xsi:nil="true" />
<ExpirationDate xsi:nil="true" />
</CreateOrderResult>
</CreateOrderResponse>
</soap:Body>
</soap:Envelope>
could anyone tell me how can I solve this problem. and since I'm a newbie when it comes to SOAP would you also tell me if the order of the xml tags in the soap:Body tag is important or not?

Thanks to Steve, I found "Why is “wsdl” namespace interjected into action name when using savon for ruby soap communication?" where Nick and Steve were talking about a similar problem.
Like Nick, my problem was in the way Savon is cooking up a SOAP envelope. As recommended by Nick, I ended up monkey patching a couple of methods in the Savon SOAP class. It's in lib/savon/soap.rb
and I'm good to go now.
I'm a novice when it comes to SOAP and it's my first time writing a SOAP client, but honestly it SUCKS! I still remember my first time writing a client for a REST service and gosh it was fun.
REST ROCKS, SOAP SUCKS. that's all!

You are missing the <request> element.
Try replacing your soap.body with a single hash with a key of ::request and a value of the existing hash record that you have already.
EDIT 1:
Your namespace line within your code should be "https://webservices.sveaekonomi.se/webpay" not the full URL you have there currently.

Related

Replicating XML Request with Savon/Ruby

I am trying to avoid using Nokogiri/Builder to build my XML and would like to instead use the Savon gem with Ruby 2.0.0. I have the following request I need to replicate:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<GetList xmlns="http://tempuri.org/">
<listRequest xmlns:a="http://schemas.datacontract.org/2004/07/Services.List"
i:type="b:NpsListRequest"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:b="http://schemas.datacontract.org/2004/07/Services.List.Strategies">
<a:id>1</a:id>
</listRequest>
</GetList>
</s:Body>
</s:Envelope>
So far I have this:
def soap_client
soap_client = Savon.client(
wsdl: "http://10.10.10.10/ListApi.svc?wsdl"
headers: {"Authorization" => "Basic"},
basic_auth: ['username', 'password'],
env_namespace: :s,
ssl_verify_mode: :none,
log: true,
:pretty_print_xml => true
)
end
Then soap_client.call :get_list, message: {'id' => 1} which returns this:
<s:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tns="http://tempuri.org/"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<tns:GetList>
<id>1</id>
</tns:GetList>
</s:Body>
</s:Envelope>
I can't figure out how to replicate the first request exactly. the tns: namespace on GetList is wrong, and I can't replicate the <listRequest xmlns:a = piece either. Any thoughts on how to do this within Savon?
The namespace on GetList is correct.
What you probably need to write is
soap_client.call(:get_list,
:attributes => {'xmlns:b'=>'http://schemas.datacontract.org/'},
message: { 'ListRequest' => { 'tns:id' => 1 } }
That won't be the exact solution for your problem, because I don't have access to your wsdl and can't test. But you should get the key to a solution.

Constructing this SOAP header properly?

I'm attempting to build a proper SOAP header:
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sen="https://webservices.averittexpress.com/SendWebImageService">
<soapenv:Header
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ns:authnHeader
soapenv:mustUnderstand="0"
xmlns:ns="http://webservices.averittexpress.com/authn">
<Username>xxxxxxxx</Username> <Password>xxxxxxxx</Password>
</ns:authnHeader> </soapenv:Header>
This is my Savon client call, I'm using version 2:
client = Savon.client(
wsdl: api_url,
raise_errors: false,
convert_request_keys_to: :camelcase,
element_form_default: :qualified,
env_namespace: :soapenv,
soap_header: {'username' => username, 'password' => password }
)
I'm getting the following SOAP error:
fault=>
{:faultcode=>"soapenv:Server",
:faultstring=>"java.lang.NullPointerException",
:detail=>nil}}
How do I get the sapoenv:mustUserstand="0" line, and what is it?
Plus I'm confused how to set the xmlns:ns="http://webservices.averittexpress.com/authn">.
I've little experience using SOAP requests, and am coming from a Ruby/Rails RESTful background. Any help would be appreciated.
Savon uses Gyoku to convert both SOAP header and body to XML.
Following this libraries conventions, here's what your Hash needs to look like:
soap_header = {
"ns:authnHeader" => {
"#soapenv:mustUnderstand" => 0,
"#xmlns:ns" => "http://webservices.averittexpress.com/authn",
"Username" => "x",
"Password" => "x"
}
}
Savon.client(:soap_header => soap_header)

Remove namespace from Savon SOAP request

I've this code that uses Savon that I'm trying get to work.
require "savon"
require "time"
Gyoku.configure do |config|
config.convert_symbols_to(&:tap)
end
client = Savon::Client.new("https://www.cfhdocmail.com/TestAPI2/DMWS.asmx?WSDL")
response = client.request(:CreateMailing) do
soap.body = {
Username: "Username",
Password: "Password",
ProductType: "A4Letter",
IsMono: false,
IsDuplex: true,
DespatchASAP: false,
DespatchDate: Time.parse("2012-04-09"),
AddressNameFormat: "Full Name",
ReturnFormat: "JSON"
}
end
The output looks like this
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wsdl="https://www.cfhdocmail.com/LiveAutoAPI/" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ins0="https://www.cfhdocmail.com/LiveAutoAPI/">
<env:Body>
<ins0:CreateMailing>
<ins0:Username>Username</ins0:Username>
<ins0:Password>Password</ins0:Password>
<ins0:ProductType>A4Letter</ins0:ProductType>
<ins0:IsMono xsi:nil="true" />
<ins0:IsDuplex>true</ins0:IsDuplex>
<ins0:DespatchASAP xsi:nil="true" />
<ins0:DespatchDate>2012-04-09 00:00:00 +0200</ins0:DespatchDate>
<ins0:AddressNameFormat>Full Name</ins0:AddressNameFormat>
<ins0:ReturnFormat>JSON</ins0:ReturnFormat>
</ins0:CreateMailing>
</env:Body>
</env:Envelope>
But I want it to look like this.
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<CreateMailing xmlns="https://www.cfhdocmail.com/LiveAutoAPI/">
<Username>Username</Username>
<Password>Password</Password>
<ProductType>A4Letter</ProductType>
<IsMono/>true</IsMono>
<IsDuplex>true</IsDuplex>
<DespatchASAP>false</DespatchASAP>
<DespatchDate>2012-04-09 00:00:00 +0200</DespatchDate>
<AddressNameFormat>Full Name</AddressNameFormat>
<ReturnFormat>JSON</ReturnFormat>
</CreateMailing>
</soap:Body>
</soap:Envelope>
How do I do that?
I'm getting this error at the moment
Server did not recognize the value of HTTP Header SOAPAction: CreateMailing. (Savon::SOAP::Fault)
These changes solved my problem.
client.request(:web, "CreateMailer") do |soap|
soap.input = [
"CreateMailer", {
xmlns: "https://www.cfhdocmail.com/LiveAutoAPI/"
}
]
soap.element_form_default = :unqualified
soap.body = {
Username: "Username",
Password: "Password",
ProductType: "A4Letter",
IsMono: false,
IsDuplex: true,
DespatchASAP: false,
DespatchDate: Time.parse("2012-04-09"),
AddressNameFormat: "Full Name",
ReturnFormat: "JSON"
}
client.http.headers.delete("SOAPAction")
end
Thanks #sluukkonen for the help.
There might be a better way to do it, but the only way I could figure out how to get the XML body exactly how I needed it was to build my own XML. I have a to_xml method in my model and it gets called like soap.body = { to_xml }.
def to_xml
xml = Builder::XmlMarkup.new(indent: 2)
xml.instruct!
xml.Username "Username"
xml.Password "Password"
end

How to create urn:localhost-catalog SOAP header in Ruby?

I have this (working) example in php:
$usernameToken = new SoapHeaderUsernameToken( $password, $username );
$soapHeaders[] = new SoapHeader("urn:localhost-catalog", 'UsernameToken', $usernameToken);
$client = new SoapClient("my_client_url");
$client->__setSoapHeaders( $soapHeaders );
But I want a client in Ruby. I'm using the savon gem, which can do authentication for you. So far, I have the following method:
def my_soap_client
client = Savon::Client.new do
wsdl.document = "my_client_url"
end
client.wsse.credentials "usr", "pass" # Adding :digest doesn't seem to help
return client
end
# I make requests like this:
my_soap_client.request :wsdl, "getProductGroups", :parent_id => 1
I can successfully list available actions, but if I try to make a request, the result is always "(SOAP-ENV:Client) Missing parameter". Also, it doesn't matter if I pass correct credentials or fake ones. That is why I believe the authentication fails.
Hope someone can help!
Using Wireshark sniffer I intercepted SOAP request that your PHP code sends. It looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="urn:examples:helloservice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ns2="urn:localhost-catalog"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Header>
<ns2:UsernameToken>
<Password>pass</Password>
<Username>login</Username>
</ns2:UsernameToken>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<ns1:getProductGroups>
<parentId>1</parentId>
</ns1:getProductGroups>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Here you can see that this code adds ns2 namespace for UsernameToken header.
Then I intercepted SOAP request that sends your Ruby code:
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:wsdl="http://www.ecerami.com/wsdl/HelloService.wsdl"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken-1"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:Username>usr</wsse:Username>
<wsse:Password
Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">
pass
</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</env:Header>
<env:Body>
<wsdl:getProductGroups>
<parentId>1</parentId>
</wsdl:getProductGroups>
</env:Body>
</env:Envelope>
You can see the difference - UsernameToken is wrapped into Security element and namespace for UsernameToken is "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
Your PHP code uses custom authorization and savon does not support it via wsse property. I looked into Savon source code, it uses Akami.wsse and in its code there is no possibility to set namespace. This means you need to manually construct headers. Here is the code in Ruby that generates SOAP request like you possibly need:
require 'savon'
client = Savon::Client.new do
wsdl.document = "test.wsdl"
end
client.request :wsdl, "sayHello" do
soap.namespaces["xmlns:ns2"] = "urn:localhost-catalog"
soap.header = { "ns2:UsernameToken" =>
{"Password" => "pass", "Username" => "login"}
}
soap.body = { :parent_id => 1 }
end
It generate the following SOAP request:
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:wsdl="http://www.ecerami.com/wsdl/HelloService.wsdl"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns2="urn:localhost-catalog">
<env:Header>
<ns2:UsernameToken>
<Password>pass</Password>
<Username>login</Username>
</ns2:UsernameToken>
</env:Header>
<env:Body>
<wsdl:sayHello>
<parentId>1</parentId>
</wsdl:sayHello>
</env:Body>
</env:Envelope>
Hope this helps!
to set a header in Savon use the following expression:
client.request :wsdl, "getProductGroups" do
soap.header = { :header_key => "your header goes here",
:attributes! => { :attr => "your attribute" }
}
soap.body = { :parent_id => 1 }
end
perhaps that helps you.
-st

Savon Ruby Soap Client - can't create a soap document correctly

I admit I'm new to both SOAP and Savon, but I got it working with another service, but this one is stumping me.
The WSDL is here:
http://stg-wholesale.carsdirect.com/ws/services/Ping?WSDL
The Document when done should look like this:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<dealerPing xmlns="http://wholesale.carsdirect.com">
<request>
<partner>
<pricePlan>integer</pricePlan>
<id>integer</id>
</partner>
<vehicle>
<year>integer</year>
<make>string</make>
<model>string</model>
<trim>string</trim>
</vehicle>
<zipcode>integer</zipcode>
</request>
</dealerPing>
</soap:Body>
</soap:Envelope>
If anyone can get close to generating a document like this or give me some pointers I would greatly appreciate it.
I tried doing something like :
client = Savon::Client.new{|wsdl| wsdl.document = #cp.wsdl}
#response = client.dealer_ping do |soap|
...
end
But, I got this error : undefined method `dealer_ping' for #
I'm pretty much stuck at this point. My knowledge of both parts of this equation is lacking.
Thanks.
I don't have access to the service, so I can't try if it works ... but this should probably work:
client = Savon::Client.new do
wsdl.endpoint = "http://stg-wholesale.carsdirect.com/ws/services/Ping"
wsdl.namespace = "http://wholesale.carsdirect.com"
end
client.request(:dealer_ping) do
soap.body = {
:request => {
:partner => {
:price_plan => 123,
:id => 1222
},
:vehicle => {
:year => 2010,
:make => "Aston Martin",
:model => "DBS",
:trim => ""
},
:zipcode => 90245
}
}
end

Resources