Stub Httparty call: Wrong number of arguments (given 2, expected 1) - ruby-on-rails

I created a simple ruby file (not Rails) and I am trying to test (using Rspec) a method where I am calling an API. In the test I am trying to mock the call via WebMock but it keeps giving me this error:
Requests::FilesManager#display fetches the files from the API
Failure/Error: Requests::FilesManager.new.display
ArgumentError:
wrong number of arguments (given 2, expected 1)
The files are:
#run.rb
module Requests
require "httparty"
require 'json'
class FilesManager
include HTTParty
def initialize
end
def display
response = HTTParty.get('https://api.publicapis.org/entries', format: :json)
parsed_response = JSON.parse(response.body)
puts "The secret message was: #{parsed_response["message"]}"
end
end
end
and the spec file:
require 'spec_helper'
require_relative '../run'
RSpec.describe Requests::FilesManager do
describe "#display" do
it 'fetches the files from the API' do
stub_request(:get, "https://api.publicapis.org/entries").
to_return(status: 200, body: "", headers: {})
Requests::FilesManager.new.display
end
end
end
EDIT:
So the error seems to come from the line:
JSON.parse(response.body)
If I comment it out it disappears. The problem then is that the output of the call is not a json (even with the format: :json when calling the HTTParty). I tried other solutions but nothing seems to work in making the response json. It is just a string.

Change
response = HTTParty.get('https://api.publicapis.org/entries', format: :json)
to
response = HTTParty.get('https://api.publicapis.org/entries').
I think you don't need the format: :json more so when you explicitly format the response to JSON anyway.

You need to return a json object in the body parameter of the stubbed response:
E.g: For an empty response:
stub_request(:get, "https://api.publicapis.org/entries").
to_return(status: 200, body: "".to_json, headers: {})
OR For a valid response: (Note: You may have to require json to convert a hash to json)
require 'json'
...
stub_request(:get, "https://api.publicapis.org/entries").
to_return(status: 200, body: { entries: { '0': { message: "Hello World" } } }.to_json, headers: {})

Solved!
It seems there was an error because the json gem version that HTTParty uses is too old.
Moved on to RestClient gem for the RESTful API calls. It had another conflict in the mime gem versioning.
Finally moved to Faraday and that solved my problems:
JSON.parse(response.body, :quirks_mode => true)

tl;dr Had the same issue and ended up having to upgrade webmock.
Long form:
Webmock inserts middleware into your calls, so when HTTParty makes the calls they end up going through the Webmock interfaces first.
You can verify this by trying the call standalone (withouth all the rspec config):
bundle console
irb> require "httparty"
=> true
irb> httparty.get("https://google.com")
If that standalone call succeeds, the issue is somewhere within Webmock itself.
For me, somewhere along the line of calls through Webmock was an outdated interface that was incompatible and throwing the Wrong Number of Arguments error. And this was also crashing my debugger (RubyMine).
Upgrading Webmock solved this issue (because they had fixed it in newer versions).

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.

How to format HTTParty POST request?

