rsb-admin 0.9.1
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.
- checksums.yaml +7 -0
- data/LICENSE +15 -0
- data/README.md +83 -0
- data/Rakefile +25 -0
- data/app/assets/javascripts/rsb/admin/themes/modern.js +37 -0
- data/app/assets/stylesheets/rsb/admin/themes/default.css +1358 -0
- data/app/assets/stylesheets/rsb/admin/themes/modern.css +1370 -0
- data/app/controllers/concerns/rsb/admin/authorization.rb +21 -0
- data/app/controllers/rsb/admin/admin_controller.rb +138 -0
- data/app/controllers/rsb/admin/admin_users_controller.rb +110 -0
- data/app/controllers/rsb/admin/dashboard_controller.rb +76 -0
- data/app/controllers/rsb/admin/profile_controller.rb +146 -0
- data/app/controllers/rsb/admin/profile_sessions_controller.rb +45 -0
- data/app/controllers/rsb/admin/resources_controller.rb +386 -0
- data/app/controllers/rsb/admin/roles_controller.rb +99 -0
- data/app/controllers/rsb/admin/sessions_controller.rb +139 -0
- data/app/controllers/rsb/admin/settings_controller.rb +203 -0
- data/app/controllers/rsb/admin/two_factor_controller.rb +105 -0
- data/app/helpers/rsb/admin/authorization_helper.rb +49 -0
- data/app/helpers/rsb/admin/branding_helper.rb +38 -0
- data/app/helpers/rsb/admin/formatting_helper.rb +205 -0
- data/app/helpers/rsb/admin/i18n_helper.rb +148 -0
- data/app/helpers/rsb/admin/icons_helper.rb +55 -0
- data/app/helpers/rsb/admin/table_helper.rb +132 -0
- data/app/helpers/rsb/admin/theme_helper.rb +84 -0
- data/app/helpers/rsb/admin/url_helper.rb +109 -0
- data/app/mailers/rsb/admin/admin_mailer.rb +37 -0
- data/app/models/rsb/admin/admin_session.rb +109 -0
- data/app/models/rsb/admin/admin_user.rb +153 -0
- data/app/models/rsb/admin/application_record.rb +10 -0
- data/app/models/rsb/admin/role.rb +63 -0
- data/app/views/layouts/rsb/admin/application.html.erb +45 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.html.erb +11 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.text.erb +11 -0
- data/app/views/rsb/admin/admin_users/_form.html.erb +52 -0
- data/app/views/rsb/admin/admin_users/edit.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/index.html.erb +77 -0
- data/app/views/rsb/admin/admin_users/new.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/show.html.erb +85 -0
- data/app/views/rsb/admin/dashboard/index.html.erb +36 -0
- data/app/views/rsb/admin/profile/edit.html.erb +67 -0
- data/app/views/rsb/admin/profile/show.html.erb +155 -0
- data/app/views/rsb/admin/resources/_filters.html.erb +58 -0
- data/app/views/rsb/admin/resources/_form.html.erb +20 -0
- data/app/views/rsb/admin/resources/_pagination.html.erb +33 -0
- data/app/views/rsb/admin/resources/_table.html.erb +70 -0
- data/app/views/rsb/admin/resources/edit.html.erb +7 -0
- data/app/views/rsb/admin/resources/index.html.erb +49 -0
- data/app/views/rsb/admin/resources/new.html.erb +7 -0
- data/app/views/rsb/admin/resources/page.html.erb +9 -0
- data/app/views/rsb/admin/resources/show.html.erb +55 -0
- data/app/views/rsb/admin/roles/_form.html.erb +197 -0
- data/app/views/rsb/admin/roles/edit.html.erb +7 -0
- data/app/views/rsb/admin/roles/index.html.erb +71 -0
- data/app/views/rsb/admin/roles/new.html.erb +7 -0
- data/app/views/rsb/admin/roles/show.html.erb +99 -0
- data/app/views/rsb/admin/sessions/new.html.erb +31 -0
- data/app/views/rsb/admin/sessions/two_factor.html.erb +39 -0
- data/app/views/rsb/admin/settings/_field.html.erb +115 -0
- data/app/views/rsb/admin/settings/index.html.erb +61 -0
- data/app/views/rsb/admin/shared/_badge.html.erb +1 -0
- data/app/views/rsb/admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/rsb/admin/shared/_empty_state.html.erb +4 -0
- data/app/views/rsb/admin/shared/_flash.html.erb +22 -0
- data/app/views/rsb/admin/shared/_header.html.erb +50 -0
- data/app/views/rsb/admin/shared/_page_tabs.html.erb +21 -0
- data/app/views/rsb/admin/shared/_sidebar.html.erb +99 -0
- data/app/views/rsb/admin/shared/disabled.html.erb +38 -0
- data/app/views/rsb/admin/shared/fields/_checkbox.html.erb +6 -0
- data/app/views/rsb/admin/shared/fields/_datetime.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_email.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_hidden.html.erb +1 -0
- data/app/views/rsb/admin/shared/fields/_json.html.erb +11 -0
- data/app/views/rsb/admin/shared/fields/_number.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_password.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_select.html.erb +12 -0
- data/app/views/rsb/admin/shared/fields/_text.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_textarea.html.erb +10 -0
- data/app/views/rsb/admin/shared/forbidden.html.erb +22 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_header.html.erb +77 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_sidebar.html.erb +135 -0
- data/app/views/rsb/admin/two_factor/backup_codes.html.erb +48 -0
- data/app/views/rsb/admin/two_factor/new.html.erb +53 -0
- data/config/locales/en.yml +140 -0
- data/config/locales/seo.en.yml +21 -0
- data/config/routes.rb +59 -0
- data/db/migrate/20260208000003_create_rsb_admin_tables.rb +43 -0
- data/db/migrate/20260214000001_add_otp_fields_to_rsb_admin_admin_users.rb +9 -0
- data/lib/generators/rsb/admin/install/install_generator.rb +45 -0
- data/lib/generators/rsb/admin/install/templates/rsb_admin_seeds.rb +24 -0
- data/lib/generators/rsb/admin/theme/templates/theme.css.tt +66 -0
- data/lib/generators/rsb/admin/theme/theme_generator.rb +218 -0
- data/lib/generators/rsb/admin/views/views_generator.rb +262 -0
- data/lib/rsb/admin/breadcrumb_item.rb +26 -0
- data/lib/rsb/admin/category_registration.rb +177 -0
- data/lib/rsb/admin/column_definition.rb +89 -0
- data/lib/rsb/admin/configuration.rb +69 -0
- data/lib/rsb/admin/engine.rb +34 -0
- data/lib/rsb/admin/filter_definition.rb +129 -0
- data/lib/rsb/admin/form_field_definition.rb +96 -0
- data/lib/rsb/admin/icons.rb +95 -0
- data/lib/rsb/admin/page_registration.rb +140 -0
- data/lib/rsb/admin/registry.rb +109 -0
- data/lib/rsb/admin/resource_dsl_context.rb +139 -0
- data/lib/rsb/admin/resource_registration.rb +287 -0
- data/lib/rsb/admin/settings_schema.rb +60 -0
- data/lib/rsb/admin/test_kit/helpers.rb +316 -0
- data/lib/rsb/admin/test_kit/resource_test_case.rb +193 -0
- data/lib/rsb/admin/test_kit.rb +11 -0
- data/lib/rsb/admin/theme_definition.rb +46 -0
- data/lib/rsb/admin/themes/modern.rb +44 -0
- data/lib/rsb/admin/version.rb +9 -0
- data/lib/rsb/admin.rb +177 -0
- data/lib/tasks/rsb/admin_tasks.rake +23 -0
- metadata +227 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
class SettingsSchema
|
|
6
|
+
def self.build
|
|
7
|
+
RSB::Settings::Schema.new('admin') do
|
|
8
|
+
setting :enabled,
|
|
9
|
+
type: :boolean,
|
|
10
|
+
default: true,
|
|
11
|
+
group: 'General',
|
|
12
|
+
description: 'Enable or disable the admin panel'
|
|
13
|
+
|
|
14
|
+
setting :app_name,
|
|
15
|
+
type: :string,
|
|
16
|
+
default: 'Admin',
|
|
17
|
+
group: 'Branding',
|
|
18
|
+
description: 'Admin panel title'
|
|
19
|
+
|
|
20
|
+
setting :company_name,
|
|
21
|
+
type: :string,
|
|
22
|
+
default: '',
|
|
23
|
+
group: 'Branding',
|
|
24
|
+
description: 'Company or product name'
|
|
25
|
+
|
|
26
|
+
setting :logo_url,
|
|
27
|
+
type: :string,
|
|
28
|
+
default: '',
|
|
29
|
+
group: 'Branding',
|
|
30
|
+
description: 'URL to logo image (sidebar header)'
|
|
31
|
+
|
|
32
|
+
setting :footer_text,
|
|
33
|
+
type: :string,
|
|
34
|
+
default: '',
|
|
35
|
+
group: 'Branding',
|
|
36
|
+
description: 'Custom footer text'
|
|
37
|
+
|
|
38
|
+
setting :theme,
|
|
39
|
+
type: :string,
|
|
40
|
+
default: 'default',
|
|
41
|
+
enum: -> { RSB::Admin.themes.keys.map(&:to_s) },
|
|
42
|
+
group: 'General',
|
|
43
|
+
description: 'Admin panel theme'
|
|
44
|
+
|
|
45
|
+
setting :per_page,
|
|
46
|
+
type: :integer,
|
|
47
|
+
default: 25,
|
|
48
|
+
group: 'General',
|
|
49
|
+
description: 'Default pagination size'
|
|
50
|
+
|
|
51
|
+
setting :require_two_factor,
|
|
52
|
+
type: :boolean,
|
|
53
|
+
default: false,
|
|
54
|
+
group: 'Security',
|
|
55
|
+
description: 'Require all admin users to enable two-factor authentication'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
module TestKit
|
|
6
|
+
module Helpers
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
teardown do
|
|
11
|
+
RSB::Admin.reset!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_test_admin!(permissions: nil, superadmin: false, no_role: false, email: nil,
|
|
16
|
+
password: 'test-password-secure')
|
|
17
|
+
email ||= "test-admin-#{SecureRandom.hex(4)}@example.com"
|
|
18
|
+
|
|
19
|
+
if no_role
|
|
20
|
+
return RSB::Admin::AdminUser.create!(
|
|
21
|
+
email: email,
|
|
22
|
+
password: password,
|
|
23
|
+
password_confirmation: password
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if superadmin || permissions.nil?
|
|
28
|
+
role = RSB::Admin::Role.create!(
|
|
29
|
+
name: "Test Superadmin #{SecureRandom.hex(4)}",
|
|
30
|
+
permissions: { '*' => ['*'] }
|
|
31
|
+
)
|
|
32
|
+
else
|
|
33
|
+
# For empty permissions, use a sentinel value that passes validation
|
|
34
|
+
perm = permissions.empty? ? { '_none' => [] } : permissions
|
|
35
|
+
role = RSB::Admin::Role.create!(
|
|
36
|
+
name: "Test Role #{SecureRandom.hex(4)}",
|
|
37
|
+
permissions: perm
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
RSB::Admin::AdminUser.create!(
|
|
42
|
+
email: email,
|
|
43
|
+
password: password,
|
|
44
|
+
password_confirmation: password,
|
|
45
|
+
role: role
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sign_in_admin(admin, password: 'test-password-secure')
|
|
50
|
+
post rsb_admin.login_path, params: { email: admin.email, password: password }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def assert_admin_authorized
|
|
54
|
+
assert_response :success
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Assert that admin access was denied.
|
|
58
|
+
#
|
|
59
|
+
# Checks that the response is either a redirect (302, for unauthenticated users
|
|
60
|
+
# redirected to login) or a forbidden response (403, for authenticated users
|
|
61
|
+
# without permission). After RFC-002, 403 responses include a rendered
|
|
62
|
+
# forbidden page body.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
#
|
|
66
|
+
# @raise [Minitest::Assertion] if response is not 302 or 403
|
|
67
|
+
#
|
|
68
|
+
# @example Verify a restricted admin is denied
|
|
69
|
+
# restricted = create_test_admin!(permissions: { "dashboard" => ["index"] })
|
|
70
|
+
# sign_in_admin(restricted)
|
|
71
|
+
# get "/admin/roles"
|
|
72
|
+
# assert_admin_denied
|
|
73
|
+
def assert_admin_denied
|
|
74
|
+
assert_includes [302, 403], response.status
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Assert that the forbidden page is rendered with proper content.
|
|
78
|
+
#
|
|
79
|
+
# Verifies that the response has a 403 status and contains the expected
|
|
80
|
+
# forbidden page elements: "Access Denied" title and explanation message.
|
|
81
|
+
# Optionally checks for the presence or absence of the "Go to Dashboard" link.
|
|
82
|
+
#
|
|
83
|
+
# @param dashboard_link [Boolean, nil] if true, asserts dashboard link is present;
|
|
84
|
+
# if false, asserts it's absent; if nil, doesn't check (default: nil)
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
#
|
|
88
|
+
# @raise [Minitest::Assertion] if forbidden page is not rendered correctly
|
|
89
|
+
#
|
|
90
|
+
# @example Basic forbidden page check
|
|
91
|
+
# get "/admin/roles"
|
|
92
|
+
# assert_admin_forbidden_page
|
|
93
|
+
#
|
|
94
|
+
# @example Verify no dashboard link for no-role user
|
|
95
|
+
# get "/admin/dashboard"
|
|
96
|
+
# assert_admin_forbidden_page(dashboard_link: false)
|
|
97
|
+
#
|
|
98
|
+
# @example Verify dashboard link present
|
|
99
|
+
# get "/admin/roles"
|
|
100
|
+
# assert_admin_forbidden_page(dashboard_link: true)
|
|
101
|
+
def assert_admin_forbidden_page(dashboard_link: nil)
|
|
102
|
+
assert_response :forbidden
|
|
103
|
+
|
|
104
|
+
# Use assert_select to properly handle HTML entities
|
|
105
|
+
assert_select 'h1', text: I18n.t('rsb.admin.shared.access_denied'),
|
|
106
|
+
message: "Expected 'Access Denied' title in forbidden page"
|
|
107
|
+
assert_select 'p', text: I18n.t('rsb.admin.shared.access_denied_message'),
|
|
108
|
+
message: 'Expected explanation message in forbidden page'
|
|
109
|
+
|
|
110
|
+
return if dashboard_link.nil?
|
|
111
|
+
|
|
112
|
+
if dashboard_link
|
|
113
|
+
assert_select 'a', text: I18n.t('rsb.admin.shared.go_to_dashboard'),
|
|
114
|
+
message: "Expected 'Go to Dashboard' link in forbidden page"
|
|
115
|
+
else
|
|
116
|
+
assert_select 'a', text: I18n.t('rsb.admin.shared.go_to_dashboard'), count: 0,
|
|
117
|
+
message: "Expected NO 'Go to Dashboard' link in forbidden page"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def assert_admin_resource_registered(model_class, category:)
|
|
122
|
+
registration = RSB::Admin.registry.find_resource(model_class)
|
|
123
|
+
assert registration, "#{model_class} not registered in admin"
|
|
124
|
+
assert_equal category, registration.category_name
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def with_fresh_admin_registry
|
|
128
|
+
old_registry = RSB::Admin.registry
|
|
129
|
+
RSB::Admin.instance_variable_set(:@registry, RSB::Admin::Registry.new)
|
|
130
|
+
yield RSB::Admin.registry
|
|
131
|
+
ensure
|
|
132
|
+
RSB::Admin.instance_variable_set(:@registry, old_registry)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Assert that a column header appears in the response body.
|
|
136
|
+
#
|
|
137
|
+
# This method verifies that a table column with the given label is rendered
|
|
138
|
+
# in the current response. It uses `assert_select` to check for a `<th>` tag
|
|
139
|
+
# containing the label text (case-insensitive). Useful for verifying that
|
|
140
|
+
# registered columns appear in resource index views.
|
|
141
|
+
#
|
|
142
|
+
# @param label [String] the column header text to look for
|
|
143
|
+
#
|
|
144
|
+
# @return [void]
|
|
145
|
+
#
|
|
146
|
+
# @raise [Minitest::Assertion] if the column header is not found
|
|
147
|
+
#
|
|
148
|
+
# @example Verify email column renders
|
|
149
|
+
# get "/admin/users"
|
|
150
|
+
# assert_admin_column_rendered("Email")
|
|
151
|
+
#
|
|
152
|
+
# @example Verify custom column label
|
|
153
|
+
# get "/admin/identities"
|
|
154
|
+
# assert_admin_column_rendered("Identity Email")
|
|
155
|
+
def assert_admin_column_rendered(label)
|
|
156
|
+
assert_select 'th', text: /#{Regexp.escape(label)}/i,
|
|
157
|
+
message: "Expected column '#{label}' in table header"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Assert that a filter input exists in the response.
|
|
161
|
+
#
|
|
162
|
+
# This method verifies that a filter form field with the given key is rendered
|
|
163
|
+
# in the current response. It uses `assert_select` to check for an input/select
|
|
164
|
+
# element with a name attribute matching the filter parameter format `q[key]`.
|
|
165
|
+
# Useful for verifying that registered filters appear in resource index views.
|
|
166
|
+
#
|
|
167
|
+
# @param key [String, Symbol] the filter key to look for
|
|
168
|
+
#
|
|
169
|
+
# @return [void]
|
|
170
|
+
#
|
|
171
|
+
# @raise [Minitest::Assertion] if the filter input is not found
|
|
172
|
+
#
|
|
173
|
+
# @example Verify email filter renders
|
|
174
|
+
# get "/admin/users"
|
|
175
|
+
# assert_admin_filter_rendered("email")
|
|
176
|
+
#
|
|
177
|
+
# @example Verify status filter renders
|
|
178
|
+
# get "/admin/identities"
|
|
179
|
+
# assert_admin_filter_rendered(:status)
|
|
180
|
+
def assert_admin_filter_rendered(key)
|
|
181
|
+
assert_select "[name*='q[#{key}]']",
|
|
182
|
+
message: "Expected filter for '#{key}'"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Assert that breadcrumbs contain specific items.
|
|
186
|
+
#
|
|
187
|
+
# This method verifies that the breadcrumb navigation contains the given labels
|
|
188
|
+
# in the current response body. It performs a simple text match for each label,
|
|
189
|
+
# which is sufficient since breadcrumbs render their labels as plain text.
|
|
190
|
+
# Useful for verifying navigation context in admin views.
|
|
191
|
+
#
|
|
192
|
+
# @param labels [Array<String>] one or more breadcrumb labels to look for
|
|
193
|
+
#
|
|
194
|
+
# @return [void]
|
|
195
|
+
#
|
|
196
|
+
# @raise [Minitest::Assertion] if any breadcrumb label is not found
|
|
197
|
+
#
|
|
198
|
+
# @example Verify dashboard breadcrumb
|
|
199
|
+
# get "/admin/users"
|
|
200
|
+
# assert_admin_breadcrumbs("Dashboard")
|
|
201
|
+
#
|
|
202
|
+
# @example Verify full breadcrumb trail
|
|
203
|
+
# get "/admin/identities/123"
|
|
204
|
+
# assert_admin_breadcrumbs("Dashboard", "Authentication", "Identities", "#123")
|
|
205
|
+
def assert_admin_breadcrumbs(*labels)
|
|
206
|
+
labels.each do |label|
|
|
207
|
+
assert_match label, response.body,
|
|
208
|
+
"Expected breadcrumb '#{label}' in response"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Assert that a form field exists in the response.
|
|
213
|
+
#
|
|
214
|
+
# This method verifies that a form field with the given key is rendered
|
|
215
|
+
# in the current response. It uses `assert_select` to check for an input/select/textarea
|
|
216
|
+
# element with a name attribute containing the field key in Rails form format `[key]`.
|
|
217
|
+
# Useful for verifying that registered form fields appear in new/edit views.
|
|
218
|
+
#
|
|
219
|
+
# @param key [String, Symbol] the form field key to look for
|
|
220
|
+
#
|
|
221
|
+
# @return [void]
|
|
222
|
+
#
|
|
223
|
+
# @raise [Minitest::Assertion] if the form field is not found
|
|
224
|
+
#
|
|
225
|
+
# @example Verify email field renders
|
|
226
|
+
# get "/admin/users/new"
|
|
227
|
+
# assert_admin_form_field("email")
|
|
228
|
+
#
|
|
229
|
+
# @example Verify name field renders
|
|
230
|
+
# get "/admin/users/1/edit"
|
|
231
|
+
# assert_admin_form_field(:name)
|
|
232
|
+
def assert_admin_form_field(key)
|
|
233
|
+
assert_select "[name*='[#{key}]']",
|
|
234
|
+
message: "Expected form field '#{key}'"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Assert that page tabs are rendered for a static page.
|
|
238
|
+
#
|
|
239
|
+
# This method verifies that page action tabs with the given labels are rendered
|
|
240
|
+
# in the current response body. It performs a simple text match for each label,
|
|
241
|
+
# which works because page tabs render their labels as plain text within tab links.
|
|
242
|
+
# Useful for verifying that registered page actions appear as tabs.
|
|
243
|
+
#
|
|
244
|
+
# @param labels [Array<String>] one or more page tab labels to look for
|
|
245
|
+
#
|
|
246
|
+
# @return [void]
|
|
247
|
+
#
|
|
248
|
+
# @raise [Minitest::Assertion] if any page tab label is not found
|
|
249
|
+
#
|
|
250
|
+
# @example Verify analytics page tabs
|
|
251
|
+
# get "/admin/analytics"
|
|
252
|
+
# assert_admin_page_tabs("Overview", "By Metric")
|
|
253
|
+
#
|
|
254
|
+
# @example Verify settings page tabs
|
|
255
|
+
# get "/admin/settings"
|
|
256
|
+
# assert_admin_page_tabs("General", "Security", "Billing")
|
|
257
|
+
def assert_admin_page_tabs(*labels)
|
|
258
|
+
labels.each do |label|
|
|
259
|
+
assert_match label, response.body,
|
|
260
|
+
"Expected page tab '#{label}' in response"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Assert that the current theme CSS is loaded in the layout.
|
|
265
|
+
#
|
|
266
|
+
# This method verifies that the stylesheet link tag for the specified theme
|
|
267
|
+
# is present in the current response. It first checks that the theme is registered,
|
|
268
|
+
# then uses `assert_select` to verify the CSS link tag exists with the theme's
|
|
269
|
+
# CSS path in its href attribute. Useful for verifying theme application.
|
|
270
|
+
#
|
|
271
|
+
# @param theme_key [String, Symbol] the theme key to verify (e.g., :default, :modern)
|
|
272
|
+
#
|
|
273
|
+
# @return [void]
|
|
274
|
+
#
|
|
275
|
+
# @raise [Minitest::Assertion] if the theme is not registered or CSS link is not found
|
|
276
|
+
#
|
|
277
|
+
# @example Verify default theme is loaded
|
|
278
|
+
# get "/admin/dashboard"
|
|
279
|
+
# assert_admin_theme(:default)
|
|
280
|
+
#
|
|
281
|
+
# @example Verify modern theme is loaded
|
|
282
|
+
# # After configuring: RSB::Admin.configuration.theme = :modern
|
|
283
|
+
# get "/admin/dashboard"
|
|
284
|
+
# assert_admin_theme(:modern)
|
|
285
|
+
def assert_admin_theme(theme_key)
|
|
286
|
+
theme = RSB::Admin.themes[theme_key.to_sym]
|
|
287
|
+
assert theme, "Theme '#{theme_key}' not registered"
|
|
288
|
+
assert_select "link[href*='#{theme.css}']",
|
|
289
|
+
message: "Expected theme CSS '#{theme.css}' in layout"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Assert that a custom dashboard page is registered with the given controller.
|
|
293
|
+
#
|
|
294
|
+
# Verifies that `RSB::Admin.registry.dashboard_page` is present and
|
|
295
|
+
# its controller matches the expected value. Useful for extension gems
|
|
296
|
+
# that register a custom dashboard to verify their registration works.
|
|
297
|
+
#
|
|
298
|
+
# @param controller [String] the expected controller path
|
|
299
|
+
#
|
|
300
|
+
# @return [void]
|
|
301
|
+
#
|
|
302
|
+
# @raise [Minitest::Assertion] if no dashboard override or wrong controller
|
|
303
|
+
#
|
|
304
|
+
# @example Verify dashboard override
|
|
305
|
+
# RSB::Admin.registry.register_dashboard(controller: "admin/dashboard")
|
|
306
|
+
# assert_admin_dashboard_override(controller: "admin/dashboard")
|
|
307
|
+
def assert_admin_dashboard_override(controller:)
|
|
308
|
+
page = RSB::Admin.registry.dashboard_page
|
|
309
|
+
assert page, 'Expected a dashboard override to be registered'
|
|
310
|
+
assert_equal controller, page.controller,
|
|
311
|
+
"Expected dashboard controller '#{controller}', got '#{page.controller}'"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
module TestKit
|
|
6
|
+
class ResourceTestCase < ActionDispatch::IntegrationTest
|
|
7
|
+
include RSB::Admin::TestKit::Helpers
|
|
8
|
+
|
|
9
|
+
class_attribute :resource_class
|
|
10
|
+
class_attribute :category
|
|
11
|
+
class_attribute :record_factory
|
|
12
|
+
class_attribute :resource_registry_block
|
|
13
|
+
|
|
14
|
+
setup do
|
|
15
|
+
# Re-register the resource if a registry block is provided
|
|
16
|
+
self.class.resource_registry_block&.call
|
|
17
|
+
@superadmin = create_test_admin!(superadmin: true)
|
|
18
|
+
@registration = RSB::Admin.registry.find_resource(resource_class) if resource_class
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Define how to register the resource (called in setup after reset)
|
|
22
|
+
def self.registers_in_admin(&block)
|
|
23
|
+
self.resource_registry_block = block
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_resource_is_registered_in_admin_registry
|
|
27
|
+
skip 'no resource_class configured' unless resource_class
|
|
28
|
+
assert @registration, "#{resource_class} not found in admin registry"
|
|
29
|
+
assert_equal category, @registration.category_name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_index_page_renders
|
|
33
|
+
skip 'no resource_class configured' unless resource_class
|
|
34
|
+
skip 'no :index action registered' unless @registration&.action?(:index)
|
|
35
|
+
record_factory&.call
|
|
36
|
+
sign_in_admin(@superadmin)
|
|
37
|
+
get admin_resources_path
|
|
38
|
+
assert_admin_authorized
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_show_page_renders
|
|
42
|
+
skip 'no resource_class configured' unless resource_class
|
|
43
|
+
skip 'no :show action registered' unless @registration&.action?(:show)
|
|
44
|
+
record = record_factory&.call
|
|
45
|
+
skip 'no record_factory configured' unless record
|
|
46
|
+
sign_in_admin(@superadmin)
|
|
47
|
+
get admin_resource_path(record)
|
|
48
|
+
assert_admin_authorized
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_admin_with_no_permissions_is_denied
|
|
52
|
+
skip 'no resource_class configured' unless resource_class
|
|
53
|
+
skip 'no :index action registered' unless @registration&.action?(:index)
|
|
54
|
+
restricted = create_test_admin!(permissions: {})
|
|
55
|
+
sign_in_admin(restricted)
|
|
56
|
+
get admin_resources_path
|
|
57
|
+
assert_admin_denied
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Contract test: Verify that registered columns appear in the index table.
|
|
61
|
+
#
|
|
62
|
+
# This test ensures that all columns defined for the resource via the DSL
|
|
63
|
+
# (or auto-detected from the model schema) are rendered as table headers
|
|
64
|
+
# in the index view. It creates a test record, signs in as superadmin,
|
|
65
|
+
# visits the index page, and asserts that each column's label appears
|
|
66
|
+
# in a `<th>` element. Skips if no records exist (table won't render).
|
|
67
|
+
#
|
|
68
|
+
# For auto-detected columns, assertions are more forgiving since some
|
|
69
|
+
# columns may be filtered out by the view (e.g., SKIP_INDEX_COLUMNS).
|
|
70
|
+
# For explicitly-defined columns, all columns must render.
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
73
|
+
#
|
|
74
|
+
# @raise [Minitest::Skip] if resource_class is not configured, :index action not registered, or no records exist
|
|
75
|
+
def test_index_page_renders_registered_columns
|
|
76
|
+
skip 'no resource_class configured' unless resource_class
|
|
77
|
+
skip 'no :index action' unless @registration&.action?(:index)
|
|
78
|
+
record = record_factory&.call
|
|
79
|
+
skip 'no record_factory configured or record creation failed' unless record&.persisted?
|
|
80
|
+
sign_in_admin(@superadmin)
|
|
81
|
+
get admin_resources_path
|
|
82
|
+
assert_admin_authorized
|
|
83
|
+
|
|
84
|
+
# Verify table renders (requires at least one th element)
|
|
85
|
+
begin
|
|
86
|
+
assert_select 'th', minimum: 1
|
|
87
|
+
rescue Minitest::Assertion
|
|
88
|
+
skip 'table not rendered (no records found in view)'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check columns - be forgiving with auto-detected columns
|
|
92
|
+
auto_detected = @registration.columns.nil?
|
|
93
|
+
@registration.index_columns.each do |col|
|
|
94
|
+
next if col.label.blank?
|
|
95
|
+
|
|
96
|
+
if auto_detected
|
|
97
|
+
# For auto-detected columns, silently skip if column doesn't render
|
|
98
|
+
# (it may be filtered by view logic we don't control)
|
|
99
|
+
begin
|
|
100
|
+
assert_admin_column_rendered(col.label)
|
|
101
|
+
rescue Minitest::Assertion
|
|
102
|
+
# Skip this column
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
# For explicitly-defined columns, all must render
|
|
107
|
+
assert_admin_column_rendered(col.label)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Contract test: Verify that registered filters appear in the index view.
|
|
113
|
+
#
|
|
114
|
+
# This test ensures that all filters defined for the resource via the DSL
|
|
115
|
+
# are rendered as form inputs in the index view. It signs in as superadmin,
|
|
116
|
+
# visits the index page, and asserts that each filter's input element exists
|
|
117
|
+
# with the correct name attribute format `q[key]`.
|
|
118
|
+
#
|
|
119
|
+
# @return [void]
|
|
120
|
+
#
|
|
121
|
+
# @raise [Minitest::Skip] if resource_class is not configured or no filters defined
|
|
122
|
+
def test_index_page_renders_registered_filters
|
|
123
|
+
skip 'no resource_class configured' unless resource_class
|
|
124
|
+
skip 'no filters' unless @registration&.filters&.any?
|
|
125
|
+
sign_in_admin(@superadmin)
|
|
126
|
+
get admin_resources_path
|
|
127
|
+
assert_admin_authorized
|
|
128
|
+
@registration.filters.each do |filter|
|
|
129
|
+
assert_admin_filter_rendered(filter.key)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Contract test: Verify that registered form fields appear in the new form.
|
|
134
|
+
#
|
|
135
|
+
# This test ensures that all form fields defined for the resource via the DSL
|
|
136
|
+
# (or auto-detected from the model schema) are rendered as form inputs in the
|
|
137
|
+
# new view. It signs in as superadmin, visits the new page, and asserts that
|
|
138
|
+
# each field's input element exists with the correct name attribute.
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
#
|
|
142
|
+
# @raise [Minitest::Skip] if resource_class is not configured, :new action not registered, or no form_fields defined
|
|
143
|
+
def test_new_page_renders_registered_form_fields
|
|
144
|
+
skip 'no resource_class configured' unless resource_class
|
|
145
|
+
skip 'no :new action' unless @registration&.action?(:new)
|
|
146
|
+
skip 'no form_fields' unless @registration&.form_fields&.any?
|
|
147
|
+
sign_in_admin(@superadmin)
|
|
148
|
+
get "#{admin_resources_path}/new"
|
|
149
|
+
assert_admin_authorized
|
|
150
|
+
@registration.new_form_fields.each do |field|
|
|
151
|
+
assert_admin_form_field(field.key)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Contract test: Verify that breadcrumbs are rendered in resource views.
|
|
156
|
+
#
|
|
157
|
+
# This test ensures that the breadcrumb navigation is present in resource views,
|
|
158
|
+
# including at minimum the "Dashboard" home breadcrumb. It signs in as superadmin,
|
|
159
|
+
# visits the index page, and asserts that the dashboard breadcrumb appears.
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
#
|
|
163
|
+
# @raise [Minitest::Skip] if resource_class is not configured or :index action not registered
|
|
164
|
+
def test_breadcrumbs_are_rendered
|
|
165
|
+
skip 'no resource_class configured' unless resource_class
|
|
166
|
+
skip 'no :index action' unless @registration&.action?(:index)
|
|
167
|
+
sign_in_admin(@superadmin)
|
|
168
|
+
get admin_resources_path
|
|
169
|
+
assert_admin_authorized
|
|
170
|
+
assert_admin_breadcrumbs('Dashboard')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def create_resource(**overrides)
|
|
176
|
+
record_factory&.call(**overrides)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def admin_resources_path
|
|
180
|
+
rsb_admin.send(:"#{resource_class.model_name.route_key}_path")
|
|
181
|
+
rescue NoMethodError
|
|
182
|
+
"/admin/#{resource_class.model_name.route_key}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def admin_resource_path(record)
|
|
186
|
+
rsb_admin.send(:"#{resource_class.model_name.singular_route_key}_path", record)
|
|
187
|
+
rescue NoMethodError
|
|
188
|
+
"/admin/#{resource_class.model_name.route_key}/#{record.id}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Represents a theme definition for the admin panel.
|
|
6
|
+
#
|
|
7
|
+
# ThemeDefinition is an immutable data structure that describes a visual theme
|
|
8
|
+
# for the admin interface, including CSS, JavaScript, and optional view overrides.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] key
|
|
11
|
+
# @return [Symbol] unique theme identifier (e.g., :default, :modern)
|
|
12
|
+
# @!attribute [r] label
|
|
13
|
+
# @return [String] the human-readable theme name
|
|
14
|
+
# @!attribute [r] css
|
|
15
|
+
# @return [String] the asset path for the theme's CSS file
|
|
16
|
+
# @!attribute [r] js
|
|
17
|
+
# @return [String, nil] optional asset path for the theme's JavaScript file
|
|
18
|
+
# @!attribute [r] views_path
|
|
19
|
+
# @return [String, nil] optional view override path prefix
|
|
20
|
+
#
|
|
21
|
+
# @example Building a minimal theme
|
|
22
|
+
# theme = ThemeDefinition.new(
|
|
23
|
+
# key: :default,
|
|
24
|
+
# label: "Default Theme",
|
|
25
|
+
# css: "rsb/admin/themes/default",
|
|
26
|
+
# js: nil,
|
|
27
|
+
# views_path: nil
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @example Building a full-featured theme
|
|
31
|
+
# theme = ThemeDefinition.new(
|
|
32
|
+
# key: :modern,
|
|
33
|
+
# label: "Modern Theme",
|
|
34
|
+
# css: "rsb/admin/themes/modern",
|
|
35
|
+
# js: "rsb/admin/themes/modern",
|
|
36
|
+
# views_path: "rsb/admin/themes/modern/views"
|
|
37
|
+
# )
|
|
38
|
+
ThemeDefinition = Data.define(
|
|
39
|
+
:key, # Symbol — :default, :modern, or custom
|
|
40
|
+
:label, # String
|
|
41
|
+
:css, # String — asset path
|
|
42
|
+
:js, # String | nil — asset path for optional JS
|
|
43
|
+
:views_path # String | nil — view override path prefix
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
module Themes
|
|
6
|
+
# Self-contained registration for the Modern theme.
|
|
7
|
+
#
|
|
8
|
+
# This module encapsulates all registration logic for the built-in Modern
|
|
9
|
+
# theme. It exists as a clear boundary so the modern theme can eventually
|
|
10
|
+
# be extracted to a separate gem (`rsb-admin-modern-theme`).
|
|
11
|
+
#
|
|
12
|
+
# To extract to a separate gem:
|
|
13
|
+
# 1. Move this module + assets + views to rsb-admin-modern-theme gem
|
|
14
|
+
# 2. Replace the call in RSB::Admin.register_built_in_themes with a
|
|
15
|
+
# dependency on the new gem
|
|
16
|
+
# 3. The gem's engine calls RSB::Admin::Themes::Modern.register! in
|
|
17
|
+
# an initializer
|
|
18
|
+
#
|
|
19
|
+
# @example Register the modern theme
|
|
20
|
+
# RSB::Admin::Themes::Modern.register!
|
|
21
|
+
module Modern
|
|
22
|
+
# Registers the Modern theme with RSB::Admin.
|
|
23
|
+
#
|
|
24
|
+
# This method registers the theme definition including CSS (with dark
|
|
25
|
+
# mode support), JavaScript (toggle + persistence), and view overrides
|
|
26
|
+
# (sidebar, header).
|
|
27
|
+
#
|
|
28
|
+
# @return [RSB::Admin::ThemeDefinition] the registered theme definition
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# RSB::Admin::Themes::Modern.register!
|
|
32
|
+
# RSB::Admin.themes[:modern]
|
|
33
|
+
# # => #<data RSB::Admin::ThemeDefinition key=:modern, ...>
|
|
34
|
+
def self.register!
|
|
35
|
+
RSB::Admin.register_theme :modern,
|
|
36
|
+
label: 'Modern',
|
|
37
|
+
css: 'rsb/admin/themes/modern',
|
|
38
|
+
js: 'rsb/admin/themes/modern',
|
|
39
|
+
views_path: 'rsb/admin/themes/modern/views'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|