Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,14 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run all spec suites (default + engine + isolated)
run: bundle exec rake spec:all
# One step per dummy app: each boots a different Rails app, so they
# cannot share a process — and a separate step names which host
# shape broke instead of burying it in a combined run.
- name: Default suite (spec/dummy — ActiveAdmin at /admin)
run: bundle exec rake spec
- name: Engine-mounted Devise suite (spec/dummy_engine)
run: bundle exec rake spec:engine
- name: Isolated-engine suite (spec/dummy_isolated)
run: bundle exec rake spec:isolated
- name: Root-mounted suite (spec/dummy_root — ActiveAdmin at /)
run: bundle exec rake spec:root
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
/spec/dummy_engine/tmp/
/spec/dummy_isolated/log/
/spec/dummy_isolated/tmp/
/spec/dummy_root/log/
/spec/dummy_root/tmp/

# The dummy app's database.yml is checked in — override a global
# ignore rule that excludes "database.yml" everywhere by default.
!/spec/dummy/config/database.yml
!/spec/dummy_engine/config/database.yml
!/spec/dummy_isolated/config/database.yml
!/spec/dummy_root/config/database.yml
11 changes: 8 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ begin

# Default spec suite — boots spec/dummy/ (main-app OIDC-only setup).
RSpec::Core::RakeTask.new(:spec) do |t|
t.exclude_pattern = "spec/{engine,isolated,dummy_engine,dummy_isolated}/**/*"
t.exclude_pattern = "spec/{engine,isolated,root,dummy_engine,dummy_isolated,dummy_root}/**/*"
end

namespace :spec do
Expand All @@ -24,8 +24,13 @@ begin
sh "bundle exec rspec --options /dev/null --require spec_helper -I spec/isolated spec/isolated"
end

desc "Run every spec suite (default + engine + isolated)"
task all: [:spec, :engine, :isolated]
desc "Run root-mounted-ActiveAdmin specs (boots spec/dummy_root/)"
task :root do
sh "bundle exec rspec --options /dev/null --require spec_helper -I spec/root spec/root"
end

desc "Run every spec suite (default + engine + isolated + root)"
task all: [:spec, :engine, :isolated, :root]
end

task default: :spec
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'devise'
require 'active_admin/devise'

module ActiveAdmin
module Oidc
Expand All @@ -17,6 +18,9 @@ module Devise
# The action name matches the provider name registered with Devise
# (`:oidc`, from ActiveAdmin::Oidc::Engine::PROVIDER_NAME).
class OmniauthCallbacksController < ::Devise::OmniauthCallbacksController
# For `#root_path` — ActiveAdmin's namespace-aware landing path.
include ::ActiveAdmin::Devise::Controller

def oidc
auth = request.env['omniauth.auth'] || {}
info = auth['info'] || {}
Expand Down Expand Up @@ -105,10 +109,16 @@ def provision_and_sign_in(claims, kind: 'OIDC')
# sign-in instead of Devise's default (host app root). Hosts
# that don't define a `/` route would otherwise hit a routing
# error immediately after login, and even when `/` does exist
# it's rarely what an admin user wants to see. ActiveAdmin
# always mounts at `/admin`, so we go there directly.
# it's rarely what an admin user wants to see.
#
# `root_path` comes from ActiveAdmin::Devise::Controller and
# follows the host's `config.default_namespace`: `/admin` by
# default, but `/` for hosts that set `default_namespace = false`
# (or `/foo` for a custom namespace). Hardcoding `/admin` 404s on
# those hosts whenever Devise has no stored location — i.e. every
# sign-in that did not start from a protected page.
def after_sign_in_path_for(resource)
stored_location_for(resource) || '/admin'
stored_location_for(resource) || root_path
end

# Devise's `new_session_path(scope)` is only generated when
Expand Down
8 changes: 8 additions & 0 deletions gemfiles/activeadmin_3.5.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ gem "omniauth_openid_connect", "~> 0.6.0"

gem "sprockets-rails", ">= 3.4"
gem "sassc-rails", ">= 2.1"

# json 3.0 dropped the `quirks_mode` keyword that ActiveSupport::JSON
# still passes to JSON.parse/JSON.generate (activesupport 7.2 and 8.0,
# lib/active_support/json/{decoding,encoding}.rb). CI has no lockfile, so
# it picks up json 3.x and every request spec dies with
# `ArgumentError: unknown keyword: quirks_mode`. Drop this once Rails
# ships a version that no longer passes it.
gem "json", "< 3"
4 changes: 4 additions & 0 deletions gemfiles/activeadmin_4.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ gem "propshaft"
gem "importmap-rails"
gem "cssbundling-rails"
gem "tailwindcss-rails", "~> 4.0"

# See the note in activeadmin_3.5.gemfile: json 3.0 removed the
# `quirks_mode` keyword ActiveSupport::JSON still passes.
gem "json", "< 3"
4 changes: 4 additions & 0 deletions spec/dummy_root/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

require_relative "config/application"
Rails.application.load_tasks
9 changes: 9 additions & 0 deletions spec/dummy_root/app/admin/dashboard.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

ActiveAdmin.register_page "Dashboard" do
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

content title: proc { I18n.t("active_admin.dashboard") } do
para "Dummy dashboard."
end
end
3 changes: 3 additions & 0 deletions spec/dummy_root/app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//= link active_admin.css
//= link active_admin.js

1 change: 1 addition & 0 deletions spec/dummy_root/app/assets/javascripts/active_admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// stub for specs — ActiveAdmin layout references this file
1 change: 1 addition & 0 deletions spec/dummy_root/app/assets/stylesheets/active_admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* stub for specs — ActiveAdmin layout references this file */
4 changes: 4 additions & 0 deletions spec/dummy_root/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class ApplicationController < ActionController::Base
end
14 changes: 14 additions & 0 deletions spec/dummy_root/app/models/admin_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class AdminUser < ApplicationRecord
devise :omniauthable,
omniauth_providers: [:oidc]

