Rspec request specs and Rails 5 - ruby-on-rails

I'm starting a new project, my first with Rails 5.1.0. I have a pb with my first request spec.
describe 'Users', type: :request do
it 'are created from external data' do
json_string = File.read('path/to/test_data/user_data.json')
params = { user: JSON.parse(json_string) }
headers = { "CONTENT_TYPE" => "application/json" }
expect do
post '/api/v1/users', params.to_s, headers
end.to change {
User.count
}.by(1)
expect(response.status).to eq 200
end
end
this spec return the error ArgumentError: wrong number of arguments (given 3, expected 1). The official documentation don't say much.
If I take out the .to_s, and send a hash, like this:
post '/api/v1/users', params, headers
I got another error:
ArgumentError: unknown keyword: user
Any thought?

I think they changed the syntax recently. Now it should use keyword args. So, something like this:
post '/api/v1/users', params: params, headers: headers

Here's a little addendum to Sergio's answer. If you are upgrading from Rails 4 to Rails 5, have lots of tests, and aren't too keen on changing them all – at least not until you've finished upgrading – I've found a way to make them work with the old method signature.
In my spec_helper I added
module FixLegacyTestRequests
def get(path, par = {}, hdr = {})
process(:get, path, params: par, headers: hdr)
end
def post(path, par = {}, hdr = {})
process(:post, path, params: par, headers: hdr)
end
def put(path, par = {}, hdr = {})
process(:put, path, params: par, headers: hdr)
end
def delete(path, par = {}, hdr = {})
process(:delete, path, params: par, headers: hdr)
end
end
and then I added this configuration for each test:
RSpec.configure do |config|
config.before :each do |example|
extend(FixLegacyTestRequests) # to be removed at some point!
end
end
My tests went back to working, and I think it should be safe because it's only applied to the currently running test and shouldn't pollute any gem's code such as with a monkey patch.

Related

Mock GoogleAPI request

I am using the google-maps gem.
I am trying to mock/stub api requests unsuccessfully.
module GoogleMap
class Route
attr_accessor :start_location, :end_location
def initialize(start_location, end_location)
#start_location = start_location
#end_location = end_location
end
def driving_duration_in_seconds
route.duration.value
end
def driving_distance_in_meters
route.distance.value
end
def driving_distance_hash
return unless start_location && end_location
{ distance_in_meters: driving_distance_in_meters, duration_in_seconds: driving_duration_in_seconds }
end
private
def coordinates_as_strings(location)
"#{location.latitude},#{location.longitude}"
end
def route
#route ||= Google::Maps.route(coordinates_as_strings(start_location), coordinates_as_strings(end_location))
end
end
end
I need to stub:
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: GET https://maps.googleapis.com/maps/api/directions/json?destination=37.19687112,116.43791248&key=<apikey>&language=en&origin=2.15362819,-81.63712649 with headers {'Accept'=>'*/*', 'Date'=>'Sat, 12 Feb 2022 21:35:55 GMT', 'User-Agent'=>'HTTPClient/1.0 (2.8.3, ruby 2.7.2 (2020-10-01))'}
You can stub this request with the following snippet:
stub_request(:get, "https://maps.googleapis.com/maps/api/directions/json?destination=37.19687112,116.43791248&key=<apikey>&language=en&origin=2.15362819,-81.63712649").
with(
headers: {
'Accept'=>'*/*',
'Date'=>'Sat, 12 Feb 2022 21:35:55 GMT',
'User-Agent'=>'HTTPClient/1.0 (2.8.3, ruby 2.7.2 (2020-10-01))'
}).
to_return(status: 200, body: "", headers: {})
If I try a most basic stub I get an error:
stub_request(:any, /maps.googleapis.com/).
to_return(status: 200, body: '', headers: {})
Google::Maps::InvalidResponseException:
unknown error: 783: unexpected token at ''
# .../gems/ruby-2.7.2/gems/google-maps-3.0.7/lib/google_maps/api.rb:64:in `rescue in response'
# .../gems/ruby-2.7.2/gems/google-maps-3.0.7/lib/google_maps/api.rb:60:in `response'
# .../.rvm/gems/ruby-2.7.2/gems/google-maps-3.0.7/lib/google_maps/api.rb:27:in `query'
I think it is erroring out because I am not passing a key in. But I don't see why I should have to pass in a valid api key into a webmock.
I also would not have my route defined by anything. And in order to test that route can return route.distance.value etc, I would need to mock with something.
For other tests I was successful in mocking instances, but to test this lib that it actually works, I feel like mocking instance methods and not that an api was actually called is a waste of a test. Maybe this is just a waste of time, and I should assume it works because I am using a gem.
But I was expecting something like this:
RSpec.describe GoogleMap do
let(:start_location) { create(:location) }
let(:end_location) { create(:location) }
context 'GoogleMaps::Route.new(start_location, end_location)' do
let(:subject) { GoogleMap::Route.new(start_location, end_location) }
# I have not been successful in stubbing this with the correct regex
# stub_request(:get, "https://maps.googleapis.com/maps/api/directions/json?destination=<lat>,<long>key=<key>&language=en&origin=<lat>,<long>").
# with(
# headers: {
# 'Accept'=>'*/*',
# 'Date'=>'Thu, 10 Feb 2022 21:09:02 GMT',
# 'User-Agent'=>'HTTPClient/1.0 (2.8.3, ruby 2.7.2 (2020-10-01))'
# }).
# to_return(status: 200, body: "", headers: {})
# stub_request(:get, %r{https:\/\/maps\.googleapis\.com\/maps\/api\/directions\/json\?destination=.+,.+&key=.+&language=en&origin=.+,.+}).
# stub_request(:any, /maps.googleapis.com/).
# to_return(status: 200, body: '', headers: {})
xit 'gives driving distance in seconds'
xit 'gives driving duration in meters'
end
end
Your WebMock is working fine. Google::Maps::InvalidResponseException is raised after WebMock has replaced the network call. At the point that exception is raised, the Google Maps API client is trying to parse what the network call returned, which is ''.
It's expecting some valid JSON to be returned. If you have your mock return {} is should get past that line. It may well stumble on some other exception later though, as the gem expects a certain schema.
You can dig that out and add in a valid response if you wanted to continue down this path. However, I'd recommend not mocking the network request as that's an implementation detail of a third party piece of code which could change at any time - making your test fail. Instead, I would mock out Google::Maps.route to return what you need it to.

