How to test posts in Rails / Capybara / Cucumber or Rspec - ruby-on-rails

I'm using rspec, cucumber and capybara and I'm looking for a way to test that a malicious user can't hack a form then post to an url he/she doesn't have permission to. I have my permissions set up in cancan such that this "should" work, however, the only way I can test it is by hacking a form myself.
How can I automate this sort of testing? With webrat I could do this in a unit test with rspec with something like
put :update, :user_id => #user.id, :id => #user_achievement.id
response.should contain("Error, you don't have permission to access that!")
In capybara, however, visit only does get's it seems. I can't find a way to do this, I've googled everwhere.
Any help would be much appreciated,
Thanks

I think you can do this with rack-test
https://github.com/brynary/rack-test
in your Gemfile:
gem 'rack-test'
in your env.rb file
module CapybaraApp
def app; Capybara.app; end
end
World(CapybaraApp)
World(Rack::Test::Methods)
step defintions somewhere:
When /^I send a POST request to "([^"]*)"$/ do |path|
post path
end
Most of what I learned came from here: http://www.anthonyeden.com/2010/11/testing-rest-apis-with-cucumber-and-rack-test
UPDATE: I think you can skip the changes to your env.rb file with newer versions of Rails and/or Cucumber (not sure which, I just don't do that part on my newer projects and it works fine)

Same as #Josh Crews I've largely based this off of: http://www.anthonyeden.com/2010/11/testing-rest-apis-with-cucumber-and-rack-test/#comment-159. But there are two notable exceptions: 1) I test the actual response body, 2) I demonstrate how to test a POST request. Here's an example using Rails 3.0.9:
Steps:
# features/step_definitions/api_step.feature
When /^I send a GET request to "([^\"]*)"$/ do |url|
authorize(User.last.email, "cucumber")
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
get url
end
When /^I send a POST request to "([^\"]*)" with:$/ do |url, body|
authorize(User.last.email, "cucumber")
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
post url, body
end
Then /^the JSON response should have (\d+) "([^\"]*)" elements$/ do |number_of_children, name|
page = JSON.parse(last_response.body)
page.map { |d| d[name] }.length.should == number_of_children.to_i
end
Then /^I should receive the following JSON response:$/ do |expected_json|
expected_json = JSON.parse(expected_json)
response_json = JSON.parse(last_response.body)
response_json.should == expected_json
end
Then /^I should receive the following JSON object response:$/ do |expected_json|
expected_json = JSON.parse(expected_json)
response_json = JSON.parse(last_response.body)
if expected_json['id'] == 'RESPONSE_ID'
expected_json['id'] = response_json['id']
end
response_json.should == expected_json
end
Feature:
# features/api/some_feature.feature
Feature: Users API
Background:
Given the following users exist:
| id | name |
| 1 | Joe |
| 2 | Sue |
| 3 | Paul |
Scenario: Index action
When I send a GET request to "/users/"
Then the JSON response should have 3 "user" elements
And I should receive the following JSON response:
"""
[
{
"id":1,
"name":"Joe"
},
{
"id":2,
"name":"Sue"
},
{
"id":3,
"name":"Paul"
}
]
"""
Scenario: Create action
When I send a POST request to "/users/" with:
"""
{
"name":"Polly"
}
"""
Then I should receive the following JSON object response:
"""
{
"id":"RESPONSE_ID",
"name":"Polly"
}
"""
And I send a GET request to "/users/"
And the JSON response should have 4 "user" elements

Related

How do I test whether a Sidekiq worker is sending the right data to an external API?

I have a Sidekiq worker that reaches out to an external API to get some data back. I am trying to write tests to make sure that this worker is designed and functioning correctly. The worker grabs a local model instance and examines two fields on the model. If one of the fields is nil, it will send the other field to the remote API.
Here's the worker code:
class TokenizeAndVectorizeWorker
include Sidekiq::Worker
sidekiq_options queue: 'tokenizer_vectorizer', retry: true, backtrace: true
def perform(article_id)
article = Article.find(article_id)
tokenizer_url = ENV['TOKENIZER_URL']
if article.content.nil?
send_content = article.abstract
else
send_content = article.content
end
# configure Faraday
conn = Faraday.new(tokenizer_url) do |c|
c.use Faraday::Response::RaiseError
c.headers['Content-Type'] = 'application/x-www-form-urlencoded'
end
# get the response from the tokenizer
resp = conn.post '/tokenize', "content=#{URI.encode(send_content)}"
# the response's body contains the JSON for the tokenized and vectorized article content
article.token_vector = resp.body
article.save
end
end
I want to write a test to ensure that if the article content is nil that the article abstract is what is sent to be encoded.
My assumption is that the "right" way to do this would be to mock responses with Faraday such that I expect a specific response to a specific input. By creating an article with nil content and an abstract x I can mock a response to sending x to the remote API, and mock a response to sending nil to the remote API. I can also create an article with x as the abstract and z as the content and mock responses for z.
I have written a test that generically mocks Faraday:
it "should fetch the token vector on ingest" do
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
# stub Faraday to return something without making a real request
allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(
double('response', status: 200, body: "some data")
)
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: Faker::Lorem.paragraphs(number: 5).join("<br>"),
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should've saved "some data" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("some data")
end
But this mocks 100% of uses of Faraday with :post. In my particular case, I have no earthly idea how to mock a response of :post with a specific body...
It's also possible that I'm going about testing this all wrong. I could be instead testing that we are sending the right content (the test should check what is being sent with Faraday) and completely ignoring the right response.
What is the correct way to test that this worker does the right thing (sends content, or sends abstract if content is nil)? Is it to test what's being sent, or test what we are getting back as a reflection of what's being sent?
If I should be testing what's coming back as a reflection of what's being sent, how do I mock different responses from Faraday depending on the value of something being sent to it/
** note added later **
I did some more digging and thought, OK, let me test that I'm sending the request I expect, and that I'm processing the response correctly. So, I tried to use webmock.
it "should fetch token vector for article content when content is not nil" do
require 'webmock/rspec'
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url)
.with(body: 'content=y')
.to_return(body: 'y')
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: "y",
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should have sent a request with content=y
expect(stub).to have_been_requested
# we should've saved "y" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("y")
end
But I think that webmock isn't getting picked up inside the sidekiq job, because I get this:
1) Article tokenization and vectorization should fetch token vector for article content when content is not nil
Failure/Error: expect(stub).to have_been_requested
The request POST https://zzzzz/tokenize with body "content=y" was expected to execute 1 time but it executed 0 times
The following requests were made:
No requests were made.
============================================================
If I try to include webmock/rspec in any of the other places, for example, at the beginning of my file, random things start to explode. For example, if I have these lines in the beginning of this spec file:
require 'spec_helper'
require 'rails_helper'
require 'sidekiq/testing'
require 'webmock/rspec'
Then I get:
root#c18df30d6d22:/usr/src/app# bundle exec rspec spec/models/article_spec.rb:174
database: test
Run options: include {:locations=>{"./spec/models/article_spec.rb"=>[174]}}
There was an error creating the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
There was an error removing the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
Which I am guessing is because the test suite is trying to initialize stuff, but webmock is interfering...
I ended up abandoning Faraday and a more complicated test as an approach. I decomposed the worker into both a Service class and a worker. The worker simply invokes the Service class. This allows me to test the service class directly, and then just validate that the worker calls the service class correctly, and that the model calls the worker correctly.
Here's the much simpler service class:
require 'excon'
# this class is used to call out to the tokenizer service to retrieve
# a tokenized and vectorized JSON to store in an article model instance
class TokenizerVectorizerService
def self.tokenize(content)
tokenizer_url = ENV['TOKENIZER_URL']
response = Excon.post("#{tokenizer_url}/tokenize",
body: URI.encode_www_form(content: content),
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
expects: [200])
# the response's body contains the JSON for the tokenized and vectorized
# article content
response.body
end
end
Here's the test to see that we are calling the right destination:
require 'rails_helper'
require 'spec_helper'
require 'webmock/rspec'
RSpec.describe TokenizerVectorizerService, type: :service do
describe "tokenize" do
it "should send the content passed in" do
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url).
with(
body: {"content"=>"y"},
headers: {
'Content-Type'=>'application/x-www-form-urlencoded',
}).
to_return(status: 200, body: "y", headers: {})
TokenizerVectorizerService.tokenize("y")
expect(stub).to have_been_requested
end
end
end

