Wednesday, May 6, 2009

Invalid Search

‹prev | My Chain | next›

The last scenario in the "Recipe Search" scenario is "Invalid search parameters":



The first undefined step, When I search for "", looks suspiciously defined:
When /^I search for "(.+)"$/ do |keyword|
@query = "/recipes/search?q=#{keyword}"
visit(@query)
end
Ah, I used the one-or-more regular expression operator (+) in there, not expecting to match an empty string. Changing it to zero-or-more (*) gets that step working. Working, but not passing:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features \
-s "Invalid search parameters"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Invalid search parameters
Given 5 "Yummy" recipes
And a 0.5 second wait to allow the search index to be updated
When I search for ""
HTTP status code 400 (RestClient::RequestFailed)
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
./features/support/../../eee.rb:35:in `GET /recipes/search'
(eval):7:in `get'
features/recipe_search.feature:114:in `When I search for ""'
Then I should see no results
When I seach for a pre-ascii character "%1F"
Then I should see no results
And an empty query string
When I search for an invalid lucene search term like "title:ingredient:egg"
Then I should see no results
And an empty query string

1 scenario
1 failed step
3 skipped steps
4 undefined steps
2 passed steps
To fix the error, I need to move into the application specs and code. Specifically, I need to handle 400 errors from CouchDB/couchdb-lucene.

There is no sense in preventing the requests from going through to CouchDB. The requests are non-destructive GETs. Besides, I might be able to blacklist a handful of invalid searches, but blacklists never work in the long run. Instead I will handle errors from the RestClient gracefully:
    it "should treat couchdb errors as no-results" do
RestClient.stub!(:get).
and_raise(Exception)

get "/recipes/search?q=title:egg"

response.should contain("No results")
end
A begin/rescue block around the RestClient call will get this passing:
  begin
data = RestClient.get couchdb_url
@results = JSON.parse(data)
rescue Exception
@results = { 'total_rows' => 0 }
end
This also satisfies the requirement of the scenario step.

So now, it is on to trying to break the search with non-ascii characters. Unfortunately, this does not work in the least:
    When I seach for a pre-ascii character "%1F"
bad URI(is not URI?): /recipes/search?q= (URI::InvalidURIError)
/usr/lib/ruby/1.8/uri/common.rb:436:in `split'
/usr/lib/ruby/1.8/uri/common.rb:485:in `parse'
/usr/lib/ruby/1.8/uri/common.rb:608:in `URI'
(eval):7:in `get'
features/recipe_search.feature:116:in `When I seach for a pre-ascii character "%1F"'
It seems that URU::parse, upon which Webrat relies, does not work with invalid characters:
>> URI::parse("http://example.org/\t")
URI::InvalidURIError: bad URI(is not URI?): http://example.org/
from /usr/lib/ruby/1.8/uri/common.rb:436:in `split'
from /usr/lib/ruby/1.8/uri/common.rb:485:in `parse'
from (irb):16
Ah well, I will have to remove that step and hope that the earlier invalid / RestClient::RequestFailed rescue will cover this case as well (it should).

So the next step in the scenario is to see an empty query string. When a search fails, it is good form to provide a search field with the failing query string for editing by the user. Adding this, and then checking for an empty string, is a nice way to verify this step:
Then /^I should see an empty query string$/ do
response.should have_selector("input[@name=query][@value='']")
end
Making this example pass is done with some simple Haml:
%form{:method => "get", :action => "/recipes/search"}
%input{:type => "text", :value => @query, :name => "query", :size => 31, :maxlength => 2048}
%input{:type => "submit", :value => "Search", :name => "s"}
Last up is to handle invalid couchdb-lucene query strings (the double fielded "title:ingredient:egg" is used). As written, the scenario treats such a query as empty when displaying the "try again" no results page. It may be better form to display a helpful warning to the user, but this should be OK—users should not be mucking with couchdb-lucene fielded search much, if at all.

The example in the Sintra application code borrows from the empty query string scenario to verify the query string displayed to the user:
    it "should treat couchdb-lucene errors as an empty query" do
RestClient.stub!(:get).
and_raise(Exception)

get "/recipes/search?q=title:egg"

response.should have_selector("input[@name=query][@value='']")
end
Getting this example to pass is a simple matter of setting the @query to the empty string when handing RestClient exceptions:
  begin
data = RestClient.get couchdb_url
@results = JSON.parse(data)
rescue Exception
@query = ""
@results = { 'total_rows' => 0 }
end
That code not only gets the Sinatra application's specification to pass, but also the scenario as well:
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Invalid search parameters
Given 5 "Yummy" recipes
And a 0.5 second wait to allow the search index to be updated
When I search for ""
Then I should see no results
And I should see an empty query string
When I search for an invalid lucene search term like "title:ingredient:egg"
Then I should see no results
And I should see an empty query string

1 scenario
8 passed steps

(commit)

That about does it for the recipe search scenarios. Some clean-up work will be required. Tomorrow.

No comments:

Post a Comment