simple_authorize 0.1.0 → 1.0.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.
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ # RSpec matchers for SimpleAuthorize policies
5
+ #
6
+ # To use these matchers, add this to your spec/rails_helper.rb or spec/spec_helper.rb:
7
+ #
8
+ # require "simple_authorize/rspec"
9
+ #
10
+ # @example
11
+ # RSpec.describe PostPolicy do
12
+ # subject { described_class.new(user, post) }
13
+ #
14
+ # context "as an admin" do
15
+ # let(:user) { build(:admin) }
16
+ #
17
+ # it { is_expected.to permit_action(:destroy) }
18
+ # it { is_expected.to forbid_action(:publish) }
19
+ # it { is_expected.to permit_viewing(:user_id) }
20
+ # it { is_expected.to permit_editing(:published) }
21
+ # end
22
+ # end
23
+ module RSpecMatchers
24
+ extend RSpec::Matchers::DSL
25
+
26
+ # Matcher for checking if a policy permits an action
27
+ #
28
+ # @example
29
+ # it { is_expected.to permit_action(:show) }
30
+ # expect(policy).to permit_action(:update)
31
+ matcher :permit_action do |action|
32
+ match do |policy|
33
+ action_method = action.to_s.end_with?("?") ? action.to_s : "#{action}?"
34
+ policy.public_send(action_method)
35
+ end
36
+
37
+ failure_message do |policy|
38
+ "expected #{policy.class} to permit action :#{action} but it was forbidden"
39
+ end
40
+
41
+ failure_message_when_negated do |policy|
42
+ "expected #{policy.class} to forbid action :#{action} but it was permitted"
43
+ end
44
+ end
45
+
46
+ # Matcher for checking if a policy forbids an action
47
+ #
48
+ # @example
49
+ # it { is_expected.to forbid_action(:destroy) }
50
+ # expect(policy).to forbid_action(:update)
51
+ matcher :forbid_action do |action|
52
+ match do |policy|
53
+ action_method = action.to_s.end_with?("?") ? action.to_s : "#{action}?"
54
+ !policy.public_send(action_method)
55
+ end
56
+
57
+ failure_message do |policy|
58
+ "expected #{policy.class} to forbid action :#{action} but it was permitted"
59
+ end
60
+
61
+ failure_message_when_negated do |policy|
62
+ "expected #{policy.class} to permit action :#{action} but it was forbidden"
63
+ end
64
+ end
65
+
66
+ # Matcher for checking if a policy permits viewing an attribute
67
+ #
68
+ # @example
69
+ # it { is_expected.to permit_viewing(:title) }
70
+ # expect(policy).to permit_viewing(:email)
71
+ matcher :permit_viewing do |attribute|
72
+ match do |policy|
73
+ policy.attribute_visible?(attribute)
74
+ end
75
+
76
+ failure_message do |policy|
77
+ "expected #{policy.class} to permit viewing :#{attribute} but it was hidden"
78
+ end
79
+
80
+ failure_message_when_negated do |policy|
81
+ "expected #{policy.class} to forbid viewing :#{attribute} but it was visible"
82
+ end
83
+ end
84
+
85
+ # Matcher for checking if a policy forbids viewing an attribute
86
+ #
87
+ # @example
88
+ # it { is_expected.to forbid_viewing(:password) }
89
+ # expect(policy).to forbid_viewing(:user_id)
90
+ matcher :forbid_viewing do |attribute|
91
+ match do |policy|
92
+ !policy.attribute_visible?(attribute)
93
+ end
94
+
95
+ failure_message do |policy|
96
+ "expected #{policy.class} to forbid viewing :#{attribute} but it was visible"
97
+ end
98
+
99
+ failure_message_when_negated do |policy|
100
+ "expected #{policy.class} to permit viewing :#{attribute} but it was hidden"
101
+ end
102
+ end
103
+
104
+ # Matcher for checking if a policy permits editing an attribute
105
+ #
106
+ # @example
107
+ # it { is_expected.to permit_editing(:title) }
108
+ # expect(policy).to permit_editing(:body)
109
+ matcher :permit_editing do |attribute|
110
+ match do |policy|
111
+ policy.attribute_editable?(attribute)
112
+ end
113
+
114
+ failure_message do |policy|
115
+ "expected #{policy.class} to permit editing :#{attribute} but it was not editable"
116
+ end
117
+
118
+ failure_message_when_negated do |policy|
119
+ "expected #{policy.class} to forbid editing :#{attribute} but it was editable"
120
+ end
121
+ end
122
+
123
+ # Matcher for checking if a policy forbids editing an attribute
124
+ #
125
+ # @example
126
+ # it { is_expected.to forbid_editing(:id) }
127
+ # expect(policy).to forbid_editing(:published)
128
+ matcher :forbid_editing do |attribute|
129
+ match do |policy|
130
+ !policy.attribute_editable?(attribute)
131
+ end
132
+
133
+ failure_message do |policy|
134
+ "expected #{policy.class} to forbid editing :#{attribute} but it was editable"
135
+ end
136
+
137
+ failure_message_when_negated do |policy|
138
+ "expected #{policy.class} to permit editing :#{attribute} but it was not editable"
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # Auto-include matchers if RSpec is loaded
145
+ if defined?(RSpec)
146
+ RSpec.configure do |config|
147
+ config.include SimpleAuthorize::RSpecMatchers
148
+ end
149
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAuthorize
4
+ # Test helpers for Minitest
5
+ # Include this module in your test cases to get assertion methods for authorization testing
6
+ #
7
+ # @example
8
+ # class PostPolicyTest < ActiveSupport::TestCase
9
+ # include SimpleAuthorize::TestHelpers
10
+ #
11
+ # def test_admin_can_destroy
12
+ # policy = PostPolicy.new(admin_user, post)
13
+ # assert_permit_action(policy, :destroy)
14
+ # end
15
+ # end
16
+ module TestHelpers
17
+ # Assert that a policy permits a specific action
18
+ #
19
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
20
+ # @param action [Symbol, String] The action to check (e.g., :show, :update)
21
+ # @param message [String] Optional custom failure message
22
+ #
23
+ # @example
24
+ # assert_permit_action(policy, :show)
25
+ # assert_permit_action(policy, "update")
26
+ def assert_permit_action(policy, action, message = nil)
27
+ action_method = action.to_s.end_with?("?") ? action.to_s : "#{action}?"
28
+ result = policy.public_send(action_method)
29
+
30
+ message ||= "Expected #{policy.class} to permit action :#{action} but it was forbidden"
31
+ assert result, message
32
+ end
33
+
34
+ # Assert that a policy forbids a specific action
35
+ #
36
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
37
+ # @param action [Symbol, String] The action to check (e.g., :destroy, :update)
38
+ # @param message [String] Optional custom failure message
39
+ #
40
+ # @example
41
+ # assert_forbid_action(policy, :destroy)
42
+ # assert_forbid_action(policy, "update")
43
+ def assert_forbid_action(policy, action, message = nil)
44
+ action_method = action.to_s.end_with?("?") ? action.to_s : "#{action}?"
45
+ result = policy.public_send(action_method)
46
+
47
+ message ||= "Expected #{policy.class} to forbid action :#{action} but it was permitted"
48
+ assert_not result, message
49
+ end
50
+
51
+ # Assert that a policy permits viewing a specific attribute
52
+ #
53
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
54
+ # @param attribute [Symbol, String] The attribute to check (e.g., :title, :email)
55
+ # @param message [String] Optional custom failure message
56
+ #
57
+ # @example
58
+ # assert_permit_viewing(policy, :title)
59
+ # assert_permit_viewing(policy, "email")
60
+ def assert_permit_viewing(policy, attribute, message = nil)
61
+ result = policy.attribute_visible?(attribute)
62
+
63
+ message ||= "Expected #{policy.class} to permit viewing :#{attribute} but it was hidden"
64
+ assert result, message
65
+ end
66
+
67
+ # Assert that a policy forbids viewing a specific attribute
68
+ #
69
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
70
+ # @param attribute [Symbol, String] The attribute to check (e.g., :password, :secret)
71
+ # @param message [String] Optional custom failure message
72
+ #
73
+ # @example
74
+ # assert_forbid_viewing(policy, :user_id)
75
+ # assert_forbid_viewing(policy, "password")
76
+ def assert_forbid_viewing(policy, attribute, message = nil)
77
+ result = policy.attribute_visible?(attribute)
78
+
79
+ message ||= "Expected #{policy.class} to forbid viewing :#{attribute} but it was visible"
80
+ assert_not result, message
81
+ end
82
+
83
+ # Assert that a policy permits editing a specific attribute
84
+ #
85
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
86
+ # @param attribute [Symbol, String] The attribute to check (e.g., :title, :body)
87
+ # @param message [String] Optional custom failure message
88
+ #
89
+ # @example
90
+ # assert_permit_editing(policy, :title)
91
+ # assert_permit_editing(policy, "body")
92
+ def assert_permit_editing(policy, attribute, message = nil)
93
+ result = policy.attribute_editable?(attribute)
94
+
95
+ message ||= "Expected #{policy.class} to permit editing :#{attribute} but it was not editable"
96
+ assert result, message
97
+ end
98
+
99
+ # Assert that a policy forbids editing a specific attribute
100
+ #
101
+ # @param policy [SimpleAuthorize::Policy] The policy instance to test
102
+ # @param attribute [Symbol, String] The attribute to check (e.g., :id, :created_at)
103
+ # @param message [String] Optional custom failure message
104
+ #
105
+ # @example
106
+ # assert_forbid_editing(policy, :published)
107
+ # assert_forbid_editing(policy, "id")
108
+ def assert_forbid_editing(policy, attribute, message = nil)
109
+ result = policy.attribute_editable?(attribute)
110
+
111
+ message ||= "Expected #{policy.class} to forbid editing :#{attribute} but it was editable"
112
+ assert_not result, message
113
+ end
114
+ end
115
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleAuthorize
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -5,24 +5,13 @@ require_relative "simple_authorize/version"
5
5
  require_relative "simple_authorize/configuration"
