Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails 8: Make test helpers work with deferred routes #5695

Closed

Conversation

jeromedalbert
Copy link
Contributor

@jeromedalbert jeromedalbert commented Jun 15, 2024

Fixes #5705.

Notes:

  • The sign_in and sign_out controller and integration test helpers all call Devise::Mapping.find_scope!, so that method seemed like a good place to lazy load routes if they were not already loaded.
  • Used try to keep backwards compatibility since reload_routes_unless_loaded only exists in Rails 8.

@jeromedalbert jeromedalbert changed the title Make test helpers work for deferred routes Make test helpers work with Rails 8 deferred routes Jun 15, 2024
@jeromedalbert
Copy link
Contributor Author

Closing, as the original issue is not an issue any more.

@jeromedalbert
Copy link
Contributor Author

Reopening, as the issue reappeared on the latest Rails main.

@jeromedalbert jeromedalbert reopened this Aug 10, 2024
@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Aug 10, 2024

Sorry for the ping @gmcgibbon, mentioning you just in case you recommend a better fix.

@gmcgibbon
Copy link

Ideally, we shouldn't need to load routes manually with the latest approach. There's no stacktrace in the issue. Please provide one and I'll see what I can do.

@jeromedalbert
Copy link
Contributor Author

Ok, I have updated the issue with the fullest backtrace I could produce.

@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Aug 12, 2024

Here is my understanding of what the problem is.

With deferred route drawing, routes are lazy loaded for Rails environments that have config.eager_load disabled, which is the case by default for the development and test environments.

Basically by default for the test environment, whenever a route is visited, routes get lazy loaded. Devise's mappings aren't loaded when the test starts, they are only loaded when the test visits a route, because Devise's entry point is defined in the route files with devise_for or devise_scope.

But in integration and controller tests you typically call the Devise sign_in helper before visiting the route. At this point Devise mappings and scopes aren't loaded yet, so Devise errors out.


This seems like a particular case that is very specific to Devise. I don't think many gems define their helper methods on route load.

@gmcgibbon
Copy link

Yeah it is basically that

mapping = Devise.add_mapping(resource, options)
happens when routes are loaded, and mappings are global. We only really need to consider controller tests because
# quickly sign_in or sign_out a user. Do not use
# `Devise::Test::ControllerHelpers` in integration tests.
integration tests aren't meant to use this API.

I think it makes sense to move the mapping management to the route set, or to add a route load line to

def self.find_scope!(obj)
.

@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Aug 13, 2024

We only really need to consider controller tests

I think we need to consider both controller tests and integration tests because sign_in is defined similarly in integration_helpers.rb:

# Devise::Test::IntegrationHelpers is a helper module for facilitating
# authentication on Rails integration tests to bypass the required steps for
# signin in or signin out a record.
#
# Examples
#
# class PostsTest < ActionDispatch::IntegrationTest
# include Devise::Test::IntegrationHelpers
#
# test 'authenticated users can see posts' do
# sign_in users(:bob)

And as shown in the attached issue, the test fails for an ActionDispatch::IntegrationTest integration test (I also separately checked that it fails for controller tests).

I think it makes sense to move the mapping management to the route set, or to add a route load line

This PR implements your second suggestion as a quick/easy fix by doing Rails.application.try(:reload_routes_unless_loaded). Although it is marked as :nodoc: in the Rails source code, which suggests that it is a private API and that it is not OK to use it outside of Rails. I guess this is on purpose? If so I can update the PR to use the slightly longer Rails.application.routes_reloader.try(:execute_unless_loaded) instead, which is a public API. I guess Rails doesn't need too many public APIs for this anyways, one is good enough, if at all. (Edit: this PR now uses execute_unless_loaded from the public API to be on the safe side)

Otherwise your first suggestion is to move the mapping management to the route set, but I am not familiar with this part and it seems like a more involved fix/refactor for Devise. But if this is the preferred way, it's all good if someone else wants to make a new PR.

@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Aug 16, 2024

Update: I have changed this PR to use the public execute_unless_loaded API to be on the safe side.

@jeromedalbert jeromedalbert changed the title Make test helpers work with Rails 8 deferred routes Rails 8: Make test helpers work with deferred routes Aug 17, 2024
huacnlee added a commit to ruby-china/homeland that referenced this pull request Sep 27, 2024
@AlanFoster
Copy link

I can't comment on the implementation, but as a datapoint - applying this patch locally fixed test our suite regressions after upgrading to rails 8.0.0.beta1 👍

@rafaelfranca
Copy link
Collaborator

Devise::Mapping.find_scope! is called in many other places that have to do with tests. Would not this code make those other methods slower when they don't need to load paths?

@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Oct 19, 2024

Would not this code make those other methods slower

Yes, the first call to find_scope! would be slower if you don't need routes (although see the paragraph below, maybe you do need routes after all). This PR is more of a quick/easy fix and may be not the best way. Gannon suggested either this solution or moving the mapping management to the route set, which seems like a more involved fix/refactor and I am not familiar with route sets. I'd be happy to close this PR if someone has a better solution.

when they don't need to load paths?

Maybe they need load paths after all. Global search results of find_scope! show that it seems to be called mostly by route path/url related methods (which need routes) or test helper methods like sign_in (which also need routes). So I guess places that call Devise::Mapping.find_scope! would need to load paths anyways, and this wouldn't be a concern? I am not familiar with the Devise codebase, but what is a scope really? My guess is that they are things defined in your routes by calls to e.g. devise_scope :users or devise_for :users (which internally calls devise_scope). If that is the case, then methods that call find_scope! are concerning themselves with routes and would need them to be loaded in order to find the scopes defined by those routes. This is just a guess.

@ciihla
Copy link

ciihla commented Nov 11, 2024

I had to do the similar patch locally:

`
module DeviseHelpersPatch
def sign_in(resource, deprecated = nil, scope: nil)
Rails.application.routes_reloader.try(:execute_unless_loaded)
super
end

def sign_out(resource_or_scope)
Rails.application.routes_reloader.try(:execute_unless_loaded)
super
end
end

Devise::Test::ControllerHelpers.prepend(DeviseHelpersPatch)
`

@nashby
Copy link
Collaborator

nashby commented Nov 15, 2024

@jeromedalbert thank you for the PR! Even though it makes sense to call Rails.application.routes_reloader.try(:execute_unless_loaded) in Devise::Mapping.find_scope! I think we need to cover more places where Devise.mappings is used (places like

Devise.mappings.each_value { |m| return m if path.include?(m.send(path_type)) }
or when users call Devise.mappings directly (for any reason))

So I was thinking about something like https://github.com/heartcombo/devise/compare/lazy-routes-fix This approach covers all possible scenarios where Devise.mappings is used. What do you think? /cc @carlosantoniodasilva

@jeromedalbert
Copy link
Contributor Author

jeromedalbert commented Nov 15, 2024

@nashby Thanks for looking into this! Sounds good to me.

I have tested your branch on a local Rails app and Devise test helpers like sign_in seem to work. And upon further inspection it looks like your Devise.mappings wrapper method is only called for my tests that visit a route, not my other tests, which is good as routes are loaded only as needed and this keeps the intent of lazy loading routes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Rails 8: test helpers do not work
6 participants