I've been playing around with API calls for a project I'm working on and I'm getting a problem when I try to pass some JSON to a POST request. The call works in Postman, but I just can't figure out how to format it in Ruby. Here's my code:
require 'httparty'
require 'json'
require 'pp'
#use the HTTParty gem
include HTTParty
#base_uri 'https://app.api.com'
#set some basic things to make the call,
#apiUrl = "https://app.api.com/"
#apiUrlEnd = 'apikey=dontStealMePls'
#apiAll = "#{#apiUrl}#{#apiUrlEnd}"
#apiTest = "https://example.com"
def cc_query
HTTParty.post(#apiAll.to_s, :body => {
"header": {"ver": 1,"src_sys_type": 2,"src_sys_name": "Test","api_version": "V999"},
"command1": {"cmd": "cc_query","ref": "test123","uid": "abc01", "dsn": "abcdb612","acct_id": 7777}
})
end
def api_test
HTTParty.post(#apiTest.to_s)
end
#pp api_test()
pp cc_query()
This code gives me this error:
{"fault"=>
{"faultstring"=>"Failed to execute the ExtractVariables: Extract-Variables",
"detail"=>{"errorcode"=>"steps.extractvariables.ExecutionFailed"}}}
I know that error because I would get it if I tried to make a call without any JSON in the body of the call (through Postman). So from that I assume that my code above is not passing any JSON when making an API call. Am I formatting my JSON incorrectly? Am I even formatting the .post call correctly? Any help is appreciated! :)
the api_test() method just makes a POSt call to example.com and it works (saving my sanity).
Just use HTTParty as a mixin instead in a class instead:
require 'httparty'
class MyApiClient
include HTTParty
base_uri 'https://app.api.com'
format :json
attr_accessor :api_key
def initalize(api_key:, **options)
#api_key = api_key
#options = options
end
def cc_query
self.class.post('/',
body: {
header: {
ver: 1,
src_sys_type: 2,
src_sys_name: 'Test',
api_version: 'V999'
},
command1: {
cmd: 'cc_query',
ref: 'test123',
uid: 'abc01',
dsn: 'abcdb612',
acct_id: 7777
}
},
query: {
api_key: api_key
}
)
end
end
Example usage:
MyApiClient.new(api_key: 'xxxxxxxx').cc_query
When you use format :json HTTParty will automatically set the content type and handle JSON encoding and decoding. I'm guessing thats where you failed.

How can I handle generic errors with a JSON response in a hybrid (JSON API + HTML) Rails 5 app?

I spent one day trying several approaches, but still haven't quite got there so decided to ask now...
I have a Rails 5 app which is mainly a JSON API (using the actual JSON API specs), but also a "normal" Rails app with transactional emails and account related pages (reset password, etc).
What I'd like to achieve is that Rails always returns a JSON response with some meaningful error response to all API calls, rather than the default HTML error page or a header only 400 error.
The main cases I'm trying to handle are JSON parsing issues and Ruby exceptions (500 errors).
I tried:
using rescue_from on the ActionController level – seems the framework handles these exceptions before they would reach the controller
Handling them on the Rack level with a middleware – this worked in test but not in dev despite setting consider_all_requests_local to false in both
Registering a new Mime-type and a parser as JSON API Resources gem does it – looked promising, but the parser code is never hit
I'm really at my wit's end, something which sounded so simple ended up being deceptively complicated with me trying to hunt down where are these exceptions get handled in the framework without much success...
Well I managed to work it out in the end, so thought I should share what worked.
What I missed before is that I had to fiddle a bit with MIME types so that Rails would understand and properly use JSON API:
config/initializers/mime_types.rb
JSON_API_MIME_TYPES = %w[
application/vnd.api+json
text/x-json
application/json
].freeze
Mime::Type.unregister :json
Mime::Type.register 'application/json', :json, JSON_API_MIME_TYPES
Mime::Type.register_alias 'application/json', :json, %i[json_api jsonapi]
After this I could finally handle 500 errors in the base controller:
rescue_from StandardError,
with: :render_standard_error
def render_standard_error
render json: {
status: 500,
error: 'Unhandled error',
message: 'An unexpected error occurred.'
}, status: :internal_server_error
end
Then for handling JSON parse errors, this was the solution:
app/middleware/catch_json_parse_errors
class CatchJsonParseErrors
def initialize(app)
#app = app
end
def call(env)
#app.call(env)
rescue ActionDispatch::Http::Parameters::ParseError => error
if JSON_API_MIME_TYPES.include?(env['CONTENT_TYPE']) ||
JSON_API_MIME_TYPES.include?(env['HTTP_ACCEPT'])
return [
400, { 'Content-Type' => 'application/json' },
[{
status: 400,
error: 'JSON parse error',
message: "There was a problem in the JSON you submitted: #{error}"
}.to_json]
]
else
raise error
end
end
end
config/application.rb
require './app/middleware/catch_json_parse_errors'
...
config.middleware.use CatchJsonParseErrors

