plutonium 0.45.3 → 0.47.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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +150 -0
- data/.claude/skills/plutonium-assets/SKILL.md +248 -157
- data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
- data/.claude/skills/plutonium-controller/SKILL.md +9 -2
- data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
- data/.claude/skills/plutonium-definition/SKILL.md +521 -7
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
- data/.claude/skills/plutonium-forms/SKILL.md +8 -1
- data/.claude/skills/plutonium-installation/SKILL.md +25 -2
- data/.claude/skills/plutonium-interaction/SKILL.md +32 -2
- data/.claude/skills/plutonium-invites/SKILL.md +11 -7
- data/.claude/skills/plutonium-model/SKILL.md +50 -50
- data/.claude/skills/plutonium-nested-resources/SKILL.md +18 -1
- data/.claude/skills/plutonium-package/SKILL.md +8 -1
- data/.claude/skills/plutonium-policy/SKILL.md +69 -78
- data/.claude/skills/plutonium-portal/SKILL.md +26 -70
- data/.claude/skills/plutonium-testing/SKILL.md +268 -0
- data/.claude/skills/plutonium-views/SKILL.md +9 -2
- data/.yarnrc.yml +1 -0
- data/CHANGELOG.md +38 -0
- data/app/assets/plutonium.css +1 -1
- data/app/views/rodauth/_login_form.html.erb +0 -3
- data/app/views/rodauth/confirm_password.html.erb +0 -4
- data/app/views/rodauth/create_account.html.erb +0 -3
- data/app/views/rodauth/logout.html.erb +0 -3
- data/docs/.vitepress/config.ts +6 -0
- data/docs/guides/nested-resources.md +10 -0
- data/docs/guides/testing.md +154 -0
- data/docs/reference/controller/index.md +9 -4
- data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
- data/docs/superpowers/plans/2026-04-14-plutonium-testing.md +2046 -0
- data/docs/superpowers/plans/2026-04-14-plutonium-testing.md.tasks.json +21 -0
- data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
- data/docs/superpowers/specs/2026-04-14-plutonium-testing-design.md +364 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +8 -0
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
- data/lib/generators/pu/invites/install_generator.rb +8 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
- data/lib/generators/pu/profile/conn_generator.rb +9 -12
- data/lib/generators/pu/profile/install_generator.rb +5 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/portal_generator.rb +4 -9
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
- data/lib/generators/pu/test/install/install_generator.rb +34 -0
- data/lib/generators/pu/test/install/templates/plutonium_testing.rb.tt +14 -0
- data/lib/generators/pu/test/scaffold/scaffold_generator.rb +55 -0
- data/lib/generators/pu/test/scaffold/templates/integration_test.rb.tt +65 -0
- data/lib/plutonium/core/controller.rb +18 -1
- data/lib/plutonium/engine.rb +18 -5
- data/lib/plutonium/testing/auth_helpers.rb +62 -0
- data/lib/plutonium/testing/dsl.rb +73 -0
- data/lib/plutonium/testing/nested_resource.rb +58 -0
- data/lib/plutonium/testing/portal_access.rb +49 -0
- data/lib/plutonium/testing/resource_crud.rb +104 -0
- data/lib/plutonium/testing/resource_definition.rb +61 -0
- data/lib/plutonium/testing/resource_interaction.rb +51 -0
- data/lib/plutonium/testing/resource_model.rb +53 -0
- data/lib/plutonium/testing/resource_policy.rb +72 -0
- data/lib/plutonium/testing.rb +16 -0
- data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +2 -0
- data/package.json +1 -1
- data/yarn.lock +6037 -3893
- metadata +27 -8
- data/.claude/skills/plutonium/skill.md +0 -130
- data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
- data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
- data/.claude/skills/plutonium-profile/SKILL.md +0 -276
- data/.claude/skills/plutonium-theming/SKILL.md +0 -424
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/dsl"
|
|
4
|
+
require "plutonium/testing/auth_helpers"
|
|
5
|
+
|
|
6
|
+
module Plutonium
|
|
7
|
+
module Testing
|
|
8
|
+
module NestedResource
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include Plutonium::Testing::DSL
|
|
11
|
+
include Plutonium::Testing::AuthHelpers
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def resource_tests_for(*args, **kwargs)
|
|
15
|
+
super
|
|
16
|
+
install_nested_tests!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install_nested_tests!
|
|
20
|
+
test "nested: index lists records from current parent" do
|
|
21
|
+
create_resource!(parent: parent_record!)
|
|
22
|
+
get scoped_index_path(parent_record!)
|
|
23
|
+
assert_response :success
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "nested: show on sibling-tenant record returns 404" do
|
|
27
|
+
sibling = create_resource!(parent: other_parent_record!)
|
|
28
|
+
get "#{scoped_index_path(parent_record!)}/#{sibling.id}"
|
|
29
|
+
assert_includes [404, 302], response.status,
|
|
30
|
+
"Expected sibling-tenant record to be inaccessible (404 or redirect), got #{response.status}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parent_record!
|
|
36
|
+
raise NotImplementedError, "Override #parent_record! to return the current tenant"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def other_parent_record!
|
|
40
|
+
raise NotImplementedError, "Override #other_parent_record! to return a sibling tenant"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_resource!(parent:)
|
|
44
|
+
raise NotImplementedError, "Override #create_resource!(parent:) to return a persisted record under the given parent"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def scoped_index_path(parent)
|
|
50
|
+
"#{current_path_prefix}/#{parent.id}/#{resource_collection}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resource_collection
|
|
54
|
+
self.class.resource_tests_config.fetch(:resource).model_name.collection
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/auth_helpers"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Testing
|
|
7
|
+
module PortalAccess
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Plutonium::Testing::AuthHelpers
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
attr_reader :portal_access_config
|
|
13
|
+
|
|
14
|
+
def portal_access_for(portals:, matrix:)
|
|
15
|
+
@portal_access_config = {portals: portals, matrix: matrix}
|
|
16
|
+
install_portal_access_tests!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install_portal_access_tests!
|
|
20
|
+
cfg = portal_access_config
|
|
21
|
+
cfg[:matrix].each do |role_sym, allowed_portals|
|
|
22
|
+
cfg[:portals].each do |portal_sym|
|
|
23
|
+
expected_allow = allowed_portals.include?(portal_sym)
|
|
24
|
+
test "portal access: #{role_sym} -> #{portal_sym} (#{expected_allow ? "allowed" : "blocked"})" do
|
|
25
|
+
login_as_role(role_sym)
|
|
26
|
+
get portal_root_path(portal_sym)
|
|
27
|
+
if expected_allow
|
|
28
|
+
assert_includes [200, 302], response.status,
|
|
29
|
+
"Expected #{role_sym} to access #{portal_sym}, got #{response.status}"
|
|
30
|
+
else
|
|
31
|
+
assert_includes [302, 401, 403, 404], response.status,
|
|
32
|
+
"Expected #{role_sym} blocked from #{portal_sym}, got #{response.status}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def login_as_role(role_sym)
|
|
41
|
+
raise NotImplementedError, "Override #login_as_role(role_sym) to log in the given role"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def portal_root_path(portal_sym)
|
|
45
|
+
raise NotImplementedError, "Override #portal_root_path(portal_sym) to return the URL"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/dsl"
|
|
4
|
+
require "plutonium/testing/auth_helpers"
|
|
5
|
+
|
|
6
|
+
module Plutonium
|
|
7
|
+
module Testing
|
|
8
|
+
module ResourceCrud
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include Plutonium::Testing::DSL
|
|
11
|
+
include Plutonium::Testing::AuthHelpers
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def resource_tests_for(*args, **kwargs)
|
|
15
|
+
super
|
|
16
|
+
install_crud_tests!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install_crud_tests!
|
|
20
|
+
define_crud_test :index do
|
|
21
|
+
create_resource!
|
|
22
|
+
get "#{current_path_prefix}/#{resource_path}"
|
|
23
|
+
assert_response :success
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
define_crud_test :show do
|
|
27
|
+
record = create_resource!
|
|
28
|
+
get "#{current_path_prefix}/#{resource_path}/#{record.id}"
|
|
29
|
+
assert_response :success
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
define_crud_test :new do
|
|
33
|
+
get "#{current_path_prefix}/#{resource_path}/new"
|
|
34
|
+
assert_response :success
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_crud_test :create do
|
|
38
|
+
assert_difference -> { resource_class.count }, 1 do
|
|
39
|
+
post "#{current_path_prefix}/#{resource_path}", params: {param_key => valid_create_params}
|
|
40
|
+
end
|
|
41
|
+
assert_response :redirect
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
define_crud_test :edit do
|
|
45
|
+
record = create_resource!
|
|
46
|
+
get "#{current_path_prefix}/#{resource_path}/#{record.id}/edit"
|
|
47
|
+
assert_response :success
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_crud_test :update do
|
|
51
|
+
record = create_resource!
|
|
52
|
+
patch "#{current_path_prefix}/#{resource_path}/#{record.id}", params: {param_key => valid_update_params}
|
|
53
|
+
assert_response :redirect
|
|
54
|
+
valid_update_params.each do |attr, value|
|
|
55
|
+
next if value.is_a?(String) && value.start_with?("gid://") # skip SGID assoc fields
|
|
56
|
+
assert_equal value, record.reload.public_send(attr),
|
|
57
|
+
"Expected ##{attr} to be updated to #{value.inspect}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
define_crud_test :destroy do
|
|
62
|
+
record = create_resource!
|
|
63
|
+
assert_difference -> { resource_class.count }, -1 do
|
|
64
|
+
delete "#{current_path_prefix}/#{resource_path}/#{record.id}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def define_crud_test(action, &block)
|
|
70
|
+
cfg = resource_tests_config
|
|
71
|
+
return unless cfg[:actions].include?(action)
|
|
72
|
+
return if cfg[:skip].include?(action)
|
|
73
|
+
test("crud: #{action}", &block)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def create_resource!
|
|
78
|
+
raise NotImplementedError, "Override #create_resource! to return a persisted record"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def valid_create_params
|
|
82
|
+
raise NotImplementedError, "Override #valid_create_params to return a Hash of valid attributes for POST"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def valid_update_params
|
|
86
|
+
raise NotImplementedError, "Override #valid_update_params to return a Hash of valid attributes for PATCH"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def resource_class
|
|
92
|
+
self.class.resource_tests_config.fetch(:resource)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resource_path
|
|
96
|
+
resource_class.model_name.collection
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def param_key
|
|
100
|
+
resource_class.model_name.param_key
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/dsl"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Testing
|
|
7
|
+
module ResourceDefinition
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Plutonium::Testing::DSL
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def resource_tests_for(*args, **kwargs)
|
|
13
|
+
super
|
|
14
|
+
install_definition_tests!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install_definition_tests!
|
|
18
|
+
test "definition: class is constantize-able" do
|
|
19
|
+
assert definition_class, "Expected #{resource_class}Definition to exist"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
test "definition: every defineable prop dictionary is queryable" do
|
|
23
|
+
klass = definition_class
|
|
24
|
+
klass._defineable_props_store.each do |prop_plural|
|
|
25
|
+
dict = klass.public_send("defined_#{prop_plural}")
|
|
26
|
+
assert dict.is_a?(Hash), "defined_#{prop_plural} must be Hash, got #{dict.class}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "definition: declared fields exist on the model" do
|
|
31
|
+
klass = definition_class
|
|
32
|
+
return unless klass.respond_to?(:defined_fields)
|
|
33
|
+
klass.defined_fields.each_key do |field_name|
|
|
34
|
+
next if field_name == :id
|
|
35
|
+
assert resource_class.column_names.include?(field_name.to_s) ||
|
|
36
|
+
resource_class.method_defined?(field_name) ||
|
|
37
|
+
resource_class.reflect_on_association(field_name),
|
|
38
|
+
"Field :#{field_name} declared in #{klass} but not defined on #{resource_class}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resource_class
|
|
44
|
+
resource_tests_config.fetch(:resource)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def definition_class
|
|
48
|
+
@definition_class ||= "#{resource_class.name}Definition".constantize
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resource_class
|
|
53
|
+
self.class.resource_class
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def definition_class
|
|
57
|
+
self.class.definition_class
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Testing
|
|
5
|
+
module ResourceInteraction
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class MockViewContext
|
|
9
|
+
def controller = @controller ||= MockController.new
|
|
10
|
+
|
|
11
|
+
class MockController
|
|
12
|
+
def helpers = @helpers ||= MockHelpers.new
|
|
13
|
+
|
|
14
|
+
class MockHelpers
|
|
15
|
+
def current_user = nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def assert_interaction_success(klass, **input)
|
|
21
|
+
outcome = build_interaction(klass, **input).call
|
|
22
|
+
assert outcome.success?, "Expected #{klass} to succeed, got #{outcome.inspect}"
|
|
23
|
+
outcome
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def assert_interaction_failure(klass, **input)
|
|
27
|
+
outcome = build_interaction(klass, **input).call
|
|
28
|
+
assert outcome.failure?, "Expected #{klass} to fail, got #{outcome.inspect}"
|
|
29
|
+
outcome
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def interaction_view_context
|
|
33
|
+
MockViewContext.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def interaction_class
|
|
37
|
+
raise NotImplementedError, "Override #interaction_class to return the interaction under test"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def valid_interaction_input
|
|
41
|
+
raise NotImplementedError, "Override #valid_interaction_input to return a Hash of valid input"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_interaction(klass, **input)
|
|
47
|
+
klass.new(view_context: interaction_view_context, **input)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/dsl"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Testing
|
|
7
|
+
module ResourceModel
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Plutonium::Testing::DSL
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def resource_tests_for(*args, **kwargs)
|
|
13
|
+
super
|
|
14
|
+
install_model_tests!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install_model_tests!
|
|
18
|
+
cfg = resource_tests_config
|
|
19
|
+
|
|
20
|
+
if (assoc = cfg[:associated_with])
|
|
21
|
+
test "model: associated_with(#{assoc}) scope filters records" do
|
|
22
|
+
record = model_test_record
|
|
23
|
+
parent = record.public_send(assoc)
|
|
24
|
+
scoped = record.class.associated_with(parent)
|
|
25
|
+
assert_includes scoped.to_a, record
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if cfg[:sgid_routing]
|
|
30
|
+
test "model: SGID round-trip locates record" do
|
|
31
|
+
record = model_test_record
|
|
32
|
+
sgid = record.to_sgid.to_s
|
|
33
|
+
found = GlobalID::Locator.locate_signed(sgid)
|
|
34
|
+
assert_equal record, found
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Array(cfg[:has_cents]).each do |attr|
|
|
39
|
+
test "model: has_cents :#{attr} provides cents accessor" do
|
|
40
|
+
record = model_test_record
|
|
41
|
+
assert record.respond_to?(attr), "Expected ##{attr}"
|
|
42
|
+
assert record.respond_to?("#{attr}_cents"), "Expected ##{attr}_cents"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def model_test_record
|
|
49
|
+
raise NotImplementedError, "Override #model_test_record to return a persisted record"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/testing/dsl"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Testing
|
|
7
|
+
module ResourcePolicy
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Plutonium::Testing::DSL
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def resource_tests_for(*args, **kwargs)
|
|
13
|
+
super
|
|
14
|
+
install_policy_tests!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install_policy_tests!
|
|
18
|
+
test "policy: matrix is asserted for every (action × role)" do
|
|
19
|
+
matrix = policy_matrix
|
|
20
|
+
roles = policy_roles
|
|
21
|
+
record = policy_record
|
|
22
|
+
policy_klass = policy_class_for(record)
|
|
23
|
+
|
|
24
|
+
matrix.each do |action, allowed_roles|
|
|
25
|
+
roles.each do |role_sym, account_proc|
|
|
26
|
+
account = instance_exec(&account_proc)
|
|
27
|
+
policy = policy_klass.new(record: record, user: account, **policy_context)
|
|
28
|
+
expected = allowed_roles.include?(role_sym)
|
|
29
|
+
actual = policy.public_send("#{action}?")
|
|
30
|
+
assert_equal expected, actual,
|
|
31
|
+
"#{policy_klass}#{action}? for #{role_sym}: expected #{expected}, got #{actual}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "policy: relation_scope returns AR::Relation per role" do
|
|
37
|
+
record = policy_record
|
|
38
|
+
policy_klass = policy_class_for(record)
|
|
39
|
+
policy_roles.each do |role_sym, account_proc|
|
|
40
|
+
account = instance_exec(&account_proc)
|
|
41
|
+
policy = policy_klass.new(record: record.class, user: account, **policy_context)
|
|
42
|
+
scope = policy.apply_scope(record.class.all, type: :active_record_relation)
|
|
43
|
+
assert_kind_of ActiveRecord::Relation, scope, "relation_scope must return AR::Relation for #{role_sym}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def policy_roles
|
|
50
|
+
raise NotImplementedError, "Override #policy_roles to return Hash{role_sym => -> { account }}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def policy_record
|
|
54
|
+
raise NotImplementedError, "Override #policy_record to return a persisted record"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def policy_matrix
|
|
58
|
+
raise NotImplementedError, "Override #policy_matrix to return Hash{action_sym => [role_syms]}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def policy_context
|
|
62
|
+
{entity_scope: nil}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def policy_class_for(record)
|
|
68
|
+
"#{record.class.name}Policy".constantize
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Testing
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require "plutonium/testing/dsl"
|
|
9
|
+
require "plutonium/testing/auth_helpers"
|
|
10
|
+
require "plutonium/testing/resource_crud"
|
|
11
|
+
require "plutonium/testing/resource_policy"
|
|
12
|
+
require "plutonium/testing/resource_definition"
|
|
13
|
+
require "plutonium/testing/resource_interaction"
|
|
14
|
+
require "plutonium/testing/resource_model"
|
|
15
|
+
require "plutonium/testing/nested_resource"
|
|
16
|
+
require "plutonium/testing/portal_access"
|
|
@@ -19,7 +19,12 @@ module Plutonium
|
|
|
19
19
|
render_logo
|
|
20
20
|
|
|
21
21
|
div(class: "w-full bg-[var(--pu-surface)] rounded-[var(--pu-radius-lg)] border border-[var(--pu-border)] md:mt-0 sm:max-w-md xl:p-0", style: "box-shadow: var(--pu-shadow-md)") {
|
|
22
|
-
div(class: "p-6 space-y-4 md:space-y-6 sm:p-8"
|
|
22
|
+
div(class: "p-6 space-y-4 md:space-y-6 sm:p-8") {
|
|
23
|
+
if page_title.present?
|
|
24
|
+
h1(class: "text-xl font-semibold leading-tight tracking-tight text-[var(--pu-text)] md:text-2xl") { page_title }
|
|
25
|
+
end
|
|
26
|
+
yield
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
render_links
|
data/lib/plutonium/version.rb
CHANGED
data/lib/plutonium.rb
CHANGED
|
@@ -27,6 +27,8 @@ module Plutonium
|
|
|
27
27
|
Loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false).tap do |loader|
|
|
28
28
|
loader.ignore("#{__dir__}/generators")
|
|
29
29
|
loader.ignore("#{__dir__}/plutonium/railtie.rb")
|
|
30
|
+
loader.ignore("#{__dir__}/plutonium/testing.rb")
|
|
31
|
+
loader.ignore("#{__dir__}/plutonium/testing")
|
|
30
32
|
loader.ignore("#{__dir__}/rodauth")
|
|
31
33
|
loader.inflector.inflect("ui" => "UI")
|
|
32
34
|
loader.inflector.inflect("workflow_dsl" => "WorkflowDSL")
|
data/package.json
CHANGED