Middleware not always called in capybara feature test (with rack-test) - ruby-on-rails

I have a gem I'm working on that uses a railtie to add a middleware. Very simple stuff, followed the rails guides section almost exactly. Works fine in development/staging/production.
The middleware initializes a hash-like object in the env at a particular key.
But in my capybara tests, this key is only sometimes initialized. I added a debugger to the middleware and found that it isn't called every time I use the visit method.
What's more is that in this particular spec file, there are 4 examples, and each example calls visit once. But when I run the spec file, the middleware is sometimes called 3 times and sometimes called 2 times. Obviously the middleware stack should be called for every request.
tl;dr: sometimes calling visit in my capybara feature specs (with rack-test driver) does not result in my middleware stack being called.
Help?
ruby 2.0.0-p353
rails 4.0.2
capybara 2.2.1
rack-test 0.6.2
EDIT: This is some of the relevant code here: how the middleware is added and what it does. MyGem::Middleware#call is only sometimes called when using Capybara's visit method.
# railtie.rb
module MyGem
class Railtie < Rails::Railtie
initializer "my_gem.configure_rails_initialization" do |app|
app.middleware.use MyGem::Middleware
end
end
end
# middleware.rb
module MyGem
class Middleware
def initialize(app, options={})
#app = app
# options unused
end
def call(env)
# using a special internal version of the Rack::Session::Cookie class
session = MyGem::Rack::Session::Cookie.new(
#app,
:coder => MyGem::Rack::Session::Cookie::Base64::Marshal.new,
:key => ENV_SESSION_KEY,
:path => '/',
:domain => domain(env),
:expire_after => 6.weeks.to_i, # seconds till this expires
:secret => 'my_gem_secret_14f1c4ad25a6be00fe53f5fd2d746167',
)
# use Rack::Session:Cookie method
session.context(env, #app)
end
end
end

Figured it out!
I was also adding a Warden hook that expected the env key to be added after signing users in and out, and if the Warden::Manager middleware was added prior to my gem's middleware, then it would error out when running my hook that expected that env key to be set.
Solution was to do this in my railtie:
app.middleware.insert_before Warden::Manager, MyGem::Middleware

Related

Zeitwerk doesn't requires lib classes properly in Rails 6.1.6

I'm trying to update my app from Rails 5.2 to 6.1, and use Zeitwerk autoload, and I am having some issues for autoloading /lib classes.
I included this in config/application.rb:
config.load_defaults 5.2
config.autoloader = :zeitwerk
config.enable_dependency_loading = true
config.autoload_paths += Dir[Rails.root.join('lib/**/*')]
config.eager_load_paths += Dir[Rails.root.join('lib/**/*')]
And, when I run zeitwerk:check, it alerts me that:
Hold on, I am eager loading the application.
expected file lib/facades/coconut/v2/foo.rb to define constant Foo
It is declared as:
# /lib/facades/coconut/v2/foo.rb
module Coconut
module V2
class Foo
# ...
end
end
end
When I try to debug it (putting a binding.pry in some test file), Zeitwerk says it do not defined, until I call it without modules (namespaces):
[1] pry(#<#filename>)> Coconut::V2::Foo
NameError: uninitialized constant Coconut::V2::Foo
from (pry):1:in `setup`
[2] pry(#<#filename>)> Foo
Zeitwerk::NameError: expected file /my-app/lib/api/coconut/v2/foo.rb to define constant Foo, but didn't
from /usr/local/bundle/gems/zeitwerk-2.5.4/lib/zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded'
[3] pry(#<#filename>)> Coconut::V2::Foo
=> Coconut::V2::Foo
It sounds really strange, because there are another classes in /lib that follows the same structure, but it works well, example
# /lib/api/services/youtube/client.rb
module Services
module Youtube
class Client
# ...
end
end
end
Inspecting it with binding.pry in some test:
pry(#<#filename>)> Services::Youtube::Client
=> Services::Youtube::Client
Has anyone had this problem or know what could be happening?
System:
ruby 2.7.6p219
Rails 6.1.6
Zeitwerk (2.5.4)
An autoload path is a root directory, not its contents.
You need to remove the wildcards as documented here.

Rails 7 controller decorator uninitialised constant error in production only

I am getting the following error zeitwerk/loader/helpers.rb:95:in const_get': uninitialized constant Controllers::BasePublicDecorator (NameError)
This is an error in a local production console using rails c -e production but not an issue in development which works perfectly.
In an engine, CcsCms::PublicTheme, I have a decorator I am using to extend the controller of another CcsCms::Core engine and it is this decorator that is causing the error.
public_theme/app/decorators/decorators/controllers/base_public_decorator.rb
CcsCms::BasePublicController.class_eval do
before_action :set_theme #ensure that #current_theme is available for the
#header in all public views
private
def set_theme
#current_theme = CcsCms::PublicTheme::Theme.current_theme
end
end
This functionality is working perfectly in development but fails in production with an error as follows
The controller I am trying to decorate in the CcsCms::Core engine is CcsCms::BasePublicController.rb
module CcsCms
class BasePublicController < ApplicationController
layout "ccs_cms/layouts/public"
protected
def authorize
end
end
end
in the theme engine with the decorator I am trying to use I have a Gemfile that defines the core engine as follows
gem 'ccs_cms_core', path: '../core'
In the ccs_cms_public_theme.gemspec I am requiring the core engine as a dependency
spec.add_dependency "ccs_cms_core"
in the engine.rb I am requiring the core engine and loading the decorator paths in a config.to_prepare do block
require "deface"
require 'ccs_cms_admin_dashboard'
require 'ccs_cms_custom_page'
require 'ccs_cms_core'
require 'css_menu'
#require 'tinymce-rails'
require 'delayed_job_active_record'
require 'daemons'
require 'sprockets/railtie'
require 'sassc-rails'
module CcsCms
module PublicTheme
class Engine < ::Rails::Engine
isolate_namespace CcsCms::PublicTheme
paths["app/views"] << "app/views/ccs_cms/public_theme"
initializer "ccs_cms.assets.precompile" do |app|
app.config.assets.precompile += %w( public_theme_manifest.js )
end
initializer :assets do |config|
Rails.application.config.assets.paths << root.join("")
end
initializer :append_migrations do |app|
unless app.root.to_s.match?(root.to_s)
config.paths['db/migrate'].expanded.each do |p|
app.config.paths['db/migrate'] << p
end
end
end
initializer :active_job_setup do |app|
app.config.active_job.queue_adapter = :delayed_job
end
config.to_prepare do
Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
request: false,
view_specs: false,
helper_specs: false,
controller_specs: false,
routing_specs: false
g.fixture_replacement :factory_bot
g.factory_bot dir: 'spec/factories'
end
end
end
end
Given that my decorator is given the same name as the controller it is decorating from the core engine but with the .decorator extension I am pretty certain that is everything hooked up correctly, as mentioned, this works perfectly in development but I am unable to start a rails console in a production environment due to this error.
It seems that the class_eval is failing somehow and I can only think that this may be a path issue but I can not figure it out
UPDATE
After quite a big learning curve, thank's muchly to #debugger comments and #Xavier Noria
answer it is clear that my issue comes down to Zeitworks autoload functionality
Rails guides here has an interesting and appealing solution to me
Another use case are engines decorating framework classes:
initializer "decorate ActionController::Base" do
> ActiveSupport.on_load(:action_controller_base) do
> include MyDecoration end end
There, the module object stored in MyDecoration by the time the
initializer runs becomes an ancestor of ActionController::Base, and
reloading MyDecoration is pointless, it won't affect that ancestor
chain.
But maybe this isn't the right solution, I again failed to make it work with the following
initializer "decorate CcsCms::BasePublicController" do
ActiveSupport.on_load(:ccs_cms_base_public_controller) do
include CcsCms::BasePublicDecorator
end
end
Generating the following error
zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded': expected file /home/jamie/Development/rails/comtech/r7/ccs_cms/engines/public_theme/app/decorators/controllers/ccs_cms/base_public_decorator.rb to define constant Controllers::CcsCms::BasePublicDecorator, but didn't (Zeitwerk::NameError)
So back to the solution provided here, thank's again for the answer below I tried the following which did work finally
config.to_prepare do
overrides = Engine.root.join("app", "decorators")
Rails.autoloaders.main.ignore(overrides)
p = Engine.root.join("app", "decorators")
loader = Zeitwerk::Loader.for_gem
loader.ignore(p)
Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
Problem here is that when lazy loading, nobody is referencing a constant called ...::BasePublicDecorator. However, Zeitwerk expects that constant to be defined in that file, and the mismatch is found when eager loading.
The solution is to configure the autoloader to ignore the decorators, because you are handling their loading, and because they do not define constants after their names. This documentation has an example. It needs to be adapted to your engine, but you'll see the idea.
For completeness, let me also explain that in Zeitwerk, eager loading is a recursive const_get, not a recursive require. This is to guarantee that if you access the constant, loading succeeds or fails consistently in both modes (and it is also a tad more efficient). Recursive const_get still issues require calls via Module#autoload, and if you ran one for some file idempotence also applies, but Zeitwerk detects the expected constant is not defined anyway, which is an error condition.

RSpec and Rack middleware: how to make sure that it is active in a spec/dummy request spec

I have a Rack middleware that inserts itself automatically into the app when the gem is specified within the Gemfile:
gem 'headhunter'
Then the middleware registers itself like this:
module Headhunter
module Rails
class Railtie < ::Rails::Railtie
initializer "headhunter.hijack" do |app|
head_hunter = Runner.new(::Rails.root)
at_exit do
head_hunter.report
head_hunter.clean_up!
end
app.middleware.insert(0, Headhunter::Rack::CapturingMiddleware, head_hunter)
end
end
end
end
Now I want to make sure that the middleware is actually inserted in an RSpec feature/request spec.
I have a dummy Rails app in spec/dummy which does the following:
require 'spec_helper'
feature 'Middleware integration' do
scenario "Integrating the middleware into the Rack stack" do
expect(Headhunter::Rack::CapturingMiddleware.any_instance).to receive(:call)
visit posts_path
end
end
Sadly this fails and I don't know why. By placing a binding.pry within call I can tell for sure that it is called.
Is there maybe even a better way to test this? Thanks a lot for help.

How to initialize a Gem in Rails 3? How to register Template Handlers?

My gem works fine with Rails 2.3 (using Bundler) but I cannot get it loaded in Rails 3.
my_app/Gemfile:
gem 'mygem', '>= 0.1.0', :path => '../mygem/'
I know that it is found because I managed to get it "working" once.
I've read between the lines of many blog posts and rails tickets that rails/init.rb is deprecated and a toplevel init.rb should be used that requires the main lib file. However, none of them gets executed unless I require 'my_gem'explicitly in my Rails app. This is how it looks like at the moment:
mygem/init.rb`:
require File.join(File.dirname(__FILE__), 'lib', 'my_gem')
raise StandardError, "derailed!"
mygem/lib/my_gem:
require 'my_gem/template_handler'
raise StandardError, "derailed!"
module MyGem
class Railtie < Rails::Railtie
raise StandardError, "derailed!"
initializer "template handler registration" do
raise StandardError, "derailed!"
ActionView::Template.register_template_handler :act, TemplateHandler
end
end
end
ActionView::Template.register_template_handler :act, TemplateHandler
I cluttered everything with raise statements... the fact that none of them is raised makes me think that all my attempts to register my template handler have been in vain... ;)
I am running out of ideas to get my gem loaded. my_gem/lib is in the $LOAD_PATH, everything seems to be fine.

Rails 3, HTTP extensions (WebDAV) and Rack App mounting

1 The following is more to point out to the code devs an issue of rails that can be percieved as a flaw.
2 And also me asking some oppinions from people who know better.
I want to add WebDAV to my Rails 3 App with Warden authentication. My warden middleware is injected via Devise.
http://github.com/chrisroberts/dav4rack
http://github.com/hassox/warden
http://github.com/plataformatec/devise
I cannot mount DAV4Rack handlers from inside rails app (routes), like this:
# in config/routes.rb
mount DAV4Rack::Handler.new(
:root => Rails.root.to_s, # <= it's just an example
:root_uri_path => '/webdav',
:resource_class => Dav::DocumentResource # <= my custom resource, you could use FileResource from dav4rack
), :at => "/webdav"
because rails validates HTTP verbs (GET POST PUT ..), and webdav uses HTTP extensions like PROPFIND that do not validate, throwing the following exception:
ActionController::UnknownHttpMethod (PROPFIND, accepted HTTP methods are get, head, put, post, delete, and options)
This validation takes place in ActionDispatch:
/usr/local/lib/ruby/gems/1.9.1/gems/actionpack-3.0.0/lib/action_dispatch/http/request.rb +56 +72
in (56) "def request_method" and (72) "def method"
Sample code from ActionDispatch that does the validation, to make things clear:
def method
#method ||= begin
method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
method
end
end
In theory, we could monkey-patch this validation to comply with webdav verbs like the railsdav project used to do (note that is rails 2 there, in rails 3 one needs to monkey-patch action_dispatch/http/request).
To add DAV4Rack handlers to the rails app I have to mount the handler outside of ActionDispatch, at rack level, like this:
# config.ru
require ::File.expand_path('../config/environment', __FILE__)
require 'dav4rack/interceptor'
require 'dav/document_resource'
app = Rack::Builder.new{
map '/webdav/' do
run DAV4Rack::Handler.new(
:root => Rails.root.to_s,
:root_uri_path => '/webdav',
:resource_class => Dav::DocumentResource
)
end
map '/' do
use DAV4Rack::Interceptor, :mappings => {
'/webdav/' => {
:resource_class => Dav::DocumentResource
},
}
run Pmp::Application
end
}.to_app
run app
Now I have Webdav support in my application. But It still needs authentication, and for that I'd like to use warden.
# in document_resource.rb
def check_authentication
puts request.env['warden'] # nil :(
end
Warden is nil because my DAV4Rack::Handler is mounted above the session and warden middleware.
Using "rake middleware" to inspect my stack I can see the following:
> rake middleware
use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActionDispatch::BestStandardsSupport
use Warden::Manager
run Pmp::Application.routes
I believe that by wrapping "Pmp::Application.routes" with DAV handler (just like I do above for "Pmp::Application" in config.ru) will inject my webdav handler in the stack at the right place to satisfy the two conditions:
Be above ActionDispatch method validation code, to avoid ActionController::UnknownHttpMethod
Be below session and Warden::Manager so I can use warden authentication.
How to do that? Looking at "rake middleware" otput it seems obvious to override the "Pmp::Application.routes" method:
# in my app at APP_ROOT/config/application.rb
# override the routes method inherited from Rails::Application#routes
def routes
routes_app = super
app = Rack::Builder.new {
map '/webdav/' do
run DAV4Rack::Handler.new(
:root => Rails.root.to_s,
:root_uri_path => '/webdav',
:resource_class => Dav::DocumentResource
)
end
map '/' do
use DAV4Rack::Interceptor, :mappings => {
'/webdav/' => {
:resource_class => Dav::DocumentResource
},
}
run routes_app
end
}.to_app
class << app; self end.class_eval do
attr_accessor :routes_app
def method_missing(sym, *args, &block)
routes_app.send sym, *args, &block
end
end
app.routes_app = routes_app
app
end
Because our new rack application "app" will be asked a few methods down the chain, that the old rack application "routes_app" used to resopnd to, we delegate theese to the old original application "routes_app" with a little method_missing magic.
And voila: everything is working!
Great success.
Only one problem: I don't like it. There must be a better way to do all this enveloping, other than overriding routes method.
Note that this doesn't work with passenger. The best way seems to be monkey patching rails.
See: dav4rack wiki
THE BIG QUESTION:
IS THERE A BETTER WAY TO ADD A RACK APP JUST ABOVE THE "Pmp::Application#routes" APP BY MEANS OF RACK MOUNT OR OTHER ???
THE BIG CONCLUSION
The "mount" semantics in routes.rb should be rack-level (not rails/railtie/whatever), to allow, in this way, handeling of HTTP extensions, or at least have a method for this case "mount_rack"
And we have a winner.
https://rails.lighthouseapp.com/projects/8994/tickets/5895-allow-mounting-of-rack-apps-that-deal-with-http-extensions

Resources