6
6
  require_relative "simple_authorize/controller"
7
7
  require_relative "simple_authorize/policy"
8
+ require_relative "simple_authorize/test_helpers"
8
9
 
10
+ # SimpleAuthorize provides a lightweight authorization framework for Rails applications
11
+ # without external dependencies. It offers policy-based access control inspired by Pundit.
9
12
  module SimpleAuthorize
10
13
  class Error < StandardError; end
11
-
12
- # Railtie for automatic integration with Rails
13
- class Railtie < Rails::Railtie
14
- initializer "simple_authorize.configure" do
15
- ActiveSupport.on_load(:action_controller) do
16
- # Make SimpleAuthorize::Controller available as Authorization for backwards compatibility
17
- ::Authorization = SimpleAuthorize::Controller unless defined?(::Authorization)
18
- # Make SimpleAuthorize::Policy available as ApplicationPolicy for backwards compatibility
19
- ::ApplicationPolicy = SimpleAuthorize::Policy unless defined?(::ApplicationPolicy)
20
- end
21
- end
22
-
23
- # Generate initializer template
24
- generators do
25
- require_relative "generators/simple_authorize/install/install_generator"
26
- end
27
- end
28
14
  end
15
+
16
+ # Only load Railtie if Rails is defined
17
+ require_relative "simple_authorize/railtie" if defined?(Rails::Railtie)
data/spec/examples.txt ADDED
@@ -0,0 +1,51 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------- | ------ | --------------- |
3
+ ./spec/rspec_matchers_spec.rb[1:1:1:1] | passed | 0.00004 seconds |
4
+ ./spec/rspec_matchers_spec.rb[1:1:1:2] | passed | 0.00004 seconds |
5
+ ./spec/rspec_matchers_spec.rb[1:1:1:3] | passed | 0.00004 seconds |
6
+ ./spec/rspec_matchers_spec.rb[1:1:2:1] | passed | 0.00004 seconds |
7
+ ./spec/rspec_matchers_spec.rb[1:1:2:2] | passed | 0.00004 seconds |
8
+ ./spec/rspec_matchers_spec.rb[1:1:3:1] | passed | 0.00005 seconds |
9
+ ./spec/rspec_matchers_spec.rb[1:1:3:2] | passed | 0.00004 seconds |
10
+ ./spec/rspec_matchers_spec.rb[1:1:4:1] | passed | 0.00004 seconds |
11
+ ./spec/rspec_matchers_spec.rb[1:1:4:2] | passed | 0.00004 seconds |
12
+ ./spec/rspec_matchers_spec.rb[1:2:1:1] | passed | 0.00004 seconds |
13
+ ./spec/rspec_matchers_spec.rb[1:2:1:2] | passed | 0.00004 seconds |
14
+ ./spec/rspec_matchers_spec.rb[1:2:2:1] | passed | 0.00005 seconds |
15
+ ./spec/rspec_matchers_spec.rb[1:2:2:2] | passed | 0.00011 seconds |
16
+ ./spec/rspec_matchers_spec.rb[1:2:3:1] | passed | 0.00005 seconds |
17
+ ./spec/rspec_matchers_spec.rb[1:2:3:2] | passed | 0.00007 seconds |
18
+ ./spec/rspec_matchers_spec.rb[1:2:4:1] | passed | 0.00003 seconds |
19
+ ./spec/rspec_matchers_spec.rb[1:2:4:2] | passed | 0.00004 seconds |
20
+ ./spec/rspec_matchers_spec.rb[1:3:1:1] | passed | 0.00004 seconds |
21
+ ./spec/rspec_matchers_spec.rb[1:3:1:2] | passed | 0.00004 seconds |
22
+ ./spec/rspec_matchers_spec.rb[1:3:1:3] | passed | 0.00004 seconds |
23
+ ./spec/rspec_matchers_spec.rb[1:3:2:1] | passed | 0.00004 seconds |
24
+ ./spec/rspec_matchers_spec.rb[1:3:3:1] | passed | 0.00005 seconds |
25
+ ./spec/rspec_matchers_spec.rb[1:3:3:2] | passed | 0.00004 seconds |
26
+ ./spec/rspec_matchers_spec.rb[1:3:4:1] | passed | 0.00003 seconds |
27
+ ./spec/rspec_matchers_spec.rb[1:3:4:2] | passed | 0.00004 seconds |
28
+ ./spec/rspec_matchers_spec.rb[1:4:1:1] | passed | 0.00004 seconds |
29
+ ./spec/rspec_matchers_spec.rb[1:4:2:1] | passed | 0.00004 seconds |
30
+ ./spec/rspec_matchers_spec.rb[1:4:2:2] | passed | 0.00004 seconds |
31
+ ./spec/rspec_matchers_spec.rb[1:4:3:1] | passed | 0.00005 seconds |
32
+ ./spec/rspec_matchers_spec.rb[1:4:4:1] | passed | 0.00003 seconds |
33
+ ./spec/rspec_matchers_spec.rb[1:4:4:2] | passed | 0.00004 seconds |
34
+ ./spec/rspec_matchers_spec.rb[1:5:1:1] | passed | 0.00004 seconds |
35
+ ./spec/rspec_matchers_spec.rb[1:5:1:2] | passed | 0.00004 seconds |
36
+ ./spec/rspec_matchers_spec.rb[1:5:2:1] | passed | 0.00004 seconds |
37
+ ./spec/rspec_matchers_spec.rb[1:5:3:1] | passed | 0.00005 seconds |
38
+ ./spec/rspec_matchers_spec.rb[1:5:3:2] | passed | 0.00004 seconds |
39
+ ./spec/rspec_matchers_spec.rb[1:5:4:1] | passed | 0.00004 seconds |
40
+ ./spec/rspec_matchers_spec.rb[1:5:4:2] | passed | 0.00004 seconds |
41
+ ./spec/rspec_matchers_spec.rb[1:6:1:1] | passed | 0.00004 seconds |
42
+ ./spec/rspec_matchers_spec.rb[1:6:2:1] | passed | 0.00004 seconds |
43
+ ./spec/rspec_matchers_spec.rb[1:6:2:2] | passed | 0.00004 seconds |
44
+ ./spec/rspec_matchers_spec.rb[1:6:3:1] | passed | 0.00004 seconds |
45
+ ./spec/rspec_matchers_spec.rb[1:6:3:2] | passed | 0.00005 seconds |
46
+ ./spec/rspec_matchers_spec.rb[1:6:4:1] | passed | 0.00003 seconds |
47
+ ./spec/rspec_matchers_spec.rb[1:6:4:2] | passed | 0.00003 seconds |
48
+ ./spec/rspec_matchers_spec.rb[1:7:1:1] | passed | 0.00004 seconds |
49
+ ./spec/rspec_matchers_spec.rb[1:7:1:2] | passed | 0.00005 seconds |
50
+ ./spec/rspec_matchers_spec.rb[1:7:1:3] | passed | 0.00003 seconds |
51
+ ./spec/rspec_matchers_spec.rb[1:7:2:1] | passed | 0.00156 seconds |
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe SimpleAuthorize::RSpecMatchers do
6
+ let(:admin) { User.new(id: 1, role: :admin) }
7
+ let(:contributor) { User.new(id: 2, role: :contributor) }
8
+ let(:viewer) { User.new(id: 3, role: :viewer) }
9
+ let(:post) { Post.new(id: 1, user_id: 2) }
10
+
11
+ describe "permit_action matcher" do
12
+ context "when action is permitted" do
13
+ subject { PostPolicy.new(admin, post) }
14
+
15
+ it { is_expected.to permit_action(:destroy) }
16
+ it { is_expected.to permit_action(:update) }
17
+ it { is_expected.to permit_action(:show) }
18
+ end
19
+
20
+ context "when action is forbidden" do
21
+ subject { PostPolicy.new(viewer, post) }
22
+
23
+ it { is_expected.not_to permit_action(:destroy) }
24
+ it { is_expected.not_to permit_action(:update) }
25
+ end
26
+
27
+ context "with string actions" do
28
+ subject { PostPolicy.new(admin, post) }
29
+
30
+ it { is_expected.to permit_action("show") }
31
+ it { is_expected.to permit_action("update") }
32
+ end
33
+
34
+ context "with explicit expect syntax" do
35
+ it "passes when action is permitted" do
36
+ policy = PostPolicy.new(admin, post)
37
+ expect(policy).to permit_action(:destroy)
38
+ end
39
+
40
+ it "fails when action is forbidden" do
41
+ policy = PostPolicy.new(viewer, post)
42
+ expect(policy).not_to permit_action(:destroy)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "forbid_action matcher" do
48
+ context "when action is forbidden" do
49
+ subject { PostPolicy.new(viewer, post) }
50
+
51
+ it { is_expected.to forbid_action(:destroy) }
52
+ it { is_expected.to forbid_action(:update) }
53
+ end
54
+
55
+ context "when action is permitted" do
56
+ subject { PostPolicy.new(admin, post) }
57
+
58
+ it { is_expected.not_to forbid_action(:show) }
59
+ it { is_expected.not_to forbid_action(:index) }
60
+ end
61
+
62
+ context "with string actions" do
63
+ subject { PostPolicy.new(viewer, post) }
64
+
65
+ it { is_expected.to forbid_action("destroy") }
66
+ it { is_expected.to forbid_action("update") }
67
+ end
68
+
69
+ context "with explicit expect syntax" do
70
+ it "passes when action is forbidden" do
71
+ policy = PostPolicy.new(viewer, post)
72
+ expect(policy).to forbid_action(:destroy)
73
+ end
74
+
75
+ it "fails when action is permitted" do
76
+ policy = PostPolicy.new(admin, post)
77
+ expect(policy).not_to forbid_action(:show)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "permit_viewing matcher" do
83
+ context "when attribute is visible" do
84
+ subject { PostPolicy.new(viewer, post) }
85
+
86
+ it { is_expected.to permit_viewing(:title) }
87
+ it { is_expected.to permit_viewing(:body) }
88
+ it { is_expected.to permit_viewing(:id) }
89
+ end
90
+
91
+ context "when attribute is hidden" do
92
+ subject { PostPolicy.new(viewer, post) }
93
+
94
+ it { is_expected.not_to permit_viewing(:user_id) }
95
+ end
96
+
97
+ context "with string attributes" do
98
+ subject { PostPolicy.new(admin, post) }
99
+
100
+ it { is_expected.to permit_viewing("title") }
101
+ it { is_expected.to permit_viewing("user_id") }
102
+ end
103
+
104
+ context "with explicit expect syntax" do
105
+ it "passes when attribute is visible" do
106
+ policy = PostPolicy.new(viewer, post)
107
+ expect(policy).to permit_viewing(:title)
108
+ end
109
+
110
+ it "fails when attribute is hidden" do
111
+ policy = PostPolicy.new(viewer, post)
112
+ expect(policy).not_to permit_viewing(:user_id)
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "forbid_viewing matcher" do
118
+ context "when attribute is hidden" do
119
+ subject { PostPolicy.new(viewer, post) }
120
+
121
+ it { is_expected.to forbid_viewing(:user_id) }
122
+ end
123
+
124
+ context "when attribute is visible" do
125
+ subject { PostPolicy.new(admin, post) }
126
+
127
+ it { is_expected.not_to forbid_viewing(:title) }
128
+ it { is_expected.not_to forbid_viewing(:user_id) }
129
+ end
130
+
131
+ context "with string attributes" do
132
+ subject { PostPolicy.new(viewer, post) }
133
+
134
+ it { is_expected.to forbid_viewing("user_id") }
135
+ end
136
+
137
+ context "with explicit expect syntax" do
138
+ it "passes when attribute is hidden" do
139
+ policy = PostPolicy.new(viewer, post)
140
+ expect(policy).to forbid_viewing(:user_id)
141
+ end
142
+
143
+ it "fails when attribute is visible" do
144
+ policy = PostPolicy.new(admin, post)
145
+ expect(policy).not_to forbid_viewing(:title)
146
+ end
147
+ end
148
+ end
149
+
150
+ describe "permit_editing matcher" do
151
+ context "when attribute is editable" do
152
+ subject { PostPolicy.new(contributor, post) }
153
+
154
+ it { is_expected.to permit_editing(:title) }
155
+ it { is_expected.to permit_editing(:body) }
156
+ end
157
+
158
+ context "when attribute is not editable" do
159
+ subject { PostPolicy.new(contributor, post) }
160
+
161
+ it { is_expected.not_to permit_editing(:published) }
162
+ end
163
+
164
+ context "with string attributes" do
165
+ subject { PostPolicy.new(admin, post) }
166
+
167
+ it { is_expected.to permit_editing("title") }
168
+ it { is_expected.to permit_editing("published") }
169
+ end
170
+
171
+ context "with explicit expect syntax" do
172
+ it "passes when attribute is editable" do
173
+ policy = PostPolicy.new(contributor, post)
174
+ expect(policy).to permit_editing(:title)
175
+ end
176
+
177
+ it "fails when attribute is not editable" do
178
+ policy = PostPolicy.new(contributor, post)
179
+ expect(policy).not_to permit_editing(:published)
180
+ end
181
+ end
182
+ end
183
+
184
+ describe "forbid_editing matcher" do
185
+ context "when attribute is not editable" do
186
+ subject { PostPolicy.new(contributor, post) }
187
+
188
+ it { is_expected.to forbid_editing(:published) }
189
+ end
190
+
191
+ context "when attribute is editable" do
192
+ subject { PostPolicy.new(admin, post) }
193
+
194
+ it { is_expected.not_to forbid_editing(:title) }
195
+ it { is_expected.not_to forbid_editing(:published) }
196
+ end
197
+
198
+ context "with string attributes" do
199
+ subject { PostPolicy.new(viewer, post) }
200
+
201
+ it { is_expected.to forbid_editing("title") }
202
+ it { is_expected.to forbid_editing("published") }
203
+ end
204
+
205
+ context "with explicit expect syntax" do
206
+ it "passes when attribute is not editable" do
207
+ policy = PostPolicy.new(contributor, post)
208
+ expect(policy).to forbid_editing(:published)
209
+ end
210
+
211
+ it "fails when attribute is editable" do
212
+ policy = PostPolicy.new(admin, post)
213
+ expect(policy).not_to forbid_editing(:title)
214
+ end
215
+ end
216
+ end
217
+
218
+ describe "edge cases" do
219
+ context "with nil user" do
220
+ subject { PostPolicy.new(nil, post) }
221
+
222
+ it { is_expected.to forbid_action(:update) }
223
+ it { is_expected.to forbid_viewing(:title) }
224
+ it { is_expected.to forbid_editing(:body) }
225
+ end
226
+
227
+ context "with missing policy methods" do
228
+ subject { PostPolicy.new(admin, post) }
229
+
230
+ it "raises NoMethodError for nonexistent actions" do
231
+ expect { subject.nonexistent_action? }.to raise_error(NoMethodError)
232
+ end
233
+ end
234
+ end
235
+ end