Stub JSON.parse in RSPEC

I'm doing some RSPEC testing here.
If i have this method:
JSON.parse("https://test.com/return_json/reviews.json")
Then I can stub it RSPEC like:
test_reviews = {"reviews" => [{"data1" => "1", "data2"=> "2"}]}
allow(JSON).to receive(:parse).and_return(test_reviews.to_json)
But for this kind (with other method inside (to_uri & read)).
JSON.parse("https://test.com/return_json/reviews.json".to_uri.read)
I tried to used receive_message_chain but no success.
Thanks in advance guys!
You code is not actually calling the url you. You need to make an http call and parse the body. I should probably look like this.
describe :ReviewsController
let(:uri) { URI('https://test.com/return_json/reviews.json') }
let(:reviews) { {"reviews" => [{"data1" => "1", "data2"=> "2"}]} }
before do
stub_request(:get, uri).
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
to_return(status: 200, body: JSON.dump(reviews), headers: {})
end
it 'does whatever you want' do
response = Net::HTTP.get(uri)
expect(JSON.parse(response.body)['data1']).to eq('1') # or whatever you want to test
end
end
It's better explained here.

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.

Rspec: add some header requests inside routing specs

I'm working on a Rails application having a REST API in JSON format and versioned (according to this excellent Ryan's cast: http://railscasts.com/episodes/350-rest-api-versioning).
For instance, there is a spec/requests spec:
require 'spec_helper'
describe "My Friends" do
describe "GET /my/friends.json" do
it "should get my_friends_path" do
get v1_my_friends_path, {}, {'HTTP_ACCEPT' => 'application/vnd.myapp+json; level=1'}
response.status.should be(401)
end
end
end
And it works well. But (keeping this example) how can we write the routing spec? For instance this spec isn't correct:
require 'spec_helper'
describe "friends routing" do
it "routes to #index" do
get("/my/friends.json", nil, {'HTTP_ACCEPT' => 'application/vnd.myapp+json; level=1'}).
should route_to({ action: "index",
controller: "api/v1/private/my/friends",
format: "json" })
end
end
I tried different ways (such as request.headers['Accept'] and #request.headers['Accept'], where request is undefined and #request is nil); I really don't see how to do.
I'm on Ruby 1.9.3, Rails 3.2.6 and rspec-rails 2.11.0. Thanks.
By combining the ideas from Cristophe's and Piotr's answers, I came up with a solution that worked for me. I'm using rspec and rails 3.0.
it 'should route like i want it to' do
Rack::MockRequest::DEFAULT_ENV["HTTP_ACCEPT"] = "*/*"
{get: "/foo/bar"}.
should route_to(
controller: 'foo',
action: 'bar',
)
Rack::MockRequest::DEFAULT_ENV.delete "HTTP_ACCEPT"
end
Currently you can't send addititional Headers in Routing specs, this is due to line 608 in actionpack-3.2.5/lib/action_dispatch/routing/route_set.rb where it says:
env = Rack::MockRequest.env_for(path, {:method => method})
path is your requested path "/my/friends.json" and method is :get
The resulting env contains something like the following:
{
"rack.version"=>[1, 1],
"rack.input"=>#<StringIO:0xb908f5c>,
"rack.errors"=>#<StringIO:0xb908fac>,
"rack.multithread"=>true,
"rack.multiprocess"=>true,
"rack.run_once"=>false,
"REQUEST_METHOD"=>"GET",
"SERVER_NAME"=>"your-url.com", # if path was http://your-url.com/
"SERVER_PORT"=>"80",
"QUERY_STRING"=>"",
"PATH_INFO"=>"/",
"rack.url_scheme"=>"http",
"HTTPS"=>"off",
"SCRIPT_NAME"=>"",
"CONTENT_LENGTH"=>"0"
}
If you are able to mock Rack::MockRequest::env_for it should be possible to inject other headers than the ones generated by env_for (see Hash above).
Other than that you are currently using the route_to matcher wrong, you should call it on a Hash where you specify the method and the path like this:
{ get: '/' }.should route_to(controller: 'main', action: 'index')
Let us know if you were able to Mock out that env_for and let it return your headers, would be nice to know.
Regards
Christoph
before do
ActionDispatch::TestRequest::DEFAULT_ENV["action_dispatch.request.accepts"] = "application/vnd.application-v1+json"
end
after do
ActionDispatch::TestRequest::DEFAULT_ENV.delete("action_dispatch.request.accepts")
end
You can using rspec's and_wrap_original to mock the Rack::MockRequest.env_for method:
expect(Rack::MockRequest).to receive(:env_for).and_wrap_original do |original_method, *args, &block|
original_method.call(*args, &block).tap { |hash| hash['HTTP_ACCEPT'] = 'application/vnd.myapp+json; level=1' }
end
For Rails 3 and 4 I had done the following in an RSpec around hook:
around do |example|
Rack::MockRequest::DEFAULT_ENV['HTTP_ACCEPT'] = 'application/vnd.vendor+json; version=1'
example.run
Rack::MockRequest::DEFAULT_ENV.delete 'HTTP_ACCEPT'
end
Since Rack >= 2.0.3 (used by Rails 5) the Rack::MockRequest::DEFAULT_ENV hash is frozen.
You can redefine the constant and use Kernel.silence_warnings to silence the Ruby warnings:
around do |example|
silence_warnings do
Rack::MockRequest::DEFAULT_ENV = Rack::MockRequest::DEFAULT_ENV.dup
end
Rack::MockRequest::DEFAULT_ENV['HTTP_ACCEPT'] = 'application/vnd.vendor+json; version=1'
example.run
Rack::MockRequest::DEFAULT_ENV.delete 'HTTP_ACCEPT'
end
It's a bit of hack but it works like a charm.

Integration testing Rails API with basic authentication

I'm trying to get a test signing in using basic authentication. I've tried a few approaches. See code below for a list of failed attempts and code. Is there anything obvious I'm doing wrong. Thanks
class ClientApiTest < ActionController::IntegrationTest
fixtures :all
test "adding an entry" do
# No access to #request
##request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("someone#somemail.com:qwerty123")
# Not sure why this didn't work
#session["warden.user.user.key"] = [User, 1]
# This didn't work either
# url = URI.parse("http://localhost.co.uk/diary/people/1/entries.xml")
# req = Net::HTTP::Post.new(url.path)
# req.basic_auth 'someone#somemail.com', 'qwerty123'
post "/diary/people/1/entries.xml", {:diary_entry => {
:drink_product_id => 4,
:drink_amount_id => 1,
:quantity => 3
},
}
puts response.body
assert_response 200
end
end
It looks like you might be running rails3 -- Rails3 switched over to Rack::test so the syntax is different. You pass in an environment hash to set your request variables like headers.
Try something like:
path = "/diary/people/1/entries.xml"
params = {:diary_entry => {
:drink_product_id => 4,
:drink_amount_id => 1,
:quantity => 3}
env=Hash.new
env["CONTENT_TYPE"] = "application/json"
env["ACCEPT"] = "application/json"
env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("someone#somemail.com:qwerty123")
get(end_point, params, env)
This could work too, but it might be a sinatra only thing:
get '/protected', {}, {'HTTP_AUTHORIZATION' => encode_credentials('go', 'away')}
Sinatra test credit
This works for me in Rails 3
#request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', 'password')
get :index

Resources