Configuring rack-test to start the server indirectly - ruby

Here is my rack application:
class MainAppLogic
def initialize
Rack::Server.start(:app =>Server, :server => "WEBrick", :Port => "8080")
end
end
class Server
def self.call(env)
return [200, {},["Hello, World"]]
end
end
When actually run, it behaves as it should and returns "Hello World" to all requests. I'm having trouble convincing rack-test to work with it. Here are my tests:
require "rspec"
require "rack/test"
require "app"
# Rspec config source: https://github.com/shiroyasha/sinatra_rspec
RSpec.configure do |config|
config.include Rack::Test::Methods
end
describe MainAppLogic do
# App method source: https://github.com/shiroyasha/sinatra_rspec
def app
MainAppLogic.new
end
it "starts a server when initialized" do
get "/", {}, "SERVER_PORT" => "8080"
last_response.body.should be != nil
end
end
When I test this, it fails complaining that MainAppLogic is not a rack server, specifically, that it doesn't respond to MainAppLogic.call. How can I let it know to ignore that MainAppLogic isn't a rack server and just place a request to localhost:8080, because there server has started?

First thing: why the custom class to run the app? You can use the rackup tool, which is the de-facto standard for running Rack apps. Some more details on it here.
Your app code then becomes:
class App
def call(env)
return [200, {}, ['Hello, World!']]
end
end
and with the config.ru
require_relative 'app'
run App.new
you can start the app by running rackup in your project's directory.
As for the error, the message is pretty clear. rack-test expects, that the return value of app method would be an instance of a rack app (an object that responds to call method). Take a look what happens in rack-test internals (it's pretty easy to follow, as a tip—focus on these in given order: lib/rack/test/methods.rb#L30 lib/rack/mock_session.rb#L7 lib/rack/test.rb#L244 lib/rack/mock_session.rb#L30. Notice how the Rack::MockSession is instantiated, how it is used when processing requests (e.g. when you call get method in your tests) and finally how the call method on your app is executed.
I hope that now it's clear why the test should look more like this (yes, you don't need to have a server running when executing your tests):
describe App do
def app
App.new
end
it "does a triple backflip" do
get "/"
expect(last_response.body).to eq("Hello, World")
end
end
P.S.
Sorry for the form of links to rack-test, can't add more than 2 with my current points :P

Your app should be the class name, for example instead of:
def app
MainAppLogic.new
end
You have to use
def app
MainAppLogic
end
You shouldn't need to indicate the port for doing the get, because the rack app runs in the context of the tests; so this should be right way:
it "starts a server when initialized" do
get "/"
last_response.body.should be != nil
end
Also, as a recommendation prefer to use the new expect format instead of the should, see http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
And your MainAppLogic, should be something like:
class MainAppLogic < Sinatra::Base
get '/' do
'Hello world'
end
end

Related

Testing that a method was called on a Sinatra request

Hoping someone can help me with some basic Sinatra testing help...
I have a toy sinatra app that implements this basic endpoint, which calls a method of the same name:
get '/hello' do
hello
end
def hello
"hello"
end
I then have these 3 specs. The first one and 3rd pass, but the second one fails:
context 'get /hello' do
let(:app) { App.new! }
it 'responds to hello' do
expect(app).to respond_to :hello
end
it 'calls hello' do
expect(app).to receive(:hello)
get '/hello'
end
it 'returns hello' do
let(:response) { get '/hello' }
expect(response.status).to eq 200
expect(response.body).to eq "hello"
end
end
Error of it not being called:
expected: 1 time with any arguments
received: 0 times with any arguments
The first spec proves the method exists, and the 3rd spec proves it is actually calling the method in terms of rspec. I'm not sure what I'm missing to get the 2nd spec working
Since you're using Rack::Test you need to provide a method named app that returns the application to be mocked instead of using a let with App.new!. Something like this:
RSpec.describe 'app' do
include Rack::Test::Methods
def app
Sinatra::Application
end
context 'get /hello' do
before { get '/hello' }
it { expect(last_response.status).to eq(200) }
it { expect(last_response.body).to eq('hello') }
end
More information about the app method can be found in the Sinatra testing docs.
FWIW I wouldn't write Rack level tests for the first two tests in your example. It unnecessarily couples the implementation to the tests. I'd test the hello method directly instead.

Raising 500 errors deliberately in Sinatra in order to test how they are handled