validates :email, presence: true

serialize :oidc_raw_info, coder: JSON

def active_for_authentication?
super && enabled?
end
end
5 changes: 5 additions & 0 deletions spec/dummy_root/app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
48 changes: 48 additions & 0 deletions spec/dummy_root/config/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require_relative "boot"

require "rails"
require "active_record/railtie"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
begin
require "sprockets/railtie"
rescue LoadError
# sprockets-rails is optional; dummy app uses ActiveAdmin's cssbundling/importmap defaults
end

Bundler.require(*Rails.groups)

require "devise"
require "activeadmin"

# Load the gem under test.
require "activeadmin-oidc"

# Host app that mounts ActiveAdmin at `/` instead of `/admin`
# (`config.default_namespace = false`). Identical to spec/dummy in every
# other respect — see config/initializers/active_admin.rb for the one
# line that differs.
module DummyRoot
class Application < Rails::Application
rails_gem_version = Gem::Version.new(Rails.version)
config.load_defaults(rails_gem_version >= Gem::Version.new("8.0") ? 8.0 : 7.2)
config.eager_load = false
config.root = File.expand_path("..", __dir__)

config.secret_key_base = "test-secret-key-base-#{"x" * 64}"
config.hosts.clear

config.action_controller.allow_forgery_protection = false
config.session_store :cookie_store, key: "_dummy_root_session"

config.action_dispatch.show_exceptions = :none
config.consider_all_requests_local = true
config.active_support.to_time_preserves_timezone = :zone if config.active_support.respond_to?(:to_time_preserves_timezone=)

config.logger = Logger.new($stdout)
config.log_level = ENV.fetch("DUMMY_LOG_LEVEL", "fatal").to_sym
end
end
5 changes: 5 additions & 0 deletions spec/dummy_root/config/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)

require "bundler/setup"
11 changes: 11 additions & 0 deletions spec/dummy_root/config/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test:
adapter: sqlite3
database: ":memory:"
pool: 5
timeout: 5000

development:
adapter: sqlite3
database: db/development.sqlite3
pool: 5
timeout: 5000
4 changes: 4 additions & 0 deletions spec/dummy_root/config/environment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

require_relative "application"
DummyRoot::Application.initialize!
17 changes: 17 additions & 0 deletions spec/dummy_root/config/environments/test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

Rails.application.configure do
config.cache_classes = true
config.eager_load = false
config.public_file_server.enabled = true
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_dispatch.show_exceptions = :none
config.action_controller.allow_forgery_protection = false
config.active_support.deprecation = :stderr
config.active_support.disallowed_deprecation = :raise
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: "www.example.com" }
config.i18n.raise_on_missing_translations = false
config.log_level = :fatal
end
15 changes: 15 additions & 0 deletions spec/dummy_root/config/initializers/active_admin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

ActiveAdmin.setup do |config|
config.site_title = "Dummy Root"
config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user
config.logout_link_path = :destroy_admin_user_session_path

config.root_to = "dashboard#index"
config.comments = false

# The whole point of this dummy app: ActiveAdmin mounted at `/`, not
# `/admin`. Real hosts do this when the admin panel *is* the app.
config.default_namespace = false
end
12 changes: 12 additions & 0 deletions spec/dummy_root/config/initializers/activeadmin_oidc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

ActiveAdmin::Oidc.configure do |c|
c.issuer = "https://idp.example.com"
c.client_id = "client-abc"
# client_secret intentionally blank so PKCE auto-enables in specs.
c.on_login = ->(admin_user, claims) {
# Default dummy on_login: accept, set department if provided.
admin_user.department = claims["department"] if claims.key?("department")
true
}
end
21 changes: 21 additions & 0 deletions spec/dummy_root/config/initializers/devise.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

Devise.setup do |config|
config.mailer_sender = 'please-change-me@example.com'
require 'devise/orm/active_record'

config.case_insensitive_keys = [:email]
config.strip_whitespace_keys = [:email]
config.skip_session_storage = [:http_auth]
config.stretches = Rails.env.test? ? 1 : 11
config.reconfirmable = true
config.expire_all_remember_me_on_sign_out = true
config.password_length = 6..128
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
config.reset_password_within = 6.hours
config.sign_out_via = :delete

# OmniAuth strategy registration and path prefix are handled automatically
# by the gem's engine (see lib/activeadmin/oidc/engine.rb) based on the
# ActiveAdmin::Oidc configuration in config/initializers/activeadmin_oidc.rb.
end
9 changes: 9 additions & 0 deletions spec/dummy_root/config/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

Rails.application.routes.draw do
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
# No host-defined `root` route: ActiveAdmin's own `root_to` supplies
# `/`. Anything the gem redirects to outside the ActiveAdmin route
# table therefore 404s, which is what these specs are here to catch.
end
19 changes: 19 additions & 0 deletions spec/dummy_root/db/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

ActiveRecord::Schema[7.2].define(version: 0) do
create_table :admin_users, force: :cascade do |t|
t.string :email
t.string :username
t.string :provider
t.string :uid
t.text :oidc_raw_info
t.string :department
t.boolean :enabled, default: true
t.timestamps
end

add_index :admin_users, :email, unique: true
add_index :admin_users, %i[provider uid], unique: true,
where: "provider IS NOT NULL AND uid IS NOT NULL",
name: "index_admin_users_on_provider_and_uid"
end
Loading
Loading