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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# State-as-Records Patterns
|
|
2
|
+
|
|
3
|
+
## Philosophy
|
|
4
|
+
|
|
5
|
+
Instead of boolean columns (`closed: true`), create separate state record models that capture who, when, and why.
|
|
6
|
+
|
|
7
|
+
## When to Use State Records vs Booleans
|
|
8
|
+
|
|
9
|
+
### Use State Records When:
|
|
10
|
+
- You need to track WHO changed the state
|
|
11
|
+
- You need to track WHEN the state changed
|
|
12
|
+
- You need to track WHY (reason, notes)
|
|
13
|
+
- State changes are business-significant events
|
|
14
|
+
- You need an audit trail
|
|
15
|
+
|
|
16
|
+
### Booleans Are OK When:
|
|
17
|
+
- It's a technical flag (`email_verified`, `terms_accepted`)
|
|
18
|
+
- No audit trail needed
|
|
19
|
+
- Simple on/off with no metadata
|
|
20
|
+
- Performance-critical hot paths
|
|
21
|
+
|
|
22
|
+
## Pattern 1: Simple Toggle (Closure)
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Migration
|
|
26
|
+
class CreateClosures < ActiveRecord::Migration[8.0]
|
|
27
|
+
def change
|
|
28
|
+
create_table :closures do |t|
|
|
29
|
+
t.references :card, null: false, foreign_key: true
|
|
30
|
+
t.references :user, foreign_key: true
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
add_index :closures, :card_id, unique: true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# app/models/closure.rb
|
|
38
|
+
class Closure < ApplicationRecord
|
|
39
|
+
belongs_to :card, touch: true
|
|
40
|
+
belongs_to :user, optional: true
|
|
41
|
+
validates :card, uniqueness: true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# app/models/concerns/closeable.rb
|
|
45
|
+
module Closeable
|
|
46
|
+
extend ActiveSupport::Concern
|
|
47
|
+
|
|
48
|
+
included do
|
|
49
|
+
has_one :closure, dependent: :destroy
|
|
50
|
+
|
|
51
|
+
scope :open, -> { where.missing(:closure) }
|
|
52
|
+
scope :closed, -> { joins(:closure) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def close(user: Current.user)
|
|
56
|
+
create_closure!(user: user)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reopen
|
|
60
|
+
closure&.destroy!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def closed?
|
|
64
|
+
closure.present?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def open?
|
|
68
|
+
!closed?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def closed_at
|
|
72
|
+
closure&.created_at
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def closed_by
|
|
76
|
+
closure&.user
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# app/models/card.rb
|
|
81
|
+
class Card < ApplicationRecord
|
|
82
|
+
include Closeable
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Pattern 2: State with Reason (Approval)
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class CreateApprovals < ActiveRecord::Migration[8.0]
|
|
90
|
+
def change
|
|
91
|
+
create_table :approvals do |t|
|
|
92
|
+
t.references :approvable, polymorphic: true, null: false
|
|
93
|
+
t.references :user, null: false, foreign_key: true
|
|
94
|
+
t.text :notes
|
|
95
|
+
t.timestamps
|
|
96
|
+
end
|
|
97
|
+
add_index :approvals, [:approvable_type, :approvable_id], unique: true
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class Approval < ApplicationRecord
|
|
102
|
+
belongs_to :approvable, polymorphic: true, touch: true
|
|
103
|
+
belongs_to :user
|
|
104
|
+
validates :approvable, uniqueness: { scope: :approvable_type }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module Approvable
|
|
108
|
+
extend ActiveSupport::Concern
|
|
109
|
+
|
|
110
|
+
included do
|
|
111
|
+
has_one :approval, as: :approvable, dependent: :destroy
|
|
112
|
+
|
|
113
|
+
scope :approved, -> { joins(:approval) }
|
|
114
|
+
scope :pending_approval, -> { where.missing(:approval) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def approve!(user:, notes: nil)
|
|
118
|
+
create_approval!(user: user, notes: notes)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def unapprove!
|
|
122
|
+
approval&.destroy!
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def approved?
|
|
126
|
+
approval.present?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def approved_by
|
|
130
|
+
approval&.user
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def approved_at
|
|
134
|
+
approval&.created_at
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Pattern 3: State with History (Publication)
|
|
140
|
+
|
|
141
|
+
When you need to track multiple state transitions over time:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class CreatePublications < ActiveRecord::Migration[8.0]
|
|
145
|
+
def change
|
|
146
|
+
create_table :publications do |t|
|
|
147
|
+
t.references :post, null: false, foreign_key: true
|
|
148
|
+
t.references :user, null: false, foreign_key: true
|
|
149
|
+
t.string :key, null: false
|
|
150
|
+
t.text :description
|
|
151
|
+
t.timestamps
|
|
152
|
+
end
|
|
153
|
+
add_index :publications, :post_id, unique: true
|
|
154
|
+
add_index :publications, :key, unique: true
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class Publication < ApplicationRecord
|
|
159
|
+
belongs_to :post, touch: true
|
|
160
|
+
belongs_to :user
|
|
161
|
+
|
|
162
|
+
before_validation :generate_key, on: :create
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def generate_key
|
|
167
|
+
self.key ||= SecureRandom.alphanumeric(12)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## CRUD Routing for State Records
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# config/routes.rb
|
|
176
|
+
resources :cards do
|
|
177
|
+
resource :closure, only: [:create, :destroy]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
resources :posts do
|
|
181
|
+
resource :publication, only: [:create, :destroy]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
resources :documents do
|
|
185
|
+
resource :approval, only: [:create, :destroy]
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# app/controllers/closures_controller.rb
|
|
191
|
+
class ClosuresController < ApplicationController
|
|
192
|
+
before_action :set_card
|
|
193
|
+
|
|
194
|
+
def create
|
|
195
|
+
authorize @card, :close?
|
|
196
|
+
@card.close(user: Current.user)
|
|
197
|
+
redirect_to @card, notice: "Closed."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def destroy
|
|
201
|
+
authorize @card, :reopen?
|
|
202
|
+
@card.reopen
|
|
203
|
+
redirect_to @card, notice: "Reopened."
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def set_card
|
|
209
|
+
@card = Card.find(params[:card_id])
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Testing State Records
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# test/models/concerns/closeable_test.rb
|
|
218
|
+
require "test_helper"
|
|
219
|
+
|
|
220
|
+
class CloseableTest < ActiveSupport::TestCase
|
|
221
|
+
setup do
|
|
222
|
+
@card = cards(:open_card)
|
|
223
|
+
@user = users(:one)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
test "#close creates a closure" do
|
|
227
|
+
assert_difference "Closure.count", 1 do
|
|
228
|
+
@card.close(user: @user)
|
|
229
|
+
end
|
|
230
|
+
assert @card.closed?
|
|
231
|
+
assert_equal @user, @card.closed_by
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
test "#reopen destroys the closure" do
|
|
235
|
+
@card.close(user: @user)
|
|
236
|
+
@card.reopen
|
|
237
|
+
assert @card.open?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
test ".open scope excludes closed cards" do
|
|
241
|
+
@card.close(user: @user)
|
|
242
|
+
assert_not_includes Card.open, @card
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
test ".closed scope includes closed cards" do
|
|
246
|
+
@card.close(user: @user)
|
|
247
|
+
assert_includes Card.closed, @card
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
```
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Testing Strategy by Layer
|
|
2
|
+
|
|
3
|
+
## Test Pyramid
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/\
|
|
7
|
+
/ \ System Tests (few)
|
|
8
|
+
/----\
|
|
9
|
+
/ \ Controller/Integration Tests (moderate)
|
|
10
|
+
/--------\
|
|
11
|
+
/ \ Unit Tests (many)
|
|
12
|
+
--------------
|
|
13
|
+
Models, Services, Queries, Presenters, Components
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Unit Tests
|
|
17
|
+
|
|
18
|
+
### Model Tests
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# test/models/event_test.rb
|
|
22
|
+
require "test_helper"
|
|
23
|
+
|
|
24
|
+
class EventTest < ActiveSupport::TestCase
|
|
25
|
+
test "requires name" do
|
|
26
|
+
event = Event.new(name: nil)
|
|
27
|
+
assert_not event.valid?
|
|
28
|
+
assert_includes event.errors[:name], "can't be blank"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test "requires event_date" do
|
|
32
|
+
event = Event.new(event_date: nil)
|
|
33
|
+
assert_not event.valid?
|
|
34
|
+
assert_includes event.errors[:event_date], "can't be blank"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
test ".upcoming returns only future events" do
|
|
38
|
+
past_event = events(:past)
|
|
39
|
+
future_event = events(:upcoming)
|
|
40
|
+
|
|
41
|
+
results = Event.upcoming
|
|
42
|
+
assert_includes results, future_event
|
|
43
|
+
assert_not_includes results, past_event
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "#days_until returns days until event" do
|
|
47
|
+
event = Event.new(event_date: 5.days.from_now.to_date)
|
|
48
|
+
assert_equal 5, event.days_until
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Service Tests
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# test/services/orders/create_service_test.rb
|
|
57
|
+
require "test_helper"
|
|
58
|
+
|
|
59
|
+
class Orders::CreateServiceTest < ActiveSupport::TestCase
|
|
60
|
+
setup do
|
|
61
|
+
@user = users(:one)
|
|
62
|
+
@product = products(:widget)
|
|
63
|
+
@service = Orders::CreateService.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test "returns success with valid params" do
|
|
67
|
+
result = @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
|
|
68
|
+
assert result.success?
|
|
69
|
+
assert_kind_of Order, result.data
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "creates an order" do
|
|
73
|
+
assert_difference "Order.count", 1 do
|
|
74
|
+
@service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
test "returns failure with empty items" do
|
|
79
|
+
result = @service.call(user: @user, items: [])
|
|
80
|
+
assert result.failure?
|
|
81
|
+
assert_equal :empty_cart, result.code
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test "does not create order on failure" do
|
|
85
|
+
assert_no_difference "Order.count" do
|
|
86
|
+
@service.call(user: @user, items: [])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Query Tests
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# test/queries/active_events_query_test.rb
|
|
96
|
+
require "test_helper"
|
|
97
|
+
|
|
98
|
+
class ActiveEventsQueryTest < ActiveSupport::TestCase
|
|
99
|
+
setup do
|
|
100
|
+
@account = accounts(:one)
|
|
101
|
+
@other_account = accounts(:two)
|
|
102
|
+
@query = ActiveEventsQuery.new(account: @account)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
test "returns active events for account" do
|
|
106
|
+
active = events(:active)
|
|
107
|
+
result = @query.call
|
|
108
|
+
assert_includes result, active
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
test "excludes inactive events" do
|
|
112
|
+
cancelled = events(:cancelled)
|
|
113
|
+
result = @query.call
|
|
114
|
+
assert_not_includes result, cancelled
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test "excludes other account events (tenant isolation)" do
|
|
118
|
+
other_event = events(:other_account_event)
|
|
119
|
+
result = @query.call
|
|
120
|
+
assert_not_includes result, other_event
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Presenter Tests
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# test/presenters/event_presenter_test.rb
|
|
129
|
+
require "test_helper"
|
|
130
|
+
|
|
131
|
+
class EventPresenterTest < ActiveSupport::TestCase
|
|
132
|
+
include ActionView::Helpers::TagHelper
|
|
133
|
+
|
|
134
|
+
test "delegates to model" do
|
|
135
|
+
event = events(:confirmed)
|
|
136
|
+
presenter = EventPresenter.new(event)
|
|
137
|
+
assert_equal event.name, presenter.name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
test "#status_badge returns HTML-safe string" do
|
|
141
|
+
presenter = EventPresenter.new(events(:confirmed))
|
|
142
|
+
assert_predicate presenter.status_badge, :html_safe?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
test "#status_badge includes status text" do
|
|
146
|
+
presenter = EventPresenter.new(events(:confirmed))
|
|
147
|
+
assert_match "Confirmed", presenter.status_badge
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test "#formatted_date with date present" do
|
|
151
|
+
event = events(:confirmed)
|
|
152
|
+
presenter = EventPresenter.new(event)
|
|
153
|
+
assert_match event.event_date.year.to_s, presenter.formatted_date
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
test "#formatted_date with nil date" do
|
|
157
|
+
event = events(:no_date)
|
|
158
|
+
presenter = EventPresenter.new(event)
|
|
159
|
+
assert_match "TBD", presenter.formatted_date
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Integration Tests
|
|
165
|
+
|
|
166
|
+
### Controller Tests
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# test/controllers/events_controller_test.rb
|
|
170
|
+
require "test_helper"
|
|
171
|
+
|
|
172
|
+
class EventsControllerTest < ActionDispatch::IntegrationTest
|
|
173
|
+
setup do
|
|
174
|
+
@user = users(:one)
|
|
175
|
+
@event = events(:one)
|
|
176
|
+
sign_in_as @user
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
test "should get index" do
|
|
180
|
+
get events_url
|
|
181
|
+
assert_response :success
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
test "shows only own account events" do
|
|
185
|
+
get events_url
|
|
186
|
+
assert_response :success
|
|
187
|
+
other_event = events(:other_account_event)
|
|
188
|
+
assert_no_match other_event.name, response.body
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
test "should create event" do
|
|
192
|
+
assert_difference("Event.count") do
|
|
193
|
+
post events_url, params: { event: { name: "New Event", event_date: 1.week.from_now } }
|
|
194
|
+
end
|
|
195
|
+
assert_redirected_to event_url(Event.last)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
test "renders form with errors for invalid params" do
|
|
199
|
+
post events_url, params: { event: { name: "" } }
|
|
200
|
+
assert_response :unprocessable_entity
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Policy Tests
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# test/policies/event_policy_test.rb
|
|
209
|
+
require "test_helper"
|
|
210
|
+
|
|
211
|
+
class EventPolicyTest < ActiveSupport::TestCase
|
|
212
|
+
test "owner can show" do
|
|
213
|
+
user = users(:one)
|
|
214
|
+
event = events(:one) # belongs to user's account
|
|
215
|
+
assert EventPolicy.new(user, event).show?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
test "non-owner cannot show" do
|
|
219
|
+
user = users(:two) # different account
|
|
220
|
+
event = events(:one)
|
|
221
|
+
assert_not EventPolicy.new(user, event).show?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
test "scope returns only own events" do
|
|
225
|
+
user = users(:one)
|
|
226
|
+
scope = EventPolicy::Scope.new(user, Event).resolve
|
|
227
|
+
assert_includes scope, events(:one)
|
|
228
|
+
assert_not_includes scope, events(:other_account_event)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## System Tests
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
# test/system/create_event_test.rb
|
|
237
|
+
require "application_system_test_case"
|
|
238
|
+
|
|
239
|
+
class CreateEventTest < ApplicationSystemTestCase
|
|
240
|
+
setup do
|
|
241
|
+
sign_in_as users(:one)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
test "creates event successfully" do
|
|
245
|
+
visit new_event_url
|
|
246
|
+
|
|
247
|
+
fill_in "Name", with: "Company Party"
|
|
248
|
+
fill_in "Event date", with: 1.month.from_now.to_date
|
|
249
|
+
click_button "Create Event"
|
|
250
|
+
|
|
251
|
+
assert_text "Event was successfully created"
|
|
252
|
+
assert_text "Company Party"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test "shows validation errors" do
|
|
256
|
+
visit new_event_url
|
|
257
|
+
click_button "Create Event"
|
|
258
|
+
assert_text "can't be blank"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Component Tests
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# test/components/event_card_component_test.rb
|
|
267
|
+
require "test_helper"
|
|
268
|
+
|
|
269
|
+
class EventCardComponentTest < ViewComponent::TestCase
|
|
270
|
+
test "renders event name" do
|
|
271
|
+
event = events(:one)
|
|
272
|
+
render_inline(EventCardComponent.new(event: event))
|
|
273
|
+
assert_text event.name
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
test "renders status badge" do
|
|
277
|
+
render_inline(EventCardComponent.new(event: events(:confirmed)))
|
|
278
|
+
assert_selector ".badge"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
test "shows days until for upcoming events" do
|
|
282
|
+
event = events(:upcoming)
|
|
283
|
+
render_inline(EventCardComponent.new(event: event))
|
|
284
|
+
assert_selector "[data-days-until]"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Test Helpers
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# test/test_helper.rb
|
|
293
|
+
class ActiveSupport::TestCase
|
|
294
|
+
fixtures :all
|
|
295
|
+
|
|
296
|
+
def sign_in_as(user)
|
|
297
|
+
post session_url, params: { email: user.email_address, password: "password" }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def sign_out
|
|
301
|
+
delete session_url
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Coverage Requirements
|
|
307
|
+
|
|
308
|
+
| Layer | Minimum Coverage |
|
|
309
|
+
|-------|-----------------|
|
|
310
|
+
| Models | 90% |
|
|
311
|
+
| Services | 95% |
|
|
312
|
+
| Queries | 90% |
|
|
313
|
+
| Controllers | 80% |
|
|
314
|
+
| Overall | 85% |
|
|
315
|
+
|
|
316
|
+
## Checklist
|
|
317
|
+
|
|
318
|
+
- [ ] Unit tests for all models (validations, scopes, methods)
|
|
319
|
+
- [ ] Service tests cover success/failure paths
|
|
320
|
+
- [ ] Query tests verify correctness and tenant isolation
|
|
321
|
+
- [ ] Controller tests for all endpoints
|
|
322
|
+
- [ ] Policy tests for authorization rules
|
|
323
|
+
- [ ] System tests for critical user flows
|
|
324
|
+
- [ ] Component tests for ViewComponents
|
|
325
|
+
- [ ] Fixtures with meaningful names
|
|
326
|
+
- [ ] Test helper with authentication methods
|