From 3ab4c9b0ddb0b070ddfa2eec31be6598f8bcf6d7 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 3 Jun 2026 13:08:46 +0200 Subject: [PATCH 1/2] Failing spec: GET /admin/logout 404s with AA default logout_link_method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ActiveAdmin renders the Sign Out link with whatever ActiveAdmin.application.logout_link_method is configured to — default :get. The gem mounted /admin/logout as DELETE only, so the rendered link going through rails-ujs 404'd on every Sign Out attempt on hosts that keep AA defaults. Next commit will mirror what AA's own Devise integration does (lib/active_admin/devise.rb) and accept whatever methods Devise.sign_out_via + ActiveAdmin.application.logout_link_method combine to. --- spec/requests/logout_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/requests/logout_spec.rb b/spec/requests/logout_spec.rb index e190fea..fd4a5c7 100644 --- a/spec/requests/logout_spec.rb +++ b/spec/requests/logout_spec.rb @@ -62,4 +62,29 @@ 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" do + get "/admin/logout" + + expect(response).to be_redirect + + get "/admin" + expect(response).to redirect_to("/admin/login") + end + end end From c7346319c030586717df033c80f7bf46dd1405c7 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 3 Jun 2026 13:09:26 +0200 Subject: [PATCH 2/2] Mount /admin/logout with the methods AA + Devise configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read whatever methods Devise.sign_out_via and ActiveAdmin.application.logout_link_method combine to (same set AA's own Devise integration uses in lib/active_admin/devise.rb) and mount the destroy route with `via:` that list. Host stays in control — keep AA defaults and the gem mounts a :get logout; set `config.logout_link_method = :delete` and the gem mounts :delete. This block runs in after_initialize, after the host's config/initializers/active_admin.rb has set its value. Turns the failing spec from the previous commit green. --- lib/activeadmin/oidc/engine.rb | 20 +++++++++-- spec/requests/logout_spec.rb | 66 +++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) 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 fd4a5c7..1fbecf6 100644 --- a/spec/requests/logout_spec.rb +++ b/spec/requests/logout_spec.rb @@ -78,7 +78,7 @@ context "with AA's default logout_link_method (:get)" do before { sign_in admin_user } - it "accepts GET /admin/logout" do + 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 @@ -87,4 +87,68 @@ 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