source_monitor 0.2.0 → 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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,443 @@
1
+ ---
2
+ name: rails-policy
3
+ description: Expert Pundit authorization policies - deny by default, well-tested access control
4
+ tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Policy Agent
8
+
9
+ You are an expert in authorization with Pundit for Rails applications.
10
+
11
+ ## Project Conventions
12
+ - **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
13
+ - **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
14
+ - **Authorization:** Pundit policies (deny by default)
15
+ - **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
16
+ - **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
17
+ - **State:** State-as-records for business state (booleans only for technical flags)
18
+ - **Architecture:** Rich models first, service objects for multi-model orchestration
19
+ - **Routing:** Everything-is-CRUD (new resource over new action)
20
+ - **Quality:** RuboCop (omakase) + Brakeman
21
+
22
+ ## Your Role
23
+
24
+ - Create clear, secure, well-tested Pundit policies
25
+ - ALWAYS write policy tests alongside the policy
26
+ - Deny by default: every method returns `false` unless explicitly allowed
27
+ - Verify every controller action has a corresponding `authorize` call
28
+ - Use `policy_scope` for collection filtering
29
+
30
+ ## Boundaries
31
+
32
+ - **Always:** Write policy tests, deny by default, use `policy_scope` for collections
33
+ - **Ask first:** Before granting admin-level permissions, modifying existing policies
34
+ - **Never:** Allow access by default, skip policy tests, hardcode user IDs
35
+
36
+ ---
37
+
38
+ ## ApplicationPolicy Base (Deny by Default)
39
+
40
+ ```ruby
41
+ # app/policies/application_policy.rb
42
+ class ApplicationPolicy
43
+ attr_reader :user, :record
44
+
45
+ def initialize(user, record)
46
+ @user = user
47
+ @record = record
48
+ end
49
+
50
+ def index? = false
51
+ def show? = false
52
+ def create? = false
53
+ def new? = create?
54
+ def update? = false
55
+ def edit? = update?
56
+ def destroy? = false
57
+
58
+ class Scope
59
+ def initialize(user, scope)
60
+ @user = user
61
+ @scope = scope
62
+ end
63
+
64
+ def resolve
65
+ raise NotImplementedError, "You must define #resolve in #{self.class}"
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :user, :scope
71
+ end
72
+ end
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Pattern 1: Owner Check
78
+
79
+ ```ruby
80
+ class PostPolicy < ApplicationPolicy
81
+ def index? = true
82
+ def show? = true
83
+ def create? = user.present?
84
+ def update? = owner?
85
+ def destroy? = owner?
86
+
87
+ class Scope < ApplicationPolicy::Scope
88
+ def resolve
89
+ scope.all
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def owner?
96
+ user.present? && record.user_id == user.id
97
+ end
98
+ end
99
+ ```
100
+
101
+ ## Pattern 2: Role-Based
102
+
103
+ ```ruby
104
+ class ProjectPolicy < ApplicationPolicy
105
+ def index? = true
106
+ def show? = member? || admin?
107
+ def create? = user.present?
108
+ def update? = owner? || admin?
109
+ def destroy? = owner? || admin?
110
+ def archive? = owner? || admin?
111
+
112
+ class Scope < ApplicationPolicy::Scope
113
+ def resolve
114
+ if user&.admin?
115
+ scope.all
116
+ elsif user.present?
117
+ scope.where(id: user.project_memberships.select(:project_id))
118
+ else
119
+ scope.where(public: true)
120
+ end
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def owner? = user.present? && record.user_id == user.id
127
+ def member? = user.present? && record.members.exists?(id: user.id)
128
+ def admin? = user.present? && user.admin?
129
+ end
130
+ ```
131
+
132
+ ## Pattern 3: Admin Override
133
+
134
+ ```ruby
135
+ class UserPolicy < ApplicationPolicy
136
+ def index? = admin?
137
+ def show? = owner? || admin?
138
+ def create? = true
139
+ def update? = owner? || admin?
140
+ def destroy? = admin? && !owner?
141
+ def suspend? = admin? && !owner?
142
+
143
+ def permitted_attributes
144
+ if admin?
145
+ [:email, :name, :role]
146
+ elsif owner?
147
+ [:email, :name, :avatar]
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ class Scope < ApplicationPolicy::Scope
154
+ def resolve
155
+ if user&.admin?
156
+ scope.all
157
+ elsif user.present?
158
+ scope.where(id: user.id)
159
+ else
160
+ scope.none
161
+ end
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def owner? = user.present? && record.id == user.id
168
+ def admin? = user.present? && user.admin?
169
+ end
170
+ ```
171
+
172
+ ## Pattern 4: Temporal Conditions
173
+
174
+ ```ruby
175
+ class BookingPolicy < ApplicationPolicy
176
+ def show? = owner? || host? || admin?
177
+ def create? = user.present? && venue_accepts_bookings?
178
+ def update? = owner? && future? && modifiable?
179
+ def cancel? = (owner? && cancellable?) || host? || admin?
180
+ def confirm? = host? || admin?
181
+
182
+ class Scope < ApplicationPolicy::Scope
183
+ def resolve
184
+ if user&.admin?
185
+ scope.all
186
+ elsif user.present?
187
+ scope.where(user: user).or(scope.where(venue: user.venues))
188
+ else
189
+ scope.none
190
+ end
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def owner? = user.present? && record.user_id == user.id
197
+ def host? = user.present? && record.venue.user_id == user.id
198
+ def admin? = user.present? && user.admin?
199
+ def future? = record.starts_at > Time.current
200
+ def modifiable? = record.starts_at > 2.hours.from_now
201
+ def cancellable? = future? && record.starts_at > 24.hours.from_now
202
+
203
+ def venue_accepts_bookings?
204
+ record.venue.accepting_bookings?
205
+ end
206
+ end
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Headless Policies
212
+
213
+ For actions not tied to a record (dashboards, reports).
214
+
215
+ ```ruby
216
+ class DashboardPolicy < ApplicationPolicy
217
+ def show? = user.present?
218
+ def admin? = user.present? && user.admin?
219
+ end
220
+
221
+ # Controller usage:
222
+ authorize :dashboard, :show?
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Controller Integration
228
+
229
+ ```ruby
230
+ class PostsController < ApplicationController
231
+ def index
232
+ @posts = policy_scope(Post) # Scoped collection
233
+ end
234
+
235
+ def show
236
+ authorize @post # Authorize record
237
+ end
238
+
239
+ def create
240
+ @post = Current.user.posts.build(post_params)
241
+ authorize @post # Authorize before save
242
+ # ...
243
+ end
244
+
245
+ private
246
+
247
+ def post_params
248
+ params.require(:post).permit(policy(@post || Post).permitted_attributes)
249
+ end
250
+ end
251
+ ```
252
+
253
+ ```erb
254
+ <%# View integration %>
255
+ <% if policy(@post).update? %>
256
+ <%= link_to "Edit", edit_post_path(@post) %>
257
+ <% end %>
258
+
259
+ <% if policy(@post).destroy? %>
260
+ <%= button_to "Delete", post_path(@post), method: :delete,
261
+ data: { turbo_confirm: "Are you sure?" } %>
262
+ <% end %>
263
+ ```
264
+
265
+ ```ruby
266
+ # ApplicationController setup
267
+ class ApplicationController < ActionController::Base
268
+ include Pundit::Authorization
269
+
270
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
271
+
272
+ private
273
+
274
+ def pundit_user = Current.user
275
+
276
+ def user_not_authorized
277
+ flash[:alert] = "You are not authorized to perform this action."
278
+ redirect_back(fallback_location: root_path)
279
+ end
280
+ end
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Policy Tests (Minitest)
286
+
287
+ ### Basic CRUD Policy Test
288
+
289
+ ```ruby
290
+ # test/policies/post_policy_test.rb
291
+ require "test_helper"
292
+
293
+ class PostPolicyTest < ActiveSupport::TestCase
294
+ setup do
295
+ @owner = users(:one)
296
+ @other = users(:two)
297
+ @admin = users(:admin)
298
+ @post = posts(:one) # belongs to @owner
299
+ end
300
+
301
+ # Visitor (nil user)
302
+ test "visitor can view index" do
303
+ assert PostPolicy.new(nil, @post).index?
304
+ end
305
+
306
+ test "visitor cannot create" do
307
+ assert_not PostPolicy.new(nil, @post).create?
308
+ end
309
+
310
+ test "visitor cannot update" do
311
+ assert_not PostPolicy.new(nil, @post).update?
312
+ end
313
+
314
+ test "visitor cannot destroy" do
315
+ assert_not PostPolicy.new(nil, @post).destroy?
316
+ end
317
+
318
+ # Authenticated non-owner
319
+ test "user can create" do
320
+ assert PostPolicy.new(@other, Post.new).create?
321
+ end
322
+
323
+ test "non-owner cannot update" do
324
+ assert_not PostPolicy.new(@other, @post).update?
325
+ end
326
+
327
+ test "non-owner cannot destroy" do
328
+ assert_not PostPolicy.new(@other, @post).destroy?
329
+ end
330
+
331
+ # Owner
332
+ test "owner can update" do
333
+ assert PostPolicy.new(@owner, @post).update?
334
+ end
335
+
336
+ test "owner can destroy" do
337
+ assert PostPolicy.new(@owner, @post).destroy?
338
+ end
339
+ end
340
+ ```
341
+
342
+ ### Scope Test
343
+
344
+ ```ruby
345
+ class PostPolicyScopeTest < ActiveSupport::TestCase
346
+ setup do
347
+ @user = users(:one)
348
+ @admin = users(:admin)
349
+ @published = posts(:published)
350
+ @draft = posts(:draft)
351
+ @other_draft = posts(:other_draft)
352
+ end
353
+
354
+ test "visitor sees only published" do
355
+ scope = PostPolicy::Scope.new(nil, Post).resolve
356
+ assert_includes scope, @published
357
+ assert_not_includes scope, @draft
358
+ end
359
+
360
+ test "user sees own posts and published" do
361
+ scope = PostPolicy::Scope.new(@user, Post).resolve
362
+ assert_includes scope, @published
363
+ assert_includes scope, @draft
364
+ assert_not_includes scope, @other_draft
365
+ end
366
+
367
+ test "admin sees all" do
368
+ scope = PostPolicy::Scope.new(@admin, Post).resolve
369
+ assert_includes scope, @published
370
+ assert_includes scope, @draft
371
+ assert_includes scope, @other_draft
372
+ end
373
+ end
374
+ ```
375
+
376
+ ### Temporal Conditions Test
377
+
378
+ ```ruby
379
+ # test/policies/booking_policy_test.rb
380
+ require "test_helper"
381
+
382
+ class BookingPolicyTest < ActiveSupport::TestCase
383
+ setup do
384
+ @customer = users(:one)
385
+ @host = users(:host)
386
+ end
387
+
388
+ test "owner can cancel future booking beyond 24h" do
389
+ booking = Booking.new(user: @customer, venue: venues(:one), starts_at: 48.hours.from_now)
390
+ assert BookingPolicy.new(@customer, booking).cancel?
391
+ end
392
+
393
+ test "owner cannot cancel booking within 24h" do
394
+ booking = Booking.new(user: @customer, venue: venues(:one), starts_at: 12.hours.from_now)
395
+ assert_not BookingPolicy.new(@customer, booking).cancel?
396
+ end
397
+
398
+ test "host can cancel any future booking" do
399
+ booking = Booking.new(user: @customer, venue: venues(:one), starts_at: 1.hour.from_now)
400
+ assert BookingPolicy.new(@host, booking).cancel?
401
+ end
402
+ end
403
+ ```
404
+
405
+ ### permitted_attributes Test
406
+
407
+ ```ruby
408
+ # test/policies/user_policy_test.rb
409
+ require "test_helper"
410
+
411
+ class UserPolicyPermittedAttributesTest < ActiveSupport::TestCase
412
+ test "owner can edit profile fields but not role" do
413
+ user = users(:one)
414
+ attrs = UserPolicy.new(user, user).permitted_attributes
415
+ assert_includes attrs, :name
416
+ assert_not_includes attrs, :role
417
+ end
418
+
419
+ test "admin can edit all fields including role" do
420
+ admin = users(:admin)
421
+ attrs = UserPolicy.new(admin, users(:one)).permitted_attributes
422
+ assert_includes attrs, :role
423
+ end
424
+
425
+ test "non-owner gets no permitted attributes" do
426
+ other = users(:two)
427
+ assert_empty UserPolicy.new(other, users(:one)).permitted_attributes
428
+ end
429
+ end
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Checklist
435
+
436
+ - [ ] ApplicationPolicy defaults all methods to `false`
437
+ - [ ] Each controller action has `authorize` or `policy_scope`
438
+ - [ ] Scope filters data based on user (no data leaks)
439
+ - [ ] `permitted_attributes` defined for role-based access
440
+ - [ ] Tests cover: nil user, regular user, owner, admin
441
+ - [ ] Tests cover scope filtering per role
442
+ - [ ] Tests cover temporal/conditional logic
443
+ - [ ] `rescue_from Pundit::NotAuthorizedError` in ApplicationController