How to share RSpec let variables between shared contexts? - ruby

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

Related

Ruby Hash Alignment

I'm trying to correct a linter error in my requests test file. I have a context block as follows:
context 'when the request is valid' do
before(:each) do
post '/api/v1/budgets',headers: authenticated_header(#user), params: {
budget: valid_attributes
}
end
end
However, I keep getting the following error:
C: Layout/ArgumentAlignment: Align the arguments of a method call if they span more than one line.
What is the proper way to align this block?
When looking at the RuboCop Layout/ArgumentAlignment documentation my guess would be:
context 'when the request is valid' do
before(:each) do
post '/api/v1/budgets',
headers: authenticated_header(#user),
params: { budget: valid_attributes }
end
end
When you need multi-line params it should probably look like this:
context 'when the request is valid' do
before(:each) do
post '/api/v1/budgets',
headers: authenticated_header(#user),
params: {
budget: valid_attributes
}
end
end
If you have more than one argument on the first line:
context 'when the request is valid' do
before(:each) do
post '/api/v1/budgets', headers: authenticated_header(#user), params: {
budget: valid_attributes
}
end
end
When you have a long method name you could also consider moving the first argument to the next line to reduce intention for all other arguments:
context 'when the request is valid' do
before(:each) do
a_somewhat_long_method_name
'/api/v1/budgets',
headers: authenticated_header(#user),
params: { budget: valid_attributes }
end
end
The above amuses you use the default configuration (:with_first_argument).

rspec webmock to_raise error not raising error

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.

ruby rackup: is it possible to programatically add a mapping from within another mapping?

I have a .ru file and can set up mappings without issue ( the 'register' mapping below ).
However I want services to be able to register themselves by hitting a url so I want to be able to add new mappings on the fly from within others mappings.
The below code does not work though. What am I doing wrong and is this possible?
Thanks!
map '/register' do
run Proc.new { |env|
# inside of register i want to add another mapping.
# obviously 'bar' would be a value read out of env
map '/bar' do
run Proc.new{ |env| ['200', { 'Content-Type' => 'text/html' },'bar' }
end
[ '200', {'Content-Type' => 'text/html'}, "registered"]
}
end
I don't think there's a way to add routes after-the-fact using map. One alternative is to use Rack::URLMap to define your app. You'll need to maintain your own list of registered routes (as a hash) and call Rack::URLMap#remap every time you add a new route to the hash:
url_map = Rack::URLMap.new
routes = {
"/register" => lambda do |env|
routes["/bar"] = lambda do |env|
[ "200", {"Content-Type" => "text/plain"}, ["bar"] ]
end
url_map.remap(routes)
[ "200", {"Content-Type" => "text/plain"}, ["registered"] ]
end
}
url_map.remap(routes)
run url_map
Note that you could do with with just the hash, but URLMap provides some nice conveniences, including 404 handling. It's actually a really nice little class and worth reading if you have five minutes to spare.
If you were so inclined, you could turn this into a tidy little class:
class Application
def initialize
#routes = {}
#url_map = Rack::URLMap.new
register_route "/register" do |env|
# When "/register" is requested, register the new route "/bar"
register_route "/bar" do |env|
[ 200, {"Content-Type" => "text/plain"}, ["bar"] ]
end
[ 200, {"Content-Type" => "text/plain"}, ["registered"] ]
end
end
def call(env)
#url_map.call(env)
end
private
def register_route(path, &block)
#routes[path] = block
#url_map.remap(#routes)
end
end
run Application.new
According to https://rack.github.io/, "To use Rack, provide an "app": an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:
The HTTP response code
A Hash of headers
The response body, which must respond to each"
Your third element will not respond to each. Maybe wrap it in an array?

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