How to test the request (not response) of a Rack middleware? - ruby

I can see how to test the response of a rack middleware, but how to test the request?
That is, how to test when the middleware changes the request going to the app?
Working in RSpec and Sinatra.

I presume you're meaning testing whether it's changing env...
A middleware goes something like:
class Foo
def initialize(app)
#app = app
end
def call(env)
# do stuff with env ...
status, headers, response = #app.call(env)
# do stuff with status, headers and response
[status, headers, response]
end
end
You could initialize it with a bogus app (or a lambda, for that matter) that returns a dummy response after doing some tests:
class FooTester
attr_accessor :env
def call(env)
# check that env == #env and whatever else you need here
[200, {}, '']
end
end

#Denis's answer would work, but I'd personally prefer an alternative, which is to put the middleware in a bare Rack app (be it Sinatra or whatever) and just pass the request on as the response and test that. It's how most Rack middleware is specced. That, and unit testing the internals of the middleware.
For example, it's what I've done here with a fork of Rack Clicky
Edit: testing middleware separately from the main app.
require 'lib/rack/mymiddelware.rb'
require 'sinatra/base'
describe "My Middleware" do
let(:app) {
Sinatra.new do
use MyMiddleware
get('/') { request.env.inspect }
end
}
let(:expected) { "Something you expect" }
before do
get "/"
end
subject { last_response.body }
it { should == expected }
end

Related

How to prevent error filtering the request via Rack Middleware

Hi I am having an issue in my current project, often we are receiving an error, Current application is running on Rails2.3.5 and Ruby 1.8.7
(ActionController::MethodNotAllowed) "Only getrequests are allowed."
For that I have found few options like
prevent the error blocking non GET/POST/HEAD requests using your webserver
prevent the error filtering the request via Rack Middleware
So would like to know how to prevent it via Rack Middleware.
Please some one suggest/assist me to get rid of this problem.
TIA.
You would need to create custom middleware and insert it before or after as per the requirement in application.rb.
I would need to handle json parse error in one of the projects and I have created the middle ware as below:
class CatchJsonParseErrors
def initialize(app)
#app = app
end
def call(env)
begin
#app.call(env)
rescue ActionDispatch::ParamsParser::ParseError => error
if env['HTTP_ACCEPT'] =~ /application\/json/
error_output = "There was a problem in the JSON: #{error}"
return [
400, { "Content-Type" => "application/json" },
[ { status: 400, error: error_output }.to_json ]
]
else
raise error
end
end
end
end
And added below line in application.rb file :
config.middleware.insert_before ActionDispatch::ParamsParser, "CatchJsonParseErrors"

Faraday::RackBuilder methods

in order to use custom middlewares from faraday docs I see that I have to use the use method. In my use case my custom builder just add a jwt auth token in the header:
Faraday.new(url: wsconfig.base_url) do |builder|
builder.use CustomMiddlewares::JwtAuthentication
builder.request :url_encoded
builder.response :json
builder.adapter :net_http
end
jwt_authentication.rb
require 'jwt'
module CustomMiddlewares
class JwtAuthentication < Faraday::Middleware
def call(env)
payload = RequestStore.store[:jwt_claims].to_h.merge({method: env.method, path: env.url.request_uri})
token = jwt(payload)
Rails.logger.debug { " with token: #{token}" }
env[:request_headers]["Authorization"] = "Token: #{token}"
#app.call(env)
rescue StandardError => e
raise "problem in JwtAuthentication Middleware"
end
private
def jwt(payload, expiration = 1.minute.from_now)
payload = payload.dup
payload['exp'] = expiration.to_i
payload['iss'] = 'cgp'
JWT.encode(payload, key, 'RS256')
end
def key
OpenSSL::PKey::RSA.new(Rails.configuration.x.secrets.ws_config.jwt_private_key)
end
end
end
CustomMiddlewares::JwtAuthentication should only be used on request phase like url_encoded middleware which is adde by request method. I wonder why I cannot do the same with mine:
builder.request CustomMiddlewares::JwtAuthentication
I got:
CustomMiddlewares::VerbosingPseudonymizationWs is not registered on Faraday::Request (Faraday::Error)
If you want to use builder.request you first need to register the middleware like this:
Faraday::Request.register_middleware jwt: -> { CustomMiddlewares::JwtAuthentication }
Afterwards you should be able to call builder.request :jwt. This is due to Faraday::RackBuilder#request essentially calling Faraday::RackBuilder#use with Faraday::Request.lookup_middleware(key) as the first parameter.
See https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L92
and https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L228
This also means that there is no difference between builder.request :jwt and builder.use CustomMiddlewares::JwtAuthentication.
The difference between a request and a response middleware is that the response middlewares should inherit from Faraday::Response::Middleware which makes sure that they only execute on the response (on_complete). See https://github.com/lostisland/faraday/blob/master/lib/faraday/response.rb#L8
I.e., even when registering a middleware via builder.request it can still act on the response if it implements the on_complete callback. Conversely you do not execute any code for the response if you do not implement the callback.

