admin_suite 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -1
  3. data/CONTRIBUTING.md +2 -2
  4. data/Gemfile +3 -0
  5. data/README.md +10 -24
  6. data/app/controllers/admin_suite/application_controller.rb +53 -11
  7. data/app/controllers/admin_suite/resources_controller.rb +63 -9
  8. data/app/views/admin_suite/resources/index.html.erb +3 -1
  9. data/app/views/admin_suite/resources/show.html.erb +2 -2
  10. data/lib/admin/base/filter_builder.rb +2 -1
  11. data/lib/admin/base/resource.rb +14 -2
  12. data/lib/admin_suite/auth/host_hook.rb +25 -0
  13. data/lib/admin_suite/auth/http_basic.rb +46 -0
  14. data/lib/admin_suite/auth/strategy.rb +25 -0
  15. data/lib/admin_suite/auth.rb +33 -0
  16. data/lib/admin_suite/configuration.rb +8 -0
  17. data/lib/admin_suite/ui/show_value_formatter.rb +2 -2
  18. data/lib/admin_suite/version.rb +1 -1
  19. data/lib/admin_suite.rb +16 -0
  20. data/lib/generators/admin_suite/install/templates/admin_suite.rb +24 -7
  21. data/test/controllers/resources_controller_test.rb +80 -0
  22. data/test/dummy/config/initializers/admin_suite_auth.rb +8 -0
  23. data/test/integration/authentication_test.rb +99 -0
  24. data/test/integration/authorization_test.rb +131 -0
  25. data/test/integration/read_only_resource_test.rb +130 -0
  26. data/test/lib/auth_http_basic_test.rb +62 -0
  27. data/test/lib/auth_resolution_test.rb +75 -0
  28. data/test/lib/auth_test.rb +29 -0
  29. data/test/lib/resource_observability_extensions_test.rb +53 -0
  30. data/test/test_helper.rb +43 -0
  31. metadata +15 -14
  32. data/docs/README.md +0 -26
  33. data/docs/actions.md +0 -98
  34. data/docs/configuration.md +0 -284
  35. data/docs/development.md +0 -64
  36. data/docs/docs_viewer.md +0 -79
  37. data/docs/fields.md +0 -188
  38. data/docs/installation.md +0 -80
  39. data/docs/portals.md +0 -140
  40. data/docs/releasing.md +0 -67
  41. data/docs/resources.md +0 -237
  42. data/docs/theming.md +0 -63
  43. data/docs/troubleshooting.md +0 -50
@@ -2,16 +2,33 @@
2
2
 
3
3
  # AdminSuite configuration (host app adapter layer).
4
4
  AdminSuite.configure do |config|
5
- # Hook called as a before_action inside the engine.
6
- # config.authenticate = ->(controller) { ... }
7
- config.authenticate = nil
5
+ # --- Authentication (REQUIRED AdminSuite fails closed) ---
6
+ #
7
+ # Built-in HTTP Basic (reads ADMIN_SUITE_USERNAME / ADMIN_SUITE_PASSWORD):
8
+ config.auth_strategy = :http_basic
9
+ # config.auth_options = { username: ENV["ADMIN_SUITE_USERNAME"], password: ENV["ADMIN_SUITE_PASSWORD"] }
10
+ #
11
+ # Or a custom strategy (e.g. your SSO). Subclass AdminSuite::Auth::Strategy,
12
+ # return an actor from #authenticate!(controller), and register it:
13
+ # AdminSuite::Auth.register(:my_sso, MySsoStrategy)
14
+ # config.auth_strategy = :my_sso
15
+ #
16
+ # Or the legacy lambda (still supported):
17
+ # config.authenticate = ->(controller) { ... render/redirect to deny ... }
18
+ #
19
+ # Development/test only — run without authentication (ignored in production):
20
+ # config.allow_unauthenticated = true
21
+
22
+ # Host before_actions the engine skips (it authenticates itself):
23
+ # config.skip_host_before_actions = [ :require_authentication ]
8
24
 
9
- # Actor used for actions/auditing/authorization.
10
- # config.current_actor = ->(controller) { ... }
25
+ # Actor used for actions/auditing/authorization when your strategy does not
26
+ # provide one (legacy fallback):
11
27
  config.current_actor = ->(controller) { controller.respond_to?(:current_user) ? controller.current_user : nil }