I want to write an RSpec test which verifies that, should a 500 error occur in my Sinatra-powered API, the error will be caught by a Sinatra error definition and returned to the client in a JSON format. That is, rather than returning some HTML error page, it returns JSON like this to conform with the rest of the API:
{
success: "false",
response: "Internal server error"
}
However, I'm unsure how to actually trigger a 500 error in my Sinatra app in order to test this behaviour with RSpec. I can't find a way to mock Sinatra routes, so currently my best idea is this route which deliberately causes a 500. This feels like a pretty dreadful solution:
get '/api/v1/testing/internal-server-error' do
1 / 0
end
Is there a way to mock Sinatra routes so that I can have, say, /'s route handler block raise an exception, therefore triggering a 500? If not, is there some other way to deliberately cause a 500 error in my app?
When facing a situation like this, what I usually do is separate concerns, and move logic outside of the Sinatra get ... block. Then, it is easy to stub it and make it raise an error.
For example, given this server code:
# server.rb
require 'sinatra'
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
You can then use this code to have the model, or any other class/function you are using to actually generate the response, raise an error, using this spec:
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
For completeness, here is a self contained file that can be tested to demonstrate this approach by running rspec thisfile.rb:
# thisfile.rb
require 'rack/test'
require 'rspec'
require 'sinatra'
# server
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
# spec_helper
ENV['APP_ENV'] = 'test'
module RSpecMixin
include Rack::Test::Methods
def app() Sinatra::Application end
end
RSpec.configure do |c|
c.include RSpecMixin
end
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
Use the halt method:
require 'sinatra'
get '/' do
halt 500, {
success: 'false',
response: 'Internal server error'
}.to_json
end

How do I get the Sinatra app instance that's being tested by rack-test?