expected 422 got 200 Cucumber with rails

Hi i am working on a ROR project with ruby-2.5.1 and rails 5. I am using cucumber in my rails app to test api i am new with cucumber. when i am trying to define feature for invalid data i am getting the error expected 422 got 200.
my feature file:
Feature: Registration Endpoint
Scenario: User registration
Given an application with application_id "1"
When the client make a valid POST /registartions request with application_id: "1"
Then response should have status 200
Scenario: using blank application id
When the client make a POST /registartions request with blank application-id
Then response should have status 422 and JSON:
"""
{ "error": "application_id does not exists" }
"""
my steps file:
Given("an application with application_id {string}") do |string|
string
end
When("the client make a valid POST \/registartions request with application_id: {string}") do |string|
params = {
"data":{
"type":"users",
"attributes":{
"email": "s2#gmail.com",
"password":"password",
"password-confirmation":"password"
}
}
}
header 'application-id', "#{string}"
post '/api/registrations', params
end
Then("response should have status {int}") do |int|
expect(last_response.status).to be(int)
end
When("the client make a POST \/registartions request with blank application-id") do
params = {
"data":{
"type":"users",
"attributes":{
"email": "s2#gmail.com",
"password":"password",
"password-confirmation":"password"
}
}
}
header 'application-id', ''
post '/api/registrations', params
end
Then("response should have status {int} and JSON:") do |int, string|
expect(last_response.status).to be(int)
end
Please help me to fix this issue i am writting this cucumber first time so i don't have the idea how to test with invalid data. Please help me. Thanks in advance.
It looks like you may have found a bug in your application.
If it is meant to respond with a 422 "Unprocessable Entity" response when you don't include the application id, and it's responding with a 200 (OK), then that would seem like the system under test has an issue.

