Monday, August 31, 2009

Ruby Web Services

What a saga. I spent a chunk of this weekend working on a test for a SOAP web service.

(NOTE: For those of you suggesting RESTful is the way and the light, that's great, but in this case exposing a SOAP web service was an explicit requirement. So SOAP it is.)

Here's the setup:
Here's how I eventually got it working:

Prep cucumber
I assume you have at least one other cucumber test, so I'm not going to tell you how to set that up.

Install gems
Install the following gems:
  • datanoise-actionwebservice (2.3.2)
  • httpclient (2.1.5.2)
  • soap4r (1.5.8).
Also install any other gems your app needs.

Create cucumber files
Create a feature file of the form .feature. You can find the API name by looking in app/services/. My service is app/services/foo_bar_api.rb, so my feature is foo_bar.feature.

Create a steps file of the form _steps.rb. My steps file is named foo_bar_steps.rb.

Create base cucumber test
I freely admit this part isn't quite baked yet. My feature so far looks like this:

Web Services clients will need to talk to us from time to time. They should be able to communicate via SOAP
interfaces.

Story: Authenticate
As an anonymous thing
I want to activate
So that I can be active
Scenario: Activate
Given an "anonymous" thing
When I activate
Then I should receive an activation key

As you can see, I only have one scenario. My steps file looks like this:

require 'rubygems'
gem 'soap4r'
require 'soap/wsdlDriver'

Given /^an "([^\"]*)" appliance$/ do |type|
puts "okay we got it"
end

When /^I activate$/ do
wsdl = "http://localhost:3000/Foo_bar/wsdl"
user = 'catherine'
password = 'password'
driver = SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
driver.options["protocol.http.basic_auth"] << [wsdl, user, password]
#driver.wiredump_dev = STDOUT
result = driver.activation("desc", "12345")
puts result
end

Then /^I should receive an activation key$/ do
puts "say hooray"
end

Also, obviously, not fully implemented. Stay with me, though. The important bits are at the top and in the "When" area. Let's talk about what I'm doing here:
  • I'm explicitly using the gem version of soap4r (the line "gem 'soap4r'"). That way I don't wind up using the version that shipped with Ruby.
  • When I define my wsdl, I'm using the format "http:/myserver/myapiname/wsdl". That seems to be the default place that ActionWebService puts it.
  • Note that I'm not actually using ActionWebService (we only installed it because the server needs it); I'm only using soap4r.
  • This web service is requires basic authentication. The "driver.options" line takes care of that.
  • I'm using the dynamically generated method definitions (that soap4r gets from reading the wsdl). Thus "activation" is actually the name of the exposed SOAP method I'm calling. If, for example, my SOAP server exposed a method "horse", I would call "driver.horse(params)".

Okay, moving on.

Configure the environment
Add these two lines to your environment.rb:
require 'rubygems'
gem 'soap4r'

This is so that the gem version of soap4r will load instead of the version that ships with Ruby.

Prep the Server
Before running the tests, you have to make sure that you have a user that can authenticate. Load this with factories, fixtures, mocks, a database insert statement, whatever, just as long as it's the username and password you specified in your steps file.

Then start your server. I've just been doing "script/server" in another terminal. There's probably a better way, but I haven't solved that problem yet.

Run Your Test
You're finally ready to run it. Use your preferred method. I used:
rake features FEATURE=features/cloudswitch_home.feature

And it should run (assuming your web service actually works!).

Lastly, here are a few of the mistakes I made along the way:
  1. Case matters, sort of. When you specify the wsdl, it doesn't have to match the case in your controller. However, httpclient will eventually turn this into an all-lowercase URL. If your URI is not all lowercase at that point, you'll get a 500 (internal server error) and it won't pass in your authentication information. (I had an ill-placed hard coding. And yes, I know I shouldn't have had it.)
  2. Ruby comes with a version of soap4r. Install the gem anyway and make sure you use that. The Ruby version of soap4r threw all kinds of errors for me.
  3. When you call the parameters, pass them in as direct arguments rather than as a hash. (i.e., call "driver.activation("desc", "12345")" rather than "driver.activation(:desc => "desc", :code => "12345")"). If you do the latter, it starts throwing errors like this: "wrong number of arguments (1 for 2) (ArgumentError)"
  4. If you see "uninitialized constant XSD::NS::KNOWN_TAG (NameError)" , it means you are using the Ruby soap4r instead of the gem. Put "gem 'soap4r'" in your environment.rb.
This is all still very rough, and I'll refine it as I get an actual test going. I did want to share, though. Thanks to the many blogs/API documentation/samples I found in Google for the assistance.

4 comments:

  1. Around 2006 I built a successful regression test suite before porting a production SOAP server to a new platform. I spent a lot of time getting help on the soap4r mail list, especially from Hiroshi Nakamura and Emil Marceta.

    http://chrismcmahonsblog.blogspot.com/2006/06/ruby-soap-is-amazing.html

    http://chrismcmahonsblog.blogspot.com/2006/03/soap-basic-authentication-in-ruby.html

    ReplyDelete
  2. I'm pretty sure I used those blog posts to help (among others). So a belated, thank you, Chris!

    ReplyDelete
  3. Hi...i don't now, how can i set a basic Authentication for my web service?? or how can i receive the user Information (username, password) from the client??
    i use:
    .
    .
    driver = SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
    driver.options["protocol.http.basic_auth"] << [wsdl, user, password]
    .
    .

    Thanks im Advance.

    ReplyDelete
  4. Anonymous, I'm not quite sure I understand your question. Are you creating a service and you want people connecting to it to use basic_auth? Or are you consuming a SOAP service that requires basic_auth?

    If it's the latter, that code should work, as long as you have all your variables set correctly and you make sure you are using the latest version of the gems (including net/http).

    ReplyDelete