I want to get hold of the app instance being tested by rack-test so that I can mock some of its methods. I thought I could simply save the app instance in the app method, but for some strange reason that doesn't work. It seems like rack-test simply uses the instance to get the class, then creates its own instance.
I've made a test to demonstrate my issue (it requires the gems "sinatra", "rack-test" and "rr" to run):
require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"
describe "instantiated app" do
include Rack::Test::Methods
def app
cls = Class.new(Sinatra::Base) do
get "/foo" do
$instance_id = self.object_id
generate_response
end
def generate_response
[200, {"Content-Type" => "text/plain"}, "I am a response"]
end
end
# Instantiate the actual class, and not a wrapped class made by Sinatra
#app = cls.new!
return #app
end
it "should have the same object id inside response handlers" do
get "/foo"
assert_equal $instance_id, #app.object_id,
"Expected object IDs to be the same"
end
it "should trigger mocked instance methods" do
mock(#app).generate_response {
[200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
}
get "/foo"
assert_equal "I am MOCKED", last_response.body
end
end
How come rack-test isn't using the instance I provided? How do I get hold of the instance that rack-test is using, so that I can mock the generate_response method?
Update
I have made no progress. It turns out rack-test creates the tested instance on the fly when the first request is made (i.e. get("/foo")), so it's not possible to mock the app instance before then.
I have used rr's stub.proxy(...) to intercept .new, .new! and .allocate; and added a puts statement with the instance's class name and object_id. I have also added such statements in the tested class' constructor, as well as a request handler.
Here's the output:
From constructor: <TestSubject 47378836917780>
Proxy intercepted new! instance: <TestSubject 47378836917780>
Proxy intercepted new instance: <Sinatra::Wrapper 47378838065200>
From request handler: <TestSubject 47378838063980>
Notice the object ids. The tested instance (printed from the request handler) never went through .new and was never initialized.
So, confusingly, the instance being tested is never created, but somehow exists none the less. My guess was that allocate was being used, but the proxy intercept shows that it doesn't. I ran TestSubject.allocate myself to verify that the intercept works, and it does.
I also added the inherited, included, extended and prepended hooks to the tested class and added print statements, but they were never called. This leaves me completely and utterly stumped as to what kind of horrible black magic rack-test is up to under the hood.
So to summarize: the tested instance is created on the fly when the first request is sent. The tested instance is created by fel magic and dodges all attempts to catch it with a hook, so I can find no way to mock it. It almost feels like the author of rake-test has gone to extraordinary lengths to make sure the app instance can't be touched during testing.
I am still fumbling around for a solution.
Ok, I finally got it.
The problem, all along, turned out to be Sinatra::Base.call. Internally, it does dup.call!(env). In other words, every time you run call, Sinatra will duplicate your app instance and send the request to the duplicate, circumventing all mocks and stubs. That explains why none of the life cycle hooks were triggered, since presumably dup uses some low level C magic to clone the instance (citation needed.)
rack-test doesn't do anything convoluted at all, all it does it call app() to retrieve the app, and then call .call(env) on the app. All I need to do then, is stub out the .call method on my class and make sure Sinatra's magic isn't being inserted anywhere. I can use .new! on my app to stop Sinatra from inserting a wrapper and a stack, and I can use .call! to call my app without Sinatra duplicating my app instance.
Note: I can no longer just create an anonymous class inside the app function, since that would create a new class each time app() is called and leave me unable to mock it.
Here's the test from the question, updated to work:
require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"
describe "sinatra app" do
include Rack::Test::Methods
class TestSubject < Sinatra::Base
get "/foo" do
generate_response
end
def generate_response
[200, {"Content-Type" => "text/plain"}, "I am a response"]
end
end
def app
return TestSubject
end
it "should trigger mocked instance methods" do
stub(TestSubject).call { |env|
instance = TestSubject.new!
mock(instance).generate_response {
[200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
}
instance.call! env
}
get "/foo"
assert_equal "I am MOCKED", last_response.body
end
end
Yes, rack test instantiates new app per each request (possibly to avoid collisions and start with a fresh state.) The option here would be to mock the Sinatra::Base derived class itself, inside app:
require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"
describe "instantiated app" do
include Rack::Test::Methods
def app
Class.new(Sinatra::Base) do
get "/foo" do
generate_response
end
def generate_response
[200, {"Content-Type" => "text/plain"}, "I am a response"]
end
end.prepend(Module.new do # ⇐ HERE
def generate_response
[200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
end
end).new!
end
it "should trigger mocked instance methods" do
get "/foo"
assert_equal "I am MOCKED", last_response.body
end
end
or, to mock app method in a whole.

How to test if some specific rack middleware is being used?

To be more particular, I'm talking about sentry-raven and sinatra here. I saw examples testing sinatra applications, or middlewares. But I didn't see ones testing if some particular middleware is present. Or should I be testing behavior, not configuration (or how should I call it)?
The important thing (I'd say) is the behaviour, but if you wish to check for middleware there are 2 ways I'd suggest after taking a delve into the Sinatra source (there are possibly much easier/better ways):
The env
In the Sinatra source there's a method that uses the env to check if a middleware is already present:
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
# if another CommonLogger is already in the middleware chain.
class CommonLogger < Rack::CommonLogger
def call(env)
env['sinatra.commonlogger'] ? #app.call(env) : super
end
You could do the same thing in a route, e.g.
get "/env-keys" do
env.keys.inspect
end
It'll only show you the middleware if it's inserted something in env hash, e.g.
class MyBad
def initialize app, options={}
#app = app
#options = options
end
def call env
#app.call env.merge("mybad" => "I'm sorry!")
end
end
output:
["SERVER_SOFTWARE", "SERVER_NAME", "rack.input", "rack.version", "rack.errors", "rack.multithread", "rack.multiprocess", "rack.run_once", "REQUEST_METHOD", "REQUEST_PATH", "PATH_INFO", "REQUEST_URI", "HTTP_VERSION", "HTTP_HOST", "HTTP_CONNECTION", "HTTP_CACHE_CONTROL", "HTTP_ACCEPT", "HTTP_USER_AGENT", "HTTP_DNT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", "GATEWAY_INTERFACE", "SERVER_PORT", "QUERY_STRING", "SERVER_PROTOCOL", "rack.url_scheme", "SCRIPT_NAME", "REMOTE_ADDR", "async.callback", "async.close", "rack.logger", "mybad", "rack.request.query_string", "rack.request.query_hash", "sinatra.route"]
It's near the end of that list.
The middleware method
There's also a method called middleware in Sinatra::Base:
# Middleware used in this class and all superclasses.
def middleware
if superclass.respond_to?(:middleware)
superclass.middleware + #middleware
else
#middleware
end
end
Call it in the class definition of a modular app and you can get the middlewares in an array:
require 'sinatra/base'
class AnExample < Sinatra::Base
use MyBad
warn "self.middleware = #{self.middleware}"
output:
self.middleware = [[MyBad, [], nil]]
There may be a way to get it from Sinatra::Application, but I haven't looked.
With help from ruby-raven guys, we've got this:
ENV['RACK_ENV'] = 'test'
# the app: start
require 'sinatra'
require 'sentry-raven'
Raven.configure(true) do |config|
config.dsn = '...'
end
use Raven::Rack
get '/' do
'Hello, world!'
end
# the app: end
require 'rspec'
require 'rack/test'
Raven.configure do |config|
logger = ::Logger.new(STDOUT)
logger.level = ::Logger::WARN
config.logger = logger
end
describe 'app' do
include Rack::Test::Methods
def app
#app || Sinatra::Application
end
class TestRavenError < StandardError; end
it 'sends errors to sentry' do
#app = Class.new Sinatra::Application do
get '/' do
raise TestRavenError
end
end
allow(Raven.client).to receive(:send).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven.client).to have_received(:send)
end
end
Or if raven sending requests is in the way (when tests fail because of raven sending requests, and not because of the underlying error), one can disable them:
Raven.configure(true) do |config|
config.should_send = Proc.new { false }
end
And mock Raven.send_or_skip instead:
...
allow(Raven).to receive(:send_or_skip).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven).to have_received(:send_or_skip)
...

Unable to use Warden in Sinatra App: env['warden'] returns nil

I'm writing a Sinatra Rack App and I want to use Warden for authentication. I'm using heroku's toolbelt so I use foreman to run my app. I've found some code that's presumably supposed to get this working. Unfortunately, when I attempt to actually access the Warden env object, it is nil.
I've attempted to use the sinatra_warden gem, but it also has its own bugs (might be related to this one).
config.ru:
require './web.rb'
use Rack::Static, :urls => ["/css", "/img", "/js"], :root => "public"
run MyApp
web.rb:
require 'sinatra'
require 'warden'
require 'data_mapper'
require './config/datamapper.rb'
require './config/warden.rb' # I've tried this inside of MyApp, still didn't work
class MyApp < Sinatra::Base
get '/test' do
env['warden'].authenticate! # env['warden'] is nil :(
end
end
config/warden.rb:
use Rack::Session::Cookie, :secret => ENV['SESSION_SECRET']
use Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = MyApp.new
end
Warden::Manager.serialize_into_session { |user| user.id }
Warden::Manager.serialize_from_session { |id| User.get(id) }
Warden::Manager.before_failure do |env,opts|
# Sinatra is very sensitive to the request method
# since authentication could fail on any type of method, we need
# to set it for the failure app so it is routed to the correct block
env['REQUEST_METHOD'] = "POST"
end
Warden::Strategies.add(:password) do
def valid?
params["email"] || params["password"]
end
def authenticate!
u = User.authenticate(params["email"], params["password"])
u.nil? ? fail!("Could not log in") : success!(u)
end
end
Versions:
Sinatra: 1.1.0
Warden: 1.2.1
Rack: 1.4.1
Ruby: 1.9.3p194
Foreman: 0.60.0
Any ideas how to use Warden the set up I've described?
(P.S. Out of curiosity, what exactly is the env variable?)
Rack internally uses the class Rack::Builder to parse your config.ru file and wrap directives to build up the middleware components.
I believe your builder calls to use in config/warden.rb are getting ignored. It may work to remove the directives from that file and add them to the middleware stack in config.ru:
require './web.rb'
use Rack::Session::Cookie, :secret => ENV['SESSION_SECRET']
use Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = MyApp.new
end
use Rack::Static, :urls => ["/css", "/img", "/js"], :root => "public"
run MyApp
Put a link to your config/warden in your config.ru
require File.dirname(__FILE__) + '/config/warden'
Read the warden readme. Or look right in the lib/warden.rb
I put
Warden.test_mode!
in place of the env call at the /test path and get a nice blank page at
http://localhost:9292/test
Some bloggers have stated that there isn't a lot of documentation for warden but I disagree. There is a whole wiki. see https://github.com/hassox/warden/wiki
Take it slow and find out how to use middleware in Rack. Here's a very good article https://blog.engineyard.com/2015/understanding-rack-apps-and-middleware
I think maybe you might want to start out with tests as I found a good example and you could use it with your app.
ENV['RACK_ENV'] = 'test'
require 'test/unit'
require 'rack/test'
require File.dirname(__FILE__) + '/web'
class AuthenticationTest < Test::Unit::TestCase
include Rack::Test::Methods
def app
WardenTest #MyApp
end
def test_without_authentication
get '/protected'
assert_equal 401, last_response.status
end
def test_with_bad_credentials
authorize 'bad', 'boy'
get '/protected'
assert_equal 401, last_response.status
end
def test_with_proper_credentials
authorize 'admin', 'admin'
get '/protected'
assert_equal 200, last_response.status
assert_equal "You're welcome, authenticated client", last_response.body
end
end
Then a few routes added to your app.
helpers do
def protected!
return if authorized?
headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
halt 401, "Not authorized\n"
end
def authorized?
#auth ||= Rack::Auth::Basic::Request.new(request.env)
#auth.provided? and #auth.basic? and #auth.credentials and
#auth.credentials == ['admin', 'admin']
end
end
get '/' do
"Everybody can see this page"
end
get '/protected' do
protected!
"You're welcome, authenticated client"
end
In my experience working with Ruby, it's always a good idea to start out with tests for any new project. I often test little pieces first though just to gain an understanding of how they work.
Once you get a better understanding of Rack, especially Rack::Builder, you can use
map '/test' do
...all the middleware needed
run App
end
and try out different configurations to see which ones work best for your needs as I'm doing while I write this.
Enjoy! ;-)

Resources