How to use lotus router with Rack::Builder::map

Is there a way to use map and the (lotus)router namespacing together? Below is a sample config.ru I'm trying to get running as a demo.
require 'bundler'
Bundler.require
module Demo
class Application
def initialize
#app = Rack::Builder.new do
map '/this_works' do
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["this_works"]]}
end
map '/api' do
run Lotus::Router.new do
get '/api/', to: ->(env) { [200, {}, ['Welcome to Lotus::Router!']] }
get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
end
end
end
end
def call(env)
#app.call(env)
end
end
end
run Demo::Application.new
Your problem is due to the precedence of do..end in method calls. In your code the section
run Lotus::Router.new do
get '/api/', to: ->(env) { [200, {}, ['Welcome to Lotus::Router!']] }
get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
end
is parsed by Ruby as
run(Lotus::Router.new) do
get '/api/', to: ->(env) { [200, {}, ['Welcome to Lotus::Router!']] }
get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
end
In other words the block is passed to run, not to Lotus::Router.new as you intended, and run simply ignores the block.
To fix it you need to ensure the block is associated with the constructor of the router rather than the call to run. There are a couple of ways to do this. You could use {...} rather than do...end, as that has a higher precedence:
run Lotus::Router.new {
#...
}
An alternative would be to assign the router to a local variable, and use that as the argument to run:
router = Lotus::Router.new do
#...
end
run router

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

Thin, Sinatra, and intercepting static file request to do CAS authentication

I'm using the casrack-the-authenticator gem for CAS authentication. My server is running Thin on top of Sinatra. I've gotten the CAS authentication bit working, but I'm not sure how to tell Rack to intercept "/index.html" requests to confirm the CAS login, and if the user is not allowed to view the page, return a HTTP 403 response instead of serving the actual page. Does anyone have experience with this? Thanks.
My app:
class Foo < Sinatra::Base
enable :sessions
set :public, "public"
use CasrackTheAuthenticator::Simple, :cas_server => "https://my.cas_server.com"
use CasrackTheAuthenticator::RequireCAS
get '/' do
puts "Hello World"
end
end
My rackup file:
require 'foo'
use Rack::CommonLogger
use Rack::Lint
run Foo
Initial attempt at getting Rack to understand authentication in its file service (comments and thoughts welcome):
builder = Rack::Builder.new do
map '/foo/index.html' do
run Proc.new { |env|
user = Rack::Request.new(env).session[CasrackTheAuthenticator::USERNAME_PARAM]
[401, { "Content-Type" => "text/html" }, "CAS Authentication Required"] unless user
# Serve index.html because we detected user
}
end
map '/foo' do
run Foo
end
end
run builder
Casrack-the-Authenticator will put the CAS information into the Rack session. You can pull that out in another piece of Rack middleware or in your Sinatra app.
The following is for a Rails application, but the concept is similar for Sinatra or a Rack middleware:
# in app/controllers/application_controller.rb:
protected
def require_sign_in!
render :nothing => true, :status => 403 unless signed_in?
end
def signed_in?
current_user.present?
end
def current_user
#current_user ||= Person.find_by_username(session[CasrackTheAuthenticator::USERNAME_PARAM])
end

Resources