heartcombo / devise

Flexible authentication solution for Rails with Warden.
http://blog.plataformatec.com.br/tag/devise/
MIT License
23.95k stars 5.55k forks source link

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

Open jeromedalbert opened 3 months ago

jeromedalbert commented 3 months ago

Fixes https://github.com/heartcombo/devise/issues/5705.

Notes:

jeromedalbert commented 2 months ago

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

jeromedalbert commented 1 month ago

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

jeromedalbert commented 1 month ago

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

gmcgibbon commented 1 month ago

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 commented 1 month ago

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

jeromedalbert commented 1 month ago

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 commented 1 month ago

Yeah it is basically that https://github.com/heartcombo/devise/blob/a259ff3c28912a27329727f4a3c2623d3f5cb6f2/lib/devise/rails/routes.rb#L243 happens when routes are loaded, and mappings are global. We only really need to consider controller tests because https://github.com/heartcombo/devise/blob/a259ff3c28912a27329727f4a3c2623d3f5cb6f2/lib/devise/test/controller_helpers.rb#L7-L8 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 https://github.com/heartcombo/devise/blob/a259ff3c28912a27329727f4a3c2623d3f5cb6f2/lib/devise/mapping.rb#L35.

jeromedalbert commented 1 month ago

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:

https://github.com/heartcombo/devise/blob/a259ff3c28912a27329727f4a3c2623d3f5cb6f2/lib/devise/test/integration_helpers.rb#L4-L14

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 commented 1 month ago

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