12
28
 
13
- # Optional authorization hook (Pundit/CanCan/ActionPolicy/custom).
14
- # config.authorize = ->(actor, action:, subject:, resource:, controller:) { true }
29
+ # Authorization hook — called for every resource request:
30
+ # action is one of :read, :create, :update, :destroy, :execute.
31
+ # config.authorize = ->(actor:, action:, resource:, record:, controller:) { true }
15
32
  config.authorize = nil
16
33
 
17
34
  # Optional sign-out action in the topbar.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class ResourcesControllerTest < ActiveSupport::TestCase
7
+ class TestController < ResourcesController
8
+ attr_writer :test_resource_config
9
+ attr_reader :filter_calls, :paginated_scope
10
+
11
+ def initialize
12
+ super
13
+ @filter_calls = 0
14
+ end
15
+
16
+ private
17
+
18
+ def resource_config
19
+ @test_resource_config
20
+ end
21
+
22
+ def filtered_collection
23
+ @filter_calls += 1
24
+ { total: 37 }
25
+ end
26
+
27
+ def paginate_collection(scope)
28
+ @paginated_scope = scope
29
+ [ Object.new, :paginated ]
30
+ end
31
+ end
32
+
33
+ class StatsResource < Admin::Base::Resource
34
+ index do
35
+ stats do
36
+ stat :legacy, -> { 11 }
37
+ stat :filtered, ->(scope) { scope.fetch(:total) }
38
+ end
39
+ end
40
+ end
41
+
42
+ class BrokenStatsResource < Admin::Base::Resource
43
+ index do
44
+ stats do
45
+ stat :broken, ->(_scope) { raise "boom" }
46
+ end
47
+ end
48
+ end
49
+
50
+ test "stats preserve zero arity calculators and pass the filtered scope to one arity calculators" do
51
+ controller = TestController.new
52
+ controller.test_resource_config = StatsResource
53
+ scope = { total: 37 }
54
+
55
+ stats = controller.send(:calculate_stats, scope)
56
+
57
+ assert_equal 11, stats.first[:value]
58
+ assert_equal 37, stats.second[:value]
59
+ end
60
+
61
+ test "stats preserve the existing calculator rescue behavior" do
62
+ controller = TestController.new
63
+ controller.test_resource_config = BrokenStatsResource
64
+
65
+ assert_equal "N/A", controller.send(:calculate_stats, Object.new).first[:value]
66
+ end
67
+
68
+ test "index reuses one filtered unpaginated scope for stats and pagination" do
69
+ controller = TestController.new
70
+ controller.test_resource_config = StatsResource
71
+
72
+ controller.index
73
+
74
+ assert_equal 1, controller.filter_calls
75
+ assert_equal({ total: 37 }, controller.paginated_scope)
76
+ assert_equal 37, controller.instance_variable_get(:@stats).second[:value]
77
+ assert_equal :paginated, controller.instance_variable_get(:@collection)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The dummy app's pre-auth integration tests exercise pages without
4
+ # authentication. Fail-closed is covered explicitly in
5
+ # test/integration/authentication_test.rb by flipping this off.
6
+ AdminSuite.configure do |config|
7
+ config.allow_unauthenticated = true
8
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "minitest/mock"
5
+
6
+ module AdminSuite
7
+ class AuthenticationTest < ActionDispatch::IntegrationTest
8
+ ROOT = "/internal/admin_suite"
9
+
10
+ def with_config(**overrides)
11
+ saved = overrides.keys.index_with { |k| AdminSuite.config.public_send(k) }
12
+ overrides.each { |k, v| AdminSuite.config.public_send("#{k}=", v) }
13
+ yield
14
+ ensure
15
+ saved.each { |k, v| AdminSuite.config.public_send("#{k}=", v) }
16
+ end
17
+
18
+ test "unconfigured auth fails closed with 403" do
19
+ with_config(allow_unauthenticated: false, auth_strategy: nil, authenticate: nil) do
20
+ get ROOT
21
+ assert_response :forbidden
22
+ assert_includes response.body, "no authentication is configured"
23
+ end
24
+ end
25
+
26
+ test "allow_unauthenticated opens access outside production" do
27
+ with_config(allow_unauthenticated: true, auth_strategy: nil, authenticate: nil) do
28
+ get ROOT
29
+ assert_response :success
30
+ end
31
+ end
32
+
33
+ test "http_basic strategy denies without credentials and allows with them" do
34
+ with_config(allow_unauthenticated: false, auth_strategy: :http_basic,
35
+ auth_options: { username: "ravi", password: "s3cret" }) do
36
+ get ROOT
37
+ assert_response :unauthorized # Basic challenge
38
+
39
+ get ROOT, headers: {
40
+ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("ravi", "s3cret")
41
+ }
42
+ assert_response :success
43
+ end
44
+ end
45
+
46
+ test "legacy authenticate lambda still works" do
47
+ denials = ->(controller) { controller.head :forbidden }
48
+ with_config(allow_unauthenticated: false, auth_strategy: nil, authenticate: denials) do
49
+ get ROOT
50
+ assert_response :forbidden
51
+ end
52
+ end
53
+
54
+ test "allow_unauthenticated is ignored in production" do
55
+ with_config(allow_unauthenticated: true, auth_strategy: nil, authenticate: nil) do
56
+ Rails.stub(:env, ActiveSupport::StringInquirer.new("production")) do
57
+ get ROOT
58
+ assert_response :forbidden
59
+ assert_includes response.body, "no authentication is configured"
60
+ end
61
+ end
62
+ end
63
+
64
+ test "current_actor is consulted at most once per request on the legacy path" do
65
+ calls = 0
66
+ passes = ->(_controller) {}
67
+ counting_actor = lambda { |_controller|
68
+ calls += 1
69
+ nil
70
+ }
71
+
72
+ with_config(allow_unauthenticated: false, auth_strategy: nil, authenticate: passes,
73
+ current_actor: counting_actor) do
74
+ get ROOT
75
+ assert_response :success
76
+ assert_equal 1, calls
77
+ end
78
+ end
79
+
80
+ test "legacy authenticate lambda passes but a raising current_actor lambda is rescued to nil" do
81
+ passes = ->(_controller) {}
82
+ raising_actor = ->(_controller) { raise "boom" }
83
+
84
+ with_config(allow_unauthenticated: false, auth_strategy: nil, authenticate: passes,
85
+ current_actor: raising_actor) do
86
+ get ROOT
87
+ assert_response :success
88
+ end
89
+ end
90
+
91
+ test "host filter skip list is config-driven with require_authentication default" do
92
+ assert_equal [ :require_authentication ], AdminSuite.config.skip_host_before_actions
93
+ # The controller must consume config rather than hardcode the name:
94
+ source = AdminSuite::Engine.root.join("app/controllers/admin_suite/application_controller.rb").read
95
+ assert_includes source, "skip_host_before_actions"
96
+ refute_match(/skip_before_action :require_authentication\b/, source)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # Reuses the in-memory fixture pattern from read_only_resource_test.rb.
6
+ module AuthzFixtures
7
+ class Gadget
8
+ extend ActiveModel::Naming
9
+
10
+ attr_reader :id, :name
11
+
12
+ def initialize(id: 1, name: "Gadget one")
13
+ @id = id
14
+ @name = name
15
+ end
16
+
17
+ def self.all = ReadOnlyResourceFixtures::Relation.new([ new ])
18
+ def self.column_names = %w[id name]
19
+ def self.primary_key = "id"
20
+ def self.columns_hash = { "id" => Struct.new(:type).new(:integer) }
21
+
22
+ def self.find(id)
23
+ raise ActiveRecord::RecordNotFound unless id.to_s == "1"
24
+ new
25
+ end
26
+
27
+ def to_param = id.to_s
28
+ def attributes = { "id" => id, "name" => name }
29
+ end
30
+ end
31
+
32
+ module Admin
33
+ module Resources
34
+ class AuthzGadgetResource < Admin::Base::Resource
35
+ model AuthzFixtures::Gadget
36
+ portal :ops
37
+ section :observability
38
+
39
+ index do
40
+ columns { column :name }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module AdminSuite
47
+ class AuthorizationTest < ActionDispatch::IntegrationTest
48
+ BASE = "/internal/admin_suite/ops/authz_gadgets"
49
+
50
+ def with_authorize(hook)
51
+ saved = AdminSuite.config.authorize
52
+ AdminSuite.config.authorize = hook
53
+ yield
54
+ ensure
55
+ AdminSuite.config.authorize = saved
56
+ end
57
+
58
+ test "nil authorize hook allows requests" do
59
+ with_authorize(nil) do
60
+ get BASE
61
+ assert_response :success
62
+ end
63
+ end
64
+
65
+ test "falsy authorize denies with 403" do
66
+ with_authorize(->(**) { false }) do
67
+ get BASE
68
+ assert_response :forbidden
69
+ end
70
+ end
71
+
72
+ test "authorize receives actor, mapped action, resource, record, controller" do
73
+ captured = nil
74
+ hook = lambda do |actor:, action:, resource:, record:, controller:|
75
+ captured = { action: action, resource: resource, record: record }
76
+ true
77
+ end
78
+
79
+ with_authorize(hook) do
80
+ get "#{BASE}/1"
81
+ end
82
+
83
+ assert_equal :read, captured[:action]
84
+ assert_equal Admin::Resources::AuthzGadgetResource, captured[:resource]
85
+ assert_instance_of AuthzFixtures::Gadget, captured[:record]
86
+ end
87
+
88
+ test "destroy maps to :destroy" do
89
+ captured_action = nil
90
+ with_authorize(->(action:, **) { captured_action = action; false }) do
91
+ delete "#{BASE}/1"
92
+ end
93
+ assert_equal :destroy, captured_action
94
+ end
95
+
96
+ test "execute_action reaches the authorize hook with :execute before the action-name 404" do
97
+ captured_action = nil
98
+ with_authorize(->(action:, **) { captured_action = action; false }) do
99
+ post "#{BASE}/1/execute_action/anything"
100
+ end
101
+ assert_equal :execute, captured_action
102
+ end
103
+
104
+ test "bulk_action reaches the authorize hook with :execute before the action-name 404" do
105
+ captured_action = nil
106
+ with_authorize(->(action:, **) { captured_action = action; false }) do
107
+ post "#{BASE}/bulk_action/anything", params: { ids: [ "1" ] }
108
+ end
109
+ assert_equal :execute, captured_action
110
+ end
111
+
112
+ test "unregistered resource name 404s instead of constantizing a host model" do
113
+ get "/internal/admin_suite/ops/strings"
114
+ assert_response :not_found
115
+ end
116
+
117
+ test "unregistered resource name 404s on mutating verbs instead of reaching the model" do
118
+ delete "/internal/admin_suite/ops/strings/1"
119
+ assert_response :not_found
120
+ end
121
+
122
+ test "unregistered resource name 404s before the authorize hook ever runs" do
123
+ hook_called = false
124
+ with_authorize(->(**) { hook_called = true; false }) do
125
+ get "/internal/admin_suite/ops/strings"
126
+ assert_response :not_found
127
+ end
128
+ refute hook_called, "authorize hook must not run for an unregistered resource name"
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module ReadOnlyResourceFixtures
6
+ class Widget
7
+ extend ActiveModel::Naming
8
+
9
+ attr_reader :id, :name
10
+
11
+ def initialize(id: 1, name: "Observed widget")
12
+ @id = id
13
+ @name = name
14
+ end
15
+
16
+ def self.all
17
+ Relation.new([ new ])
18
+ end
19
+
20
+ def self.column_names
21
+ %w[id name]
22
+ end
23
+
24
+ def self.primary_key
25
+ "id"
26
+ end
27
+
28
+ def self.columns_hash
29
+ { "id" => Struct.new(:type).new(:integer) }
30
+ end
31
+
32
+ def self.find(id)
33
+ raise ActiveRecord::RecordNotFound unless id.to_s == "1"
34
+
35
+ new
36
+ end
37
+
38
+ def to_param
39
+ id.to_s
40
+ end
41
+
42
+ def attributes
43
+ { "id" => id, "name" => name }
44
+ end
45
+
46
+ def ping
47
+ true
48
+ end
49
+ end
50
+ end
51
+
52
+ module Admin
53
+ module Resources
54
+ class ReadOnlyWidgetResource < Admin::Base::Resource
55
+ model ReadOnlyResourceFixtures::Widget
56
+ portal :ops
57
+ section :observability
58
+ read_only
59
+
60
+ index do
61
+ columns do
62
+ column :name
63
+ end
64
+ end
65
+
66
+ actions do
67
+ action :ping
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ module AdminSuite
74
+ class ReadOnlyResourceTest < ActionDispatch::IntegrationTest
75
+ BASE_PATH = "/internal/admin_suite/ops/read_only_widgets"
76
+
77
+ test "direct built in mutation endpoints are rejected" do
78
+ get "#{BASE_PATH}/new"
79
+ assert_response :not_found
80
+
81
+ post BASE_PATH, params: { read_only_resource_fixtures_widget: { name: "changed" } }
82
+ assert_response :not_found
83
+
84
+ get "#{BASE_PATH}/1/edit"
85
+ assert_response :not_found
86
+
87
+ patch "#{BASE_PATH}/1", params: { read_only_resource_fixtures_widget: { name: "changed" } }
88
+ assert_response :not_found
89
+
90
+ delete "#{BASE_PATH}/1"
91
+ assert_response :not_found
92
+ end
93
+
94
+ test "index hides create and edit controls" do
95
+ get BASE_PATH
96
+
97
+ assert_response :success
98
+ assert_includes response.body, "Observed widget"
99
+ refute_includes response.body, "New Widget"
100
+ refute_match(/>\s*Edit\s*</, response.body)
101
+ end
102
+
103
+ test "show mutation controls are conditional on write access" do
104
+ template = AdminSuite::Engine.root.join("app/views/admin_suite/resources/show.html.erb").read
105
+
106
+ assert_includes template, "has_edit_route = !resource_config.read_only?"
107
+ assert_includes template, "has_destroy_route = !resource_config.read_only?"
108
+ end
109
+
110
+ test "toggle endpoint is rejected on read_only resources" do
111
+ post "#{BASE_PATH}/1/toggle", params: { field: "name" }
112
+ assert_response :not_found
113
+ end
114
+
115
+ test "undeclared execute_action names respond 404" do
116
+ post "#{BASE_PATH}/1/execute_action/nonexistent_action"
117
+ assert_response :not_found
118
+ end
119
+
120
+ test "undeclared bulk_action names respond 404" do
121
+ post "#{BASE_PATH}/bulk_action/nonexistent_bulk", params: { ids: ["1"] }
122
+ assert_response :not_found
123
+ end
124
+
125
+ test "declared member actions remain allowed on read_only resources" do
126
+ post "#{BASE_PATH}/1/execute_action/ping"
127
+ refute_equal 404, response.status
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ raise "unset ADMIN_SUITE_USERNAME in test env" if ENV["ADMIN_SUITE_USERNAME"]
4
+
5
+ require "test_helper"
6
+
7
+ module AdminSuite
8
+ class AuthHttpBasicTest < ActiveSupport::TestCase
9
+ # Minimal stand-in for the parts of ActionController the strategy touches.
10
+ class ControllerDouble
11
+ attr_reader :rendered_status, :challenged
12
+
13
+ def initialize(given_username: nil, given_password: nil)
14
+ @given_username = given_username
15
+ @given_password = given_password
16
+ end
17
+
18
+ def authenticate_with_http_basic
19
+ return false if @given_username.nil?
20
+ yield(@given_username, @given_password)
21
+ end
22
+
23
+ def render(plain:, status:)
24
+ @rendered_status = status
25
+ end
26
+
27
+ def request_http_basic_authentication(_realm)
28
+ @challenged = true
29
+ end
30
+ end
31
+
32
+ test "returns an actor for correct credentials" do
33
+ strategy = Auth::HttpBasic.new(username: "ravi", password: "s3cret")
34
+ controller = ControllerDouble.new(given_username: "ravi", given_password: "s3cret")
35
+
36
+ actor = strategy.authenticate!(controller)
37
+
38
+ assert_equal "ravi", actor.username
39
+ assert_equal "http-basic:ravi", actor.to_s
40
+ end
41
+
42
+ test "challenges on wrong credentials and returns nil" do
43
+ strategy = Auth::HttpBasic.new(username: "ravi", password: "s3cret")
44
+ controller = ControllerDouble.new(given_username: "ravi", given_password: "wrong")
45
+
46
+ assert_nil strategy.authenticate!(controller)
47
+ assert controller.challenged
48
+ end
49
+
50
+ test "denies with 403 when credentials are not configured" do
51
+ strategy = Auth::HttpBasic.new(username: nil, password: nil)
52
+ controller = ControllerDouble.new(given_username: "any", given_password: "any")
53
+
54
+ assert_nil strategy.authenticate!(controller)
55
+ assert_equal :forbidden, controller.rendered_status
56
+ end
57
+
58
+ test "is registered as :http_basic" do
59
+ assert_equal Auth::HttpBasic, Auth.lookup(:http_basic)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class AuthResolutionTest < ActiveSupport::TestCase
7
+ class PerformableDouble
8
+ def initialize(performed:) = @performed = performed
9
+ def performed? = @performed
10
+ end
11
+
12
+ setup do
13
+ @saved = {
14
+ auth_strategy: AdminSuite.config.auth_strategy,
15
+ auth_options: AdminSuite.config.auth_options,
16
+ authenticate: AdminSuite.config.authenticate,
17
+ current_actor: AdminSuite.config.current_actor
18
+ }
19
+ end
20
+
21
+ teardown do
22
+ @saved.each { |k, v| AdminSuite.config.public_send("#{k}=", v) }
23
+ end
24
+
25
+ test "config defaults are fail-closed friendly" do
26
+ fresh = AdminSuite::Configuration.new
27
+ assert_nil fresh.auth_strategy
28
+ assert_equal({}, fresh.auth_options)
29
+ assert_equal false, fresh.allow_unauthenticated
30
+ assert_equal [ :require_authentication ], fresh.skip_host_before_actions
31
+ end
32
+
33
+ test "resolves a symbol strategy through the registry with options" do
34
+ AdminSuite.config.auth_strategy = :http_basic
35
+ AdminSuite.config.auth_options = { username: "u", password: "p" }
36
+
37
+ strategy = AdminSuite.resolved_auth_strategy
38
+
39
+ assert_instance_of Auth::HttpBasic, strategy
40
+ assert_equal "u", strategy.options[:username]
41
+ end
42
+
43
+ test "resolves a class strategy directly" do
44
+ klass = Class.new(Auth::Strategy)
45
+ AdminSuite.config.auth_strategy = klass
46
+ assert_instance_of klass, AdminSuite.resolved_auth_strategy
47
+ end
48
+
49
+ test "wraps legacy authenticate lambda when no strategy is set" do
50
+ AdminSuite.config.auth_strategy = nil
51
+ AdminSuite.config.authenticate = ->(_controller) { :called }
52
+ AdminSuite.config.current_actor = ->(_controller) { :legacy_actor }
53
+
54
+ strategy = AdminSuite.resolved_auth_strategy
55
+ assert_instance_of Auth::HostHook, strategy
56
+
57
+ actor = strategy.authenticate!(PerformableDouble.new(performed: false))
58
+ assert_equal :legacy_actor, actor
59
+ end
60
+
61
+ test "legacy lambda that halts (renders/redirects) denies" do
62
+ AdminSuite.config.auth_strategy = nil
63
+ AdminSuite.config.authenticate = ->(_controller) { :redirected_inside }
64
+
65
+ strategy = AdminSuite.resolved_auth_strategy
66
+ assert_nil strategy.authenticate!(PerformableDouble.new(performed: true))
67
+ end
68
+
69
+ test "returns nil when nothing configured" do
70
+ AdminSuite.config.auth_strategy = nil
71
+ AdminSuite.config.authenticate = nil
72
+ assert_nil AdminSuite.resolved_auth_strategy
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class AuthTest < ActiveSupport::TestCase
7
+ class FakeStrategy < AdminSuite::Auth::Strategy
8
+ def authenticate!(_controller) = :fake_actor
9
+ end
10
+
11
+ test "register and lookup a strategy by name" do
12
+ AdminSuite::Auth.register(:fake, FakeStrategy)
13
+ assert_equal FakeStrategy, AdminSuite::Auth.lookup(:fake)
14
+ assert_equal FakeStrategy, AdminSuite::Auth.lookup("fake")
15
+ end
16
+
17
+ test "lookup of unknown strategy raises UnknownStrategyError" do
18
+ assert_raises(AdminSuite::Auth::UnknownStrategyError) do
19
+ AdminSuite::Auth.lookup(:nope)
20
+ end
21
+ end
22
+
23
+ test "base strategy exposes options and requires authenticate!" do
24
+ strategy = AdminSuite::Auth::Strategy.new(username: "u")
25
+ assert_equal({ username: "u" }, strategy.options)
26
+ assert_raises(NotImplementedError) { strategy.authenticate!(nil) }
27
+ end
28
+ end
29
+ end