How to stub HTTP request on Mechanize in Rails?

I have some codebase like this, and I wanna use rspec test favicon_href, but as you like, the favicon_href will call the page function, I know I can mock page function, but for this stage I wanna mock the HTTP request from the given url, so I use WebMock gem's syntax to stub HTTP request, but it seems WebMock is not compatibility with Mechanize, it always show the error in the below despite I relleay have done the stub, anyone know how can solve it or any gem can stub HTTP request on Mechanize?
Code
def favicon_href
#favicon_href ||=
begin
page.at(FAVICON_DOM).attributes['href'].value # finding <link> elements
rescue Exception
'/favicon.ico' # there are some situation the favicon's not <link>'
end
end
def page
#page ||= mechanize.get(url)
end
def mechanize
#mechanize ||= Mechanize.new
end
Error
Failure/Error: #page ||= mechanize.get(valid_url(url))
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: GET https://tsaohucn.wordpress.com/ with headers {'Accept'=>'*/*', 'Accept-Charset'=>'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept-Encoding'=>'gzip,deflate,identity', 'Accept-Language'=>'en-us,en;q=0.5', 'Connection'=>'keep-alive', 'Host'=>'tsaohucn.wordpress.com', 'Keep-Alive'=>'300', 'User-Agent'=>'Mechanize/2.7.5 Ruby/2.3.1p112 (http://github.com/sparklemotion/mechanize/)'}
You can stub this request with the following snippet:
stub_request(:get, "https://tsaohucn.wordpress.com/").
with(headers: {'Accept'=>'*/*', 'Accept-Charset'=>'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept-Encoding'=>'gzip,deflate,identity', 'Accept-Language'=>'en-us,en;q=0.5', 'Connection'=>'keep-alive', 'Host'=>'tsaohucn.wordpress.com', 'Keep-Alive'=>'300', 'User-Agent'=>'Mechanize/2.7.5 Ruby/2.3.1p112 (http://github.com/sparklemotion/mechanize/)'}).
to_return(status: 200, body: "", headers: {})
registered request stubs:
stub_request(:get, "https://tsaohucn.wordpress.com/").
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'})
stub_request(:any, "http://api.stripe.com/")
stub_request(:any, "/api.stripe.com/")
============================================================
There exists an incompatibility between WebMock and net-http-persistent.
See
https://github.com/bblimke/webmock#connecting-on-nethttpstart
Add
WebMock.allow_net_connect!(:net_http_connect_on_start => true)
to your test set up.

Webmock caching responses? Or: How to respond to repeated requests with randomized content

I've tried using a lambda in my custom response:
stub_request(
:post,
'http://blah.blah/token'
).to_return(
status: 200,
body: lambda { |a| '{"token":"' + SecureRandom.hex(20) + '","expires_in":"259200"}' }
)
Maybe this isn't the correct way to handle dynamic responses, but anyway, webmock seems to execute the lambda exactly once. The request is identical each time, so either:
My asumption that using a lambda would allow me to generate dynamic content on a per-response basis was wrong.
Because the repeated requests are identical, webmock just uses the last response it generated.
Since this question was written, I strongly suspect that something in Webmock has changed, because the following test passes:
require 'webmock/rspec'
require 'securerandom'
require 'uri'
describe "something" do
it "happens" do
s = stub_request(:get, 'example.com/blah').
to_return(status: 200, body: lambda { |x| SecureRandom.hex(20) })
expect(Net::HTTP.get(URI('http://example.com/blah')))
.to_not eq(Net::HTTP.get(URI('http://example.com/blah')))
expect(s).to have_been_requested.at_least_once
end
end
Tested with Ruby 2.1.5p273, RSpec 3.3.1, and WebMock 1.21.0.

Resources