rspec webmock to_raise error not raising error - ruby

I have the following code snippet and test
class Service
def self.status(base_uri = 'localhost', basic_auth = {})
base_uri = "#{ base_uri }/status"
response = HTTParty.get(base_uri, basic_auth: basic_auth)
rescue StandardError => e
binding.pry
raise "Error occured code: #{ response.code } msg: #{ e.message }"
end
end
it 'should return error msg if url is not valid' do
bad_url = "http://bar_url.com"
expected = 'Error occured'
stub_request(:get, bad_url).
with(headers: {'Authorization'=>'Basic YWRtaW46YWRtaW4='}).
to_raise(StandardError)
expect { Service.status(bad_url, { username: #username, password: #password }) }.to raise_error(/#{ expected }/)
end
This code is based on the documentation.
As a result rspec always raises an exception:
expected /Error occured/, got #<WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: GET h...rization'=>'Basic YWRtaW46YWRtaW4='})
Can anyone help me figure out what I should change to get a StandardError as a result?

Since the service calls "#{ base_uri }/status" which is http://bar_url.com/status but the stub_request is declared with http://bar_url.com only, the Net::HTTP.Get looks for match with this instead of http://bar_url/status.
This is the reason why there is no error raising in the test.

Related

Set http status code 201 to an API response in action controller rails

I'm new to rails and I'm building a Rails app that will function as an API. Currently I don't have any models or database just an Api::ProductController controller:
class Api::ProductController < ApplicationController
def create
Rails.logger.info "product was created and the parameters are #{product_params[:name]}, #{product_params[:age]}"
end
private
def product_params
params.permit(:name, :age)
end
end
. As I continue and wrote the request Rspec:
RSpec.describe Api::productsController, type: :request do
it "creates a product" do
post "/api/products", params: { name: "name", age: "22"}
expect(response).to have_http_status(:created)
expect(response.body).to include("product was successfully created.")
end
end
But when I run the request rspec test on the command line I get the following error:
Failure/Error: expect(response).to have_http_status(:created)
expected the response to have status code :created (201) but it was :no_content (204)
My question is how can I set the status code to :created (201)? is the Head method a good approach? any solution or guiding would be appreciated!
It is common to return json presentation of created item.
Now there is no returning data from your action.
Try to use render json: product_params.to_json, status: :created
def create
Rails.logger.info "product was created and the parameters are #{product_params[:name]}, #{product_params[:age]}"
render status: :created
end

API POST request returns 404 route not found

Rails 5.2.2.1
ruby 2.6.3p62
I'm writing an API endpoint that should accept a post request. I created the route:
namespace :api do
scope module: :v1, constraints: Example::ApiVersionConstraint.new(1) do
resources 'books', only: [:create]
end
end
bundle exec rails routes | grep books returns:
api_books POST /api/books(.:format) api/v1/books#create
app/controllers/api/v1/books_controller.rb:
class Api::V1::BooksController < Api::BaseController
attr_reader :book
def create
book = Book.build(title: 'test')
if book.save
render json: book
else
render json: { error: 'error' }, status: 400
end
end
end
server is running on port 3000 and when submitting a POST request to http://localhost:3000/api/books.json using Postman I get this response:
{
"errors": [
{
"code": "routing.not_found",
"status": 404,
"title": "Not found",
"message": "The path '/api/books' does not exist."
}
],
"request": ""
}
lib/example/api_version_constraint.rb:
module Example
class ApiVersionConstraint
def initialize(version)
#version = version
end
def matches?(request)
request.headers.fetch(:accept).include?("version=#{#version}")
rescue KeyError
false
end
end
end
why isn't the request finding the route?
Something is likely failing in ApiVersionConstraint. To troubleshoot you can do something like:
def matches?(request)
byebug
request.headers.fetch(:accept).include?("version=#{#version}")
rescue KeyError
false
end
Guessing it's a problem with how you're targeting the header, so something like this might work:
request&.headers&.fetch("Accept")&.include?("version=#{#version}")
Because you have a rescue clause, you'll never get the full error; only false, so you might try removing that and seeing if you get a more descriptive error.

How to share RSpec let variables between shared contexts?

I'm trying to DRY my RSpec request specs by using shared contexts. I'd like to share let variables between shared contexts so that they inherit and extend from one another.
Rspec.shared_context 'JSON request' do
let(:headers) do
{
'Accept' => 'application/json'
}
end
end
Rspec.shared_context 'Authenticated request' do
let(:headers) do
super().merge('Authorization' => "Bearer #{token}")
end
end
Rspec.describe 'user management' do
let(:token) { create(:oauth_token) }
include_context 'JSON request'
include_context 'Authenticated request'
it 'responds with a 200 ok' do
get '/user', headers: headers
expect(response).to have_http_status(:ok)
end
end
Declaring token works as expected, but using super() to override headers returns a NoMethodError suggesting super() is nil.
I'm not aware of a way to refer to the currently defined value of a let variable in a let block. (When I try it I get "stack level too deep".) I'd do what you're trying to do this way:
Rspec.shared_context 'JSON request' do
let(:common_headers) do
{
'Accept' => 'application/json'
}
end
let(:headers) { common_headers }
end
Rspec.shared_context 'Authenticated request' do
let(:headers) do
common_headers.merge('Authorization' => "Bearer #{token}")
end
end
You can use super() to get the value of a predefined let:
RSpec.shared_context 'common' do
let(:foo) { 'foo' }
end
RSpec.describe 'test' do
include_context 'common'
context 'reversed' do
let(:foo) { super().reverse }
it 'works' do
expect(foo).to eq('oof')
end
end
end

Ruby stubbing with faraday, can't get it to work

Sorry for the title, I'm too frustrated to come up with anything better right now.
I have a class, Judge, which has a method #stats. This stats method is supposed to send a GET request to an api and get some data as response. I'm trying to test this and stub the stats method so that I don't perform an actual request. This is what my test looks like:
describe Judge do
describe '.stats' do
context 'when success' do
subject { Judge.stats }
it 'returns stats' do
allow(Faraday).to receive(:get).and_return('some data')
expect(subject.status).to eq 200
expect(subject).to be_success
end
end
end
end
This is the class I'm testing:
class Judge
def self.stats
Faraday.get "some-domain-dot-com/stats"
end
end
This currently gives me the error: Faraday does not implement: get
So How do you stub this with faraday? I have seen methods like:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('http://stats-api.com') { [200, {}, 'Lorem ipsum'] }
end
But I can't seem to apply it the right way. What am I missing here?
Note that Faraday.new returns an instance of Faraday::Connection, not Faraday. So you can try using
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return("some data")
Note that I don't know if returning "some data" as shown in your question is correct, because Faraday::Connection.get should return a response object, which would include the body and status code instead of a string. You might try something like this:
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double("response", status: 200, body: "some data")
)
Here's a rails console that shows the class you get back from Faraday.new
$ rails c
Loading development environment (Rails 4.1.5)
2.1.2 :001 > fara = Faraday.new
=> #<Faraday::Connection:0x0000010abcdd28 #parallel_manager=nil, #headers={"User-Agent"=>"Faraday v0.9.1"}, #params={}, #options=#<Faraday::RequestOptions (empty)>, #ssl=#<Faraday::SSLOptions (empty)>, #default_parallel_manager=nil, #builder=#<Faraday::RackBuilder:0x0000010abcd990 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x0000010abcd378 URL:http:/>, #proxy=nil>
2.1.2 :002 > fara.class
=> Faraday::Connection
Coming to this late, but incase anyone else is too, this is what worked for me - a combination of the approaches above:
let(:json_data) { File.read Rails.root.join("..", "fixtures", "ror", "501100000267.json") }
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double(Faraday::Response, status: 200, body: json_data, success?: true)
)
end
Faraday the class has no get method, only the instance does. Since you are using this in a class method what you can do is something like this:
class Judge
def self.stats
connection.get "some-domain-dot-com/stats"
end
def self.connection=(val)
#connection = val
end
def self.connection
#connection ||= Faraday.new(some stuff to build up connection)
end
end
Then in your test you can just set up a double:
let(:connection) { double :connection, get: nil }
before do
allow(connection).to receive(:get).with("some-domain-dot-com/stats").and_return('some data')
Judge.connection = connection
end
I ran into the same problem with Faraday::Adapter::Test::Stubs erroring with Faraday does not implement: get. It seems you need to set stubs to a Faraday adapter, like so:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get("some-domain-dot-com/stats") { |env| [200, {}, 'egg'] }
end
test = Faraday.new do |builder|
builder.adapter :test, stubs
end
allow(Faraday).to receive(:new).and_return(test)
expect(Judge.stats.body).to eq "egg"
expect(Judge.stats.status).to eq 200
A better way to do this, rather than using allow_any_instance_of, is to set the default connection for Faraday, so that Faraday.get will use the connection you setup in your tests.
For example:
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
before do
stubs.get('/maps/api/place/details/json') do |_env|
[
200,
{ 'Content-Type': 'application/json' },
{ 'result' => { 'photos' => [] } }.to_json
]
end
Faraday.default_connection = conn
end
after do
Faraday.default_connection = nil
end

Standardizing api responses in a modular Sinatra application

I'm developing an api as a modular Sinatra web application and would like to standardize the responses that are returned without having to do so explicitly. I thought this could be achieved by using middleware but it fails in most scenarios. The below sample application is what I have so far.
config.ru
require 'sinatra/base'
require 'active_support'
require 'rack'
class Person
attr_reader :name, :surname
def initialize(name, surname)
#name, #surname = name, surname
end
end
class MyApp < Sinatra::Base
enable :dump_errors, :raise_errors
disable :show_exceptions
get('/string') do
"Hello World"
end
get('/hash') do
{"person" => { "name" => "john", "surname" => "smith" }}
end
get('/array') do
[1,2,3,4,5,6,7, "232323", '3245235']
end
get('/object') do
Person.new('simon', 'hernandez')
end
get('/error') do
raise 'Failure of some sort'
end
end
class ResponseMiddleware
def initialize(app)
#app = app
end
def call(env)
begin
status, headers, body = #app.call(env)
response = {'status' => 'success', 'data' => body}
format(status, headers, response)
rescue ::Exception => e
response = {'status' => 'error', 'message' => e.message}
format(500, {'Content-Type' => 'application/json'}, response)
end
end
def format(status, headers, response)
result = ActiveSupport::JSON.encode(response)
headers["Content-Length"] = result.length.to_s
[status, headers, result]
end
end
use ResponseMiddleware
run MyApp
Examples (in JSON):
/string
Expected: {"status":"success","data":"Hello World"}
Actual: {"status":"success","data":["Hello World"]}
/hash (works)
Expected: {"status":"success","data":{"person":{"name":"john","surname":"smith"}}}
Actual: {"status":"success","data":{"person":{"name":"john","surname":"smith"}}}
/array
Expected: {"status":"success","data": [1,2,3,4,5,6,7,"232323","3245235"]}
Actual: {"status":"error","message":"wrong number of arguments (7 for 1)"}
/object
Expected: {"status":"success","data":{"name":"simon","surname":"hernandez"}}
Actual: {"status":"success","data":[]}
/error (works)
Expected: {"status":"error","message":"Failure of some sort"}
Actual: {"status":"error","message":"Failure of some sort"}
If you execute the code, you will see that /hash and /error give back the required responses, but the rest do not. Ideally, I would not like to change anything in the MyApp class. It's currently being built on top of Sinatra 1.3.3, ActiveSupport 3.2.9 and Rack 1.4.1.
With some help from #sinatra on irc.freenode.org, I managed to get it down to what I want. I added the following to MyApp:
def route_eval
result = catch(:halt) { super }
throw :halt, {"result" => result}
end
I then changed the following line in ResponseMiddleware:
response = {'status' => 'success', 'data' => body}
to
response = {'status' => 'success', 'data' => body["result"]}
and all my test cases passed.

Resources