diff --git a/lib/activeadmin/oidc/engine.rb b/lib/activeadmin/oidc/engine.rb index d696433..98d880b 100644 --- a/lib/activeadmin/oidc/engine.rb +++ b/lib/activeadmin/oidc/engine.rb @@ -156,8 +156,24 @@ def controllers # isolated engines don't try to resolve the controller as # `::ActiveAdmin::Devise::SessionsController` from # the relative string form. - get login_path, to: ::ActiveAdmin::Devise::SessionsController.action(:new), as: :"new_#{scope_name}_session" - delete logout_path, to: ::ActiveAdmin::Devise::SessionsController.action(:destroy), as: :"destroy_#{scope_name}_session" + get login_path, to: ::ActiveAdmin::Devise::SessionsController.action(:new), as: :"new_#{scope_name}_session" + + # Mirror what AA's own Devise integration does in + # lib/active_admin/devise.rb: accept whichever HTTP + # methods Devise.sign_out_via and + # ActiveAdmin.application.logout_link_method combine to. + # Read at route-draw time (not in the enclosing + # after_initialize) so `Rails.application.reload_routes!` + # picks up host changes to either value — useful for + # specs that stub the setting and re-evaluate routes. + # AA 4 dropped `logout_link_method` (its layout uses + # `button_to` + Turbo), so only consult the setting when + # the version still exposes it. + aa_app = ::ActiveAdmin.application + aa_method = aa_app.logout_link_method if aa_app.respond_to?(:logout_link_method) + logout_via = [*::Devise.sign_out_via, aa_method].compact.uniq + + match logout_path, to: ::ActiveAdmin::Devise::SessionsController.action(:destroy), as: :"destroy_#{scope_name}_session", via: logout_via end end end diff --git a/spec/requests/logout_spec.rb b/spec/requests/logout_spec.rb index e190fea..1fbecf6 100644 --- a/spec/requests/logout_spec.rb +++ b/spec/requests/logout_spec.rb @@ -62,4 +62,93 @@ expect(response).to redirect_to("/admin/login") end end + + # The mounted route should accept whichever HTTP method + # `ActiveAdmin.application.logout_link_method` is set to, mirroring + # what AA's own Devise integration does in + # `lib/active_admin/devise.rb`: + # + # sign_out_via: [*::Devise.sign_out_via, ActiveAdmin.application.logout_link_method].uniq + # + # Hardcoding DELETE-only meant AA hosts that kept AA's default + # `logout_link_method = :get` (the vast majority) 404'd on every + # Sign Out click: the rendered `` link goes + # through rails-ujs and lands on a route the gem never mounted for + # GET. + context "with AA's default logout_link_method (:get)" do + before { sign_in admin_user } + + it "accepts GET /admin/logout", skip: (ActiveAdmin::Oidc.aa_v4? && "AA 4 dropped logout_link_method; layout uses button_to + Turbo") do + get "/admin/logout" + + expect(response).to be_redirect + + get "/admin" + expect(response).to redirect_to("/admin/login") + end + end + + # Route-table introspection so a future regression in the mount-time + # method-resolution logic is caught even if no request spec happens + # to exercise the affected verb. Reads the verbs actually advertised + # by the destroy route and compares them against what AA and Devise + # configured. + describe "destroy_admin_user_session route verbs" do + subject(:route) do + Rails.application.routes.routes.find { |r| r.name == "destroy_admin_user_session" } + end + + it "exists" do + expect(route).not_to be_nil + end + + it "includes Devise.sign_out_via" do + Array(::Devise.sign_out_via).each do |method| + expect(route.verb).to match(/#{method.to_s.upcase}/), + "destroy_admin_user_session does not accept #{method.to_s.upcase} (verb: #{route.verb.inspect})" + end + end + + it "includes ActiveAdmin.application.logout_link_method when AA exposes it" do + aa_app = ::ActiveAdmin.application + skip "AA 4 dropped logout_link_method" unless aa_app.respond_to?(:logout_link_method) + + expected = aa_app.logout_link_method&.to_s&.upcase + skip "logout_link_method not set" if expected.nil? + + expect(route.verb).to match(/#{expected}/), + "destroy_admin_user_session does not accept #{expected} (verb: #{route.verb.inspect})" + end + + # End-to-end proof that the gem follows host overrides: change + # AA's setting, reload routes, then drive an actual request + # through the freshly-drawn route. Catches regressions where + # logout_link_method gets read once at boot and frozen into the + # closure (route introspection alone wouldn't catch a frozen + # `via:` array if Rails resolved it before the stub took effect). + %i[get post put delete].each do |method| + it "logs the user out when logout_link_method = #{method.inspect}" do + aa_app = ::ActiveAdmin.application + skip "AA 4 dropped logout_link_method" unless aa_app.respond_to?(:logout_link_method) + + original = aa_app.logout_link_method + begin + allow(aa_app).to receive(:logout_link_method).and_return(method) + Rails.application.reload_routes! + + sign_in admin_user + public_send(method, "/admin/logout") + expect(response).to be_redirect + + # Session is really cleared — subsequent admin request is + # bounced back to the login page. + get "/admin" + expect(response).to redirect_to("/admin/login") + ensure + allow(aa_app).to receive(:logout_link_method).and_return(original) + Rails.application.reload_routes! + end + end + end + end end