Multiple HTTP requests matchable in one VCR cassette for rspec tests

I have a spec file with an expectation that a controller action will return success.
The POST api/v1/users/:id/features/block action in the controller calls two HTTP calls on an external API, the only difference being in the body.
I've put the two requests and responses in the same VCR cassette, but when the cassette is being used, only the first request ever gets compared against and fails when it should be matching the second, causing the tests to fail.
What I'm looking for is a way of having the multiple requests match so the controller action completes and returns successfully.
The error I'm getting is at the end.
describe "POST /api/v1/users/:id/features/block" do
before(:each) do
#user = FactoryGirl.create(:user)
post :block, user_id: #user.id, block: "0"
end
it "should return 200 OK" do
expect(response).to be_success
end
end
Simplified versions of my VCR configuration and RSpec configuration follow:
VCR.configure do |c|
c.hook_into :webmock
c.default_cassette_options = {
match_requests_on: [:method, :uri, :body_regex]
}
c.register_request_matcher :body_regex do |request_1, request_2|
# Match body against regex if cassette body is "--ruby_regex /regexhere/"
if request_2.body[/^--ruby_regex\s*\//]
regex = request_2.body.gsub(/^--ruby_regex\s*\//, '').gsub(/\/$/, '')
request_1.body[/#{regex}/] ? true : false
else
true # No regex defined, continue processing
end
end
end
RSpec.configure do |c|
c.around(:each) do |example|
options = example.metadata[:vcr] || {}
name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_")
VCR.use_cassette(name, options, &example)
end
end
end
A summarized version of the cassette being used in this comparison that I'm having trouble with is:
---
http_interactions:
- request:
method: post
uri: https://upstream/api
body:
string: --ruby_regex /query1.+block/
response:
status:
code: 200
body:
string: { "response": "SUCCESS" }
- request:
method: post
uri: https://upstream/api
body:
string: --ruby_regex /query2.+block/
response:
status:
code: 200
body:
string: { "response": "SUCCESS" }
recorded_at: Fri, 05 Sep 2014 08:26:12 GMT
recorded_with: VCR 2.8.0
Error during tests:
An HTTP request has been made that VCR does not know how to handle
...
VCR is using the current cassette: (Correct cassette file path)
...
Under the current configuration VCR can not find a suitable HTTP interaction to replay and is prevented from recording new requests.
I don't want to record new requests because then the second one overwrites the first instead of adding the second request to the end of the cassette.

How Can I Tell Controller Specs to Use the Signed OAuth Request

I am building a 2-Legged OAuth provider for my api. Everything is hooked up properly and I can make signed calls from the rails console. The problem I have is that I am having trouble integrating OAuth into the controller_spec.
Here is an example of a working call on my server:
coneybeare $ rails c test
Loading test environment (Rails 3.2.0)
rails test: main
>> consumer = OAuth::Consumer.new("one_key", "MyString", :site => [REDACTED])
# => #<OAuth::Consumer:0x007f9d01252268 #key="one_key", #secret="MyString", #options={:signature_method=>"HMAC-SHA1", :request_token_path=>"/oauth/request_token", :authorize_path=>"/oauth/authorize", :access_token_path=>"/oauth/access_token", :proxy=>nil, :scheme=>:header, :http_method=>:post, :oauth_version=>"1.0", :site=>[REDACTED]}>
ruby: main
>> req = consumer.create_signed_request(:get, "/api/v1/client_applications.json", nil)
# => #<Net::HTTP::Get GET>
ruby: main
>> res = Net::HTTP.start([REDACTED]) {|http| http.request(req) }
# => #<Net::HTTPOK 200 OK readbody=true>
ruby: main
>> puts res.body
{"client_applications":[{"id":119059960,"name":"FooBar1","url":"http://test1.com"},{"id":504489040,"name":"FooBar2","url":"http://test2.com"}]}
# => nil
And here is what I am doing in my controller tests:
require 'oauth/client/action_controller_request'
describe Api::ClientApplicationsController do
include OAuthControllerSpecHelper
…
…
it "assigns all client_applications as #client_applications" do
consumer = OAuth::Consumer.new("one_key", "MyString", :site => [REDACTED])
ActionController::TestRequest.use_oauth=true
#request.configure_oauth(consumer)
#request.apply_oauth!
puts "request.env['Authorization'] = #{#request.env['Authorization']}"
get :index, {:api_version => 'v1', :format => :json}
response.should be_success # Just this for now until I can get authorization, then proper controller testing
end
end
The output of that test:
request.env['Authorization'] = OAuth oauth_consumer_key="one_key", oauth_nonce="gzAbvBSWyFtIYKfuokMAdu6VnH39EHeXvebbH2qUtE", oauth_signature="juBkJo5K0WLu9mYqHVC3Ar%2FATUs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1328474800", oauth_version="1.0"
1) Api::ClientApplicationsController GET index assigns all client_applications as #client_applications
Failure/Error: response.should be_success
expected success? to return true, got false
And the corresponding server call from the rails log:
Processing by Api::ClientApplicationsController#index as JSON
Parameters: {"api_version"=>1}
Rendered text template (0.0ms)
Filter chain halted as #<OAuth::Controllers::ApplicationControllerMethods::Filter:0x007f85a51a8858 #options={:interactive=>false, :strategies=>:two_legged}, #strategies=[:two_legged]> rendered or redirected
Completed 401 Unauthorized in 15ms (Views: 14.1ms | ActiveRecord: 0.0ms)
(0.2ms) ROLLBACK
I just can't figure out why it's not working :/ Am I making an obvious mistake?
If you'd like to test it in a request spec and actually need to test without stubbing, you can build an OAuth consumer and sign a request like this:
#access_token = FactoryGirl.create :access_token
#consumer = OAuth::Consumer.new(#access_token.app.key, #access_token.app.secret, :site => "http://www.example.com/")
#path = "/path/to/request"
#request = #consumer.create_signed_request(:get, #path, OAuth::AccessToken.new(#consumer, #access_token.token, #access_token.secret))
get #path, nil, { 'HTTP_AUTHORIZATION' => #request.get_fields('authorization').first }
I would take a look as to how the Omniauth test helpers work, specifically these files: https://github.com/intridea/omniauth/tree/master/lib/omniauth/test. See their wiki page on integration testing for ideas of how this is set up. I realize that you're building a provider, not a client, but this may be a good starting point. Also, as some of the commenters have already said, I don't know if you can do this with a controller test; you may need a request or integration test to fully simulate the rack environment.
Turns out that the best way to test my controller was the simplest as well. Instead of trying to sign each test so the controller gets the right information (something that indeed does belong in a request spec not a controller spec), I figured out that I could just give the controller the information it needed manually.
To do this, I simply had to stub 2 methods:
fixtures :client_applications
before(:each) do
#client_application1 = client_applications(:client_application1)
Api::ClientApplicationsController::Authenticator.any_instance.stub(:allow?).and_return(true)
controller.stub(:client_application).and_return(#client_application1)
end
Stubbing the allow? method caused the rack auth to be fooled into thinking it was authenticated. allow? also set the client_application based on the credentials though, so I had to stub that as well. Now that the auth is out of the way, I can test my controller properly.

Ruby: HTTParty: can't format XML POST data correctly?

NOTE: "object" is a placeholder work, as I don't think I should be saying what the controller does specifically.
so, I have multiple ways of calling my apps API, the following works in the command line:
curl -H 'Content-Type: application/xml' -d '<object><name>Test API object</name><password>password</password><description>This is a test object</description></object>' "http://acme.example.dev/objects.xml?api_key=1234"
the above command generates the following request in the devlog:
Processing ObjectsController#create to xml (for 127.0.0.1 at 2011-07-07 09:17:51) [POST]
Parameters: {"format"=>"xml", "action"=>"create", "api_key"=>"1234", "controller"=>"objects",
"object"=>{"name"=>"Test API object", "description"=>"This is a test object", "password"=>"[FILTERED]"}}
Now, I'm trying to write tests for the actions using the API, to make sure the API works, as well as the controllers.
Here is my current (broken) httparty command:
response = post("create", :api_key => SharedTest.user_api_key, :xml => data, :format => "xml")
this command generates the following request in the testlog:
Processing ObjectsController#create to xml (for 0.0.0.0 at 2011-07-07 09:37:35) [POST]
Parameters: {
"xml"=>"<object><name><![CDATA[first post]]></name>
<description><![CDATA[Things are not as they used to be]]></description>
<password><![CDATA[WHEE]]></password>
</object>",
"format"=>"xml",
"api_key"=>"the_hatter_wants_to_have_tea1",
"action"=>"create",
"controller"=>"objects
So, as you can see, the command line command actually generates the object hash from the xml, whereas the httparty command ends up staying in xml, which causes problems for the create method, as it needs a hash.
Any ideas / proper documentation?
Current documentation says that post takes an url, and "options" and then never says what options are available
**EDIT:
as per #Casper's suggestion, my method now looks like this:
def post_through_api_to_url(url, data, api_key = SharedTest.user_api_key)
response = post("create", {
:query => {
:api_key => api_key
},
:headers => {
"Content-Type" => "application/xml"
},
:body => data
})
ap #request.env["REQUEST_URI"]
assert_response :success
return response
end
unfortunately, the assert_response fails, because the authentication via the api key fails.
looking at the very of of the request_uri, the api_key isn't being set properly... it shows:
api_key%5D=the_hatter_wants_to_have_tea1"
but it should just be equals, without the %5D (right square bracket)
I think this is how you're supposed to use it:
options = {
:query => {
:api_key => 1234
},
:headers => {
"Content-Type" => "application/xml"
},
:body => "<xmlcode>goes here</xmlcode>"
}
post("/create", options)
Forgive me for being basic about it but if you only want to send one variable as a parameter, why don't you do as Casper suggests, but just do:
post("/create?api_key=1234", options)
Or rather than testing HTTParty's peculiarities in accessing your API, perhaps write your tests using Rack::Test? Very rough example...
require "rack/test"
require "nokogiri"
class ObjectsTest < Test::Unit::TestCase
include Rack::Test::Methods
def app
MyApp.new
end
def create_an_object(o)
authorize "x", "1234" # or however you want to authenticate using query params
header 'Accept', 'text/xml'
header 'Content-Type', 'text/xml'
body o.to_xml
post "/create"
xml = Nokogiri::XML(last_response.body)
assert something_logic_about(xml)
end
end

Resources