plutonium 0.46.0 → 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 +4 -0
- data/.claude/skills/plutonium-interaction/SKILL.md +23 -0
- data/.claude/skills/plutonium-nested-resources/SKILL.md +10 -0
- data/.claude/skills/plutonium-testing/SKILL.md +268 -0
- data/.yarnrc.yml +1 -0
- data/CHANGELOG.md +10 -0
- data/app/assets/plutonium.css +1 -1
- 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-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-14-plutonium-testing-design.md +364 -0
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- 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/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/version.rb +1 -1
- data/lib/plutonium.rb +2 -0
- data/package.json +1 -1
- data/yarn.lock +6037 -3893
- metadata +22 -2
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
# Plutonium::Testing Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Ship `Plutonium::Testing` — opt-in Minitest concerns and Rails generators that give Plutonium app developers default test coverage for resources, policies, definitions, interactions, models, nested scoping, portal access, and authentication.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Concerns live under `lib/plutonium/testing/` and are loaded via `require "plutonium/testing"` (no autoload, no production cost). A shared DSL (`resource_tests_for ResourceClass, portal: :admin`) drives portal-aware test generation. Test data is supplied via stub methods that raise `NotImplementedError` until the caller overrides them. One test file per (resource × portal) pairing, scaffolded by `pu:test:scaffold`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby, Rails, Minitest, ActiveSupport::Concern, Plutonium core (Portal::Engine, Resource::Definition, Auth::Rodauth), Thor (generators).
|
|
10
|
+
|
|
11
|
+
**User Verification:** NO — no user verification required. All acceptance is via automated tests.
|
|
12
|
+
|
|
13
|
+
**Spec:** `docs/superpowers/specs/2026-04-14-plutonium-testing-design.md`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
lib/plutonium/
|
|
21
|
+
testing.rb # entry point — requires all submodules
|
|
22
|
+
testing/
|
|
23
|
+
dsl.rb # resource_tests_for + portal resolution
|
|
24
|
+
auth_helpers.rb # login_as / sign_out / with_portal
|
|
25
|
+
resource_crud.rb # CRUD integration tests
|
|
26
|
+
resource_policy.rb # policy matrix + relation_scope
|
|
27
|
+
resource_definition.rb # definition smoke tests
|
|
28
|
+
resource_interaction.rb # interaction outcome assertions
|
|
29
|
+
resource_model.rb # associated_with / SGID / has_cents
|
|
30
|
+
nested_resource.rb # tenant-scoped CRUD + boundary
|
|
31
|
+
portal_access.rb # cross-portal access boundaries
|
|
32
|
+
|
|
33
|
+
lib/generators/pu/test/
|
|
34
|
+
install/install_generator.rb
|
|
35
|
+
install/templates/plutonium_testing.rb.tt
|
|
36
|
+
scaffold/scaffold_generator.rb
|
|
37
|
+
scaffold/templates/integration_test.rb.tt
|
|
38
|
+
scaffold/templates/policy_test.rb.tt
|
|
39
|
+
scaffold/templates/definition_test.rb.tt
|
|
40
|
+
|
|
41
|
+
test/plutonium/testing/ # tests for the testing module itself
|
|
42
|
+
dsl_test.rb
|
|
43
|
+
auth_helpers_test.rb
|
|
44
|
+
resource_crud_test.rb
|
|
45
|
+
resource_policy_test.rb
|
|
46
|
+
resource_definition_test.rb
|
|
47
|
+
resource_interaction_test.rb
|
|
48
|
+
resource_model_test.rb
|
|
49
|
+
nested_resource_test.rb
|
|
50
|
+
portal_access_test.rb
|
|
51
|
+
|
|
52
|
+
test/generators/pu/test/
|
|
53
|
+
install_generator_test.rb
|
|
54
|
+
scaffold_generator_test.rb
|
|
55
|
+
|
|
56
|
+
.claude/skills/plutonium-testing/SKILL.md
|
|
57
|
+
.claude/skills/plutonium/SKILL.md # router gets new entry
|
|
58
|
+
|
|
59
|
+
docs/guides/testing.md
|
|
60
|
+
docs/.vitepress/config.ts # sidebar nav
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Task 1: Module skeleton + entry point
|
|
66
|
+
|
|
67
|
+
**Goal:** Establish `lib/plutonium/testing.rb` and empty submodule files so subsequent tasks can drop code in.
|
|
68
|
+
|
|
69
|
+
**Files:**
|
|
70
|
+
- Create: `lib/plutonium/testing.rb`
|
|
71
|
+
- Create: `lib/plutonium/testing/dsl.rb`
|
|
72
|
+
- Create: `lib/plutonium/testing/auth_helpers.rb`
|
|
73
|
+
- Create: `lib/plutonium/testing/resource_crud.rb`
|
|
74
|
+
- Create: `lib/plutonium/testing/resource_policy.rb`
|
|
75
|
+
- Create: `lib/plutonium/testing/resource_definition.rb`
|
|
76
|
+
- Create: `lib/plutonium/testing/resource_interaction.rb`
|
|
77
|
+
- Create: `lib/plutonium/testing/resource_model.rb`
|
|
78
|
+
- Create: `lib/plutonium/testing/nested_resource.rb`
|
|
79
|
+
- Create: `lib/plutonium/testing/portal_access.rb`
|
|
80
|
+
- Test: `test/plutonium/testing/loadable_test.rb`
|
|
81
|
+
|
|
82
|
+
**Acceptance Criteria:**
|
|
83
|
+
- [ ] `require "plutonium/testing"` succeeds from a clean Ruby process
|
|
84
|
+
- [ ] `Plutonium::Testing` namespace defined
|
|
85
|
+
- [ ] All submodule constants resolve (even if empty)
|
|
86
|
+
- [ ] Not loaded by default — adding `require "plutonium/testing"` is opt-in
|
|
87
|
+
|
|
88
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/loadable_test.rb -v` → all tests pass.
|
|
89
|
+
|
|
90
|
+
**Steps:**
|
|
91
|
+
|
|
92
|
+
- [ ] **Step 1: Write the failing test**
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# test/plutonium/testing/loadable_test.rb
|
|
96
|
+
require "test_helper"
|
|
97
|
+
require "plutonium/testing"
|
|
98
|
+
|
|
99
|
+
class Plutonium::Testing::LoadableTest < ActiveSupport::TestCase
|
|
100
|
+
test "namespace is defined" do
|
|
101
|
+
assert defined?(Plutonium::Testing)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
test "all submodules are defined" do
|
|
105
|
+
%w[DSL AuthHelpers ResourceCrud ResourcePolicy ResourceDefinition
|
|
106
|
+
ResourceInteraction ResourceModel NestedResource PortalAccess].each do |name|
|
|
107
|
+
assert Plutonium::Testing.const_defined?(name), "#{name} not defined"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
114
|
+
|
|
115
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/loadable_test.rb -v`
|
|
116
|
+
Expected: FAIL — `cannot load such file -- plutonium/testing`
|
|
117
|
+
|
|
118
|
+
- [ ] **Step 3: Create entry point**
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# lib/plutonium/testing.rb
|
|
122
|
+
# frozen_string_literal: true
|
|
123
|
+
|
|
124
|
+
require "plutonium/testing/dsl"
|
|
125
|
+
require "plutonium/testing/auth_helpers"
|
|
126
|
+
require "plutonium/testing/resource_crud"
|
|
127
|
+
require "plutonium/testing/resource_policy"
|
|
128
|
+
require "plutonium/testing/resource_definition"
|
|
129
|
+
require "plutonium/testing/resource_interaction"
|
|
130
|
+
require "plutonium/testing/resource_model"
|
|
131
|
+
require "plutonium/testing/nested_resource"
|
|
132
|
+
require "plutonium/testing/portal_access"
|
|
133
|
+
|
|
134
|
+
module Plutonium
|
|
135
|
+
module Testing
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- [ ] **Step 4: Create empty submodule stubs**
|
|
141
|
+
|
|
142
|
+
For each submodule (dsl, auth_helpers, resource_crud, resource_policy, resource_definition, resource_interaction, resource_model, nested_resource, portal_access), create a file like:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# lib/plutonium/testing/dsl.rb
|
|
146
|
+
# frozen_string_literal: true
|
|
147
|
+
|
|
148
|
+
module Plutonium
|
|
149
|
+
module Testing
|
|
150
|
+
module DSL
|
|
151
|
+
extend ActiveSupport::Concern
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use the matching constant name for each file (`DSL`, `AuthHelpers`, `ResourceCrud`, etc.).
|
|
158
|
+
|
|
159
|
+
- [ ] **Step 5: Run test to verify it passes**
|
|
160
|
+
|
|
161
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/loadable_test.rb -v`
|
|
162
|
+
Expected: PASS — 2 runs, 0 failures.
|
|
163
|
+
|
|
164
|
+
- [ ] **Step 6: Commit**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
git add lib/plutonium/testing.rb lib/plutonium/testing/ test/plutonium/testing/loadable_test.rb
|
|
168
|
+
git commit -m "feat(testing): scaffold Plutonium::Testing module skeleton"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Task 2: Shared DSL + portal resolution
|
|
174
|
+
|
|
175
|
+
**Goal:** `Plutonium::Testing::DSL` provides `resource_tests_for` (class method) that captures config and resolves portal symbol → path prefix, default sign-in helper key, and parent association.
|
|
176
|
+
|
|
177
|
+
**Files:**
|
|
178
|
+
- Modify: `lib/plutonium/testing/dsl.rb`
|
|
179
|
+
- Test: `test/plutonium/testing/dsl_test.rb`
|
|
180
|
+
|
|
181
|
+
**Acceptance Criteria:**
|
|
182
|
+
- [ ] `resource_tests_for Klass, portal: :admin` stores config accessible via class-level reader
|
|
183
|
+
- [ ] Portal symbol resolves to a mounted engine's path prefix using Rails routes
|
|
184
|
+
- [ ] Explicit `path_prefix:` overrides portal resolution
|
|
185
|
+
- [ ] `parent:` / `actions:` / `skip:` keywords stored
|
|
186
|
+
- [ ] Raises `Plutonium::Testing::DSL::PortalNotFound` with a clear message when portal can't be resolved
|
|
187
|
+
- [ ] Instance-level `current_portal` reader returns the symbol from DSL
|
|
188
|
+
|
|
189
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/dsl_test.rb -v` → all tests pass.
|
|
190
|
+
|
|
191
|
+
**Steps:**
|
|
192
|
+
|
|
193
|
+
- [ ] **Step 1: Write the failing test**
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# test/plutonium/testing/dsl_test.rb
|
|
197
|
+
require "test_helper"
|
|
198
|
+
require "plutonium/testing"
|
|
199
|
+
|
|
200
|
+
class Plutonium::Testing::DSLTest < ActiveSupport::TestCase
|
|
201
|
+
class FakeTest < ActiveSupport::TestCase
|
|
202
|
+
include Plutonium::Testing::DSL
|
|
203
|
+
resource_tests_for Blogging::Post,
|
|
204
|
+
portal: :admin,
|
|
205
|
+
parent: :organization,
|
|
206
|
+
actions: %i[index show],
|
|
207
|
+
skip: %i[show]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
test "stores resource class" do
|
|
211
|
+
assert_equal Blogging::Post, FakeTest.resource_tests_config.fetch(:resource)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
test "stores portal symbol" do
|
|
215
|
+
assert_equal :admin, FakeTest.resource_tests_config.fetch(:portal)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
test "resolves path prefix from portal" do
|
|
219
|
+
assert_equal "/admin", FakeTest.resource_tests_config.fetch(:path_prefix)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
test "stores parent / actions / skip" do
|
|
223
|
+
cfg = FakeTest.resource_tests_config
|
|
224
|
+
assert_equal :organization, cfg.fetch(:parent)
|
|
225
|
+
assert_equal %i[index show], cfg.fetch(:actions)
|
|
226
|
+
assert_equal %i[show], cfg.fetch(:skip)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
test "explicit path_prefix overrides portal resolution" do
|
|
230
|
+
klass = Class.new(ActiveSupport::TestCase) do
|
|
231
|
+
include Plutonium::Testing::DSL
|
|
232
|
+
resource_tests_for Blogging::Post, portal: :admin, path_prefix: "/custom"
|
|
233
|
+
end
|
|
234
|
+
assert_equal "/custom", klass.resource_tests_config.fetch(:path_prefix)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
test "raises when portal cannot be resolved" do
|
|
238
|
+
err = assert_raises(Plutonium::Testing::DSL::PortalNotFound) do
|
|
239
|
+
Class.new(ActiveSupport::TestCase) do
|
|
240
|
+
include Plutonium::Testing::DSL
|
|
241
|
+
resource_tests_for Blogging::Post, portal: :nonexistent
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
assert_match(/nonexistent/, err.message)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
test "instance current_portal returns symbol" do
|
|
248
|
+
instance = FakeTest.new(:noop)
|
|
249
|
+
assert_equal :admin, instance.current_portal
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
255
|
+
|
|
256
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/dsl_test.rb -v`
|
|
257
|
+
Expected: FAIL — `resource_tests_for` undefined.
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 3: Implement DSL**
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# lib/plutonium/testing/dsl.rb
|
|
263
|
+
# frozen_string_literal: true
|
|
264
|
+
|
|
265
|
+
module Plutonium
|
|
266
|
+
module Testing
|
|
267
|
+
module DSL
|
|
268
|
+
extend ActiveSupport::Concern
|
|
269
|
+
|
|
270
|
+
class PortalNotFound < StandardError; end
|
|
271
|
+
|
|
272
|
+
DEFAULT_ACTIONS = %i[index show new create edit update destroy].freeze
|
|
273
|
+
|
|
274
|
+
class_methods do
|
|
275
|
+
def resource_tests_for(resource_class, portal:, path_prefix: nil, parent: nil,
|
|
276
|
+
actions: DEFAULT_ACTIONS, skip: [])
|
|
277
|
+
@resource_tests_config = {
|
|
278
|
+
resource: resource_class,
|
|
279
|
+
portal: portal,
|
|
280
|
+
path_prefix: path_prefix || resolve_portal_path_prefix(portal),
|
|
281
|
+
parent: parent,
|
|
282
|
+
actions: actions,
|
|
283
|
+
skip: skip
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def resource_tests_config
|
|
288
|
+
@resource_tests_config or raise "resource_tests_for not called on #{name}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def resolve_portal_path_prefix(portal_sym)
|
|
294
|
+
engine_const = "#{portal_sym.to_s.camelize}Portal::Engine".safe_constantize
|
|
295
|
+
raise PortalNotFound, "Could not resolve portal :#{portal_sym} (looked for #{portal_sym.to_s.camelize}Portal::Engine)" unless engine_const
|
|
296
|
+
|
|
297
|
+
mount = Rails.application.routes.routes.find { |r| r.app.app == engine_const }
|
|
298
|
+
raise PortalNotFound, "Engine #{engine_const} is not mounted in routes" unless mount
|
|
299
|
+
|
|
300
|
+
mount.path.spec.to_s.sub(/\(\.:format\)\z/, "").chomp("/")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def current_portal
|
|
305
|
+
self.class.resource_tests_config.fetch(:portal)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def current_path_prefix
|
|
309
|
+
self.class.resource_tests_config.fetch(:path_prefix)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
317
|
+
|
|
318
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/dsl_test.rb -v`
|
|
319
|
+
Expected: PASS — 7 runs, 0 failures.
|
|
320
|
+
|
|
321
|
+
- [ ] **Step 5: Commit**
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
git add lib/plutonium/testing/dsl.rb test/plutonium/testing/dsl_test.rb
|
|
325
|
+
git commit -m "feat(testing): add DSL with portal resolution"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Task 3: AuthHelpers (portal-scoped)
|
|
331
|
+
|
|
332
|
+
**Goal:** `login_as`, `sign_out`, `current_account`, `with_portal` — default portal from DSL, override via `portal:` kwarg. Stock implementation hits the portal's Rodauth login endpoint; non-Rodauth apps override `sign_in_for_tests`.
|
|
333
|
+
|
|
334
|
+
**Files:**
|
|
335
|
+
- Modify: `lib/plutonium/testing/auth_helpers.rb`
|
|
336
|
+
- Test: `test/plutonium/testing/auth_helpers_test.rb`
|
|
337
|
+
|
|
338
|
+
**Acceptance Criteria:**
|
|
339
|
+
- [ ] `login_as(account)` uses portal from DSL config
|
|
340
|
+
- [ ] `login_as(account, portal: :admin)` overrides
|
|
341
|
+
- [ ] `sign_out` and `sign_out(portal:)` mirror login_as
|
|
342
|
+
- [ ] `with_portal(:org) { ... }` temporarily switches portal default for the block
|
|
343
|
+
- [ ] Calls `sign_in_for_tests(account, portal:)` if defined; otherwise falls back to default Rodauth POST flow
|
|
344
|
+
- [ ] Default Rodauth flow: `post "#{portal_login_path}", params: {email:, password:}` then follows redirect
|
|
345
|
+
|
|
346
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/auth_helpers_test.rb -v` → all tests pass.
|
|
347
|
+
|
|
348
|
+
**Steps:**
|
|
349
|
+
|
|
350
|
+
- [ ] **Step 1: Write the failing test**
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# test/plutonium/testing/auth_helpers_test.rb
|
|
354
|
+
require "test_helper"
|
|
355
|
+
require "plutonium/testing"
|
|
356
|
+
|
|
357
|
+
class Plutonium::Testing::AuthHelpersTest < ActionDispatch::IntegrationTest
|
|
358
|
+
include Plutonium::Testing::DSL
|
|
359
|
+
include Plutonium::Testing::AuthHelpers
|
|
360
|
+
include DataHelpers
|
|
361
|
+
|
|
362
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
363
|
+
|
|
364
|
+
setup do
|
|
365
|
+
@admin = create_admin!
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
teardown do
|
|
369
|
+
Admin.delete_all
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
test "login_as uses default portal from DSL" do
|
|
373
|
+
login_as(@admin)
|
|
374
|
+
get "/admin"
|
|
375
|
+
assert_response :success
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
test "login_as with explicit portal kwarg" do
|
|
379
|
+
user = create_user!
|
|
380
|
+
login_as(user, portal: :user)
|
|
381
|
+
User.delete_all
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
test "with_portal switches default for block scope" do
|
|
385
|
+
with_portal(:user) do
|
|
386
|
+
assert_equal :user, current_portal
|
|
387
|
+
end
|
|
388
|
+
assert_equal :admin, current_portal
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
test "delegates to sign_in_for_tests when defined" do
|
|
392
|
+
called = nil
|
|
393
|
+
define_singleton_method(:sign_in_for_tests) { |account, portal:| called = [account, portal] }
|
|
394
|
+
login_as(@admin)
|
|
395
|
+
assert_equal [@admin, :admin], called
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
401
|
+
|
|
402
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/auth_helpers_test.rb -v`
|
|
403
|
+
Expected: FAIL — `login_as` undefined.
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 3: Implement AuthHelpers**
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
# lib/plutonium/testing/auth_helpers.rb
|
|
409
|
+
# frozen_string_literal: true
|
|
410
|
+
|
|
411
|
+
module Plutonium
|
|
412
|
+
module Testing
|
|
413
|
+
module AuthHelpers
|
|
414
|
+
extend ActiveSupport::Concern
|
|
415
|
+
|
|
416
|
+
def login_as(account, portal: nil)
|
|
417
|
+
portal ||= current_portal
|
|
418
|
+
if respond_to?(:sign_in_for_tests)
|
|
419
|
+
sign_in_for_tests(account, portal: portal)
|
|
420
|
+
else
|
|
421
|
+
default_rodauth_login(account, portal: portal)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def sign_out(portal: nil)
|
|
426
|
+
portal ||= current_portal
|
|
427
|
+
post logout_path_for(portal)
|
|
428
|
+
follow_redirect! if response.redirect?
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def current_account(portal: nil)
|
|
432
|
+
portal ||= current_portal
|
|
433
|
+
instance_variable_get(:"@__current_account_#{portal}")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def with_portal(portal)
|
|
437
|
+
prev = @__portal_override
|
|
438
|
+
@__portal_override = portal
|
|
439
|
+
yield
|
|
440
|
+
ensure
|
|
441
|
+
@__portal_override = prev
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def current_portal
|
|
445
|
+
@__portal_override || self.class.resource_tests_config.fetch(:portal)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
private
|
|
449
|
+
|
|
450
|
+
def default_rodauth_login(account, portal:)
|
|
451
|
+
post login_path_for(portal), params: {email: account.email, password: "password123"}
|
|
452
|
+
follow_redirect! if response.redirect?
|
|
453
|
+
instance_variable_set(:"@__current_account_#{portal}", account)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Convention: account model name pluralized → /<accounts>/login.
|
|
457
|
+
# For :admin portal → /admins/login. For :user portal → /users/login.
|
|
458
|
+
def login_path_for(portal)
|
|
459
|
+
"/#{account_table_for(portal)}/login"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def logout_path_for(portal)
|
|
463
|
+
"/#{account_table_for(portal)}/logout"
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def account_table_for(portal)
|
|
467
|
+
# Override hook if account-table mapping diverges from portal symbol.
|
|
468
|
+
case portal
|
|
469
|
+
when :admin then "admins"
|
|
470
|
+
when :user, :org then "users"
|
|
471
|
+
else portal.to_s.pluralize
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
480
|
+
|
|
481
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/auth_helpers_test.rb -v`
|
|
482
|
+
Expected: PASS — 4 runs, 0 failures.
|
|
483
|
+
|
|
484
|
+
- [ ] **Step 5: Commit**
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
git add lib/plutonium/testing/auth_helpers.rb test/plutonium/testing/auth_helpers_test.rb
|
|
488
|
+
git commit -m "feat(testing): add portal-scoped AuthHelpers"
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Task 4: ResourceCrud concern
|
|
494
|
+
|
|
495
|
+
**Goal:** Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource. Test data via stub methods (`create_resource!`, `valid_create_params`, `valid_update_params`).
|
|
496
|
+
|
|
497
|
+
**Files:**
|
|
498
|
+
- Modify: `lib/plutonium/testing/resource_crud.rb`
|
|
499
|
+
- Test: `test/plutonium/testing/resource_crud_test.rb`
|
|
500
|
+
|
|
501
|
+
**Acceptance Criteria:**
|
|
502
|
+
- [ ] One `test "..."` block per action in `actions:` list, minus `skip:`
|
|
503
|
+
- [ ] Stubs raise `NotImplementedError` with the stub name when not overridden
|
|
504
|
+
- [ ] Tests run against the dummy app's `Blogging::Post` for the admin portal and pass
|
|
505
|
+
- [ ] Resource path inferred from class name (`Blogging::Post` → `blogging/posts`)
|
|
506
|
+
|
|
507
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_crud_test.rb -v` → all tests pass.
|
|
508
|
+
|
|
509
|
+
**Steps:**
|
|
510
|
+
|
|
511
|
+
- [ ] **Step 1: Write the failing test**
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
# test/plutonium/testing/resource_crud_test.rb
|
|
515
|
+
require "test_helper"
|
|
516
|
+
require "plutonium/testing"
|
|
517
|
+
|
|
518
|
+
class Plutonium::Testing::ResourceCrudTest < ActionDispatch::IntegrationTest
|
|
519
|
+
include IntegrationTestHelper
|
|
520
|
+
include Plutonium::Testing::ResourceCrud
|
|
521
|
+
|
|
522
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
523
|
+
|
|
524
|
+
setup do
|
|
525
|
+
@admin = create_admin!
|
|
526
|
+
@org = create_organization!
|
|
527
|
+
@user = create_user!
|
|
528
|
+
login_as(@admin)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def create_resource!
|
|
532
|
+
create_post!
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def valid_create_params
|
|
536
|
+
{title: "New", body: "Body", status: :draft, user: @user.to_sgid.to_s, organization: @org.to_sgid.to_s}
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def valid_update_params
|
|
540
|
+
{title: "Updated"}
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Also write a stubs-required test:
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
# test/plutonium/testing/resource_crud_stubs_test.rb
|
|
549
|
+
require "test_helper"
|
|
550
|
+
require "plutonium/testing"
|
|
551
|
+
|
|
552
|
+
class Plutonium::Testing::ResourceCrudStubsTest < ActiveSupport::TestCase
|
|
553
|
+
test "create_resource! raises when unimplemented" do
|
|
554
|
+
klass = Class.new do
|
|
555
|
+
include Plutonium::Testing::ResourceCrud
|
|
556
|
+
end
|
|
557
|
+
err = assert_raises(NotImplementedError) { klass.new.create_resource! }
|
|
558
|
+
assert_match(/create_resource!/, err.message)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
564
|
+
|
|
565
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_crud_test.rb -v`
|
|
566
|
+
Expected: FAIL — no test methods generated.
|
|
567
|
+
|
|
568
|
+
- [ ] **Step 3: Implement ResourceCrud**
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
# lib/plutonium/testing/resource_crud.rb
|
|
572
|
+
# frozen_string_literal: true
|
|
573
|
+
|
|
574
|
+
module Plutonium
|
|
575
|
+
module Testing
|
|
576
|
+
module ResourceCrud
|
|
577
|
+
extend ActiveSupport::Concern
|
|
578
|
+
include Plutonium::Testing::DSL
|
|
579
|
+
include Plutonium::Testing::AuthHelpers
|
|
580
|
+
|
|
581
|
+
included do
|
|
582
|
+
cattr_accessor :__crud_installed, default: false
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
class_methods do
|
|
586
|
+
def resource_tests_for(*args, **kwargs)
|
|
587
|
+
super
|
|
588
|
+
install_crud_tests! unless __crud_installed
|
|
589
|
+
self.__crud_installed = true
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def install_crud_tests!
|
|
593
|
+
define_crud_test :index do
|
|
594
|
+
create_resource!
|
|
595
|
+
get "#{current_path_prefix}/#{resource_path}"
|
|
596
|
+
assert_response :success
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
define_crud_test :show do
|
|
600
|
+
record = create_resource!
|
|
601
|
+
get "#{current_path_prefix}/#{resource_path}/#{record.id}"
|
|
602
|
+
assert_response :success
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
define_crud_test :new do
|
|
606
|
+
get "#{current_path_prefix}/#{resource_path}/new"
|
|
607
|
+
assert_response :success
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
define_crud_test :create do
|
|
611
|
+
assert_difference -> { resource_class.count }, 1 do
|
|
612
|
+
post "#{current_path_prefix}/#{resource_path}", params: {param_key => valid_create_params}
|
|
613
|
+
end
|
|
614
|
+
assert_response :redirect
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
define_crud_test :edit do
|
|
618
|
+
record = create_resource!
|
|
619
|
+
get "#{current_path_prefix}/#{resource_path}/#{record.id}/edit"
|
|
620
|
+
assert_response :success
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
define_crud_test :update do
|
|
624
|
+
record = create_resource!
|
|
625
|
+
patch "#{current_path_prefix}/#{resource_path}/#{record.id}", params: {param_key => valid_update_params}
|
|
626
|
+
assert_response :redirect
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
define_crud_test :destroy do
|
|
630
|
+
record = create_resource!
|
|
631
|
+
assert_difference -> { resource_class.count }, -1 do
|
|
632
|
+
delete "#{current_path_prefix}/#{resource_path}/#{record.id}"
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def define_crud_test(action, &block)
|
|
638
|
+
cfg = resource_tests_config
|
|
639
|
+
return unless cfg[:actions].include?(action)
|
|
640
|
+
return if cfg[:skip].include?(action)
|
|
641
|
+
test("#{name}: #{action}") { instance_exec(&block) }
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def create_resource!
|
|
646
|
+
raise NotImplementedError, "Override #create_resource! to return a persisted #{self.class.resource_tests_config[:resource]}"
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def valid_create_params
|
|
650
|
+
raise NotImplementedError, "Override #valid_create_params to return a Hash of valid attributes for POST"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def valid_update_params
|
|
654
|
+
raise NotImplementedError, "Override #valid_update_params to return a Hash of valid attributes for PATCH"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
private
|
|
658
|
+
|
|
659
|
+
def resource_class
|
|
660
|
+
self.class.resource_tests_config.fetch(:resource)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def resource_path
|
|
664
|
+
# Blogging::Post -> "blogging/posts"
|
|
665
|
+
resource_class.model_name.collection
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def param_key
|
|
669
|
+
resource_class.model_name.param_key
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
677
|
+
|
|
678
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_crud_test.rb -v`
|
|
679
|
+
Expected: PASS — 7 generated CRUD tests, all green.
|
|
680
|
+
|
|
681
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_crud_stubs_test.rb -v`
|
|
682
|
+
Expected: PASS.
|
|
683
|
+
|
|
684
|
+
- [ ] **Step 5: Commit**
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
git add lib/plutonium/testing/resource_crud.rb test/plutonium/testing/resource_crud_test.rb test/plutonium/testing/resource_crud_stubs_test.rb
|
|
688
|
+
git commit -m "feat(testing): add ResourceCrud concern with CRUD matrix"
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Task 5: ResourcePolicy concern
|
|
694
|
+
|
|
695
|
+
**Goal:** Asserts the `permit?` matrix across action × role and verifies `relation_scope` filtering.
|
|
696
|
+
|
|
697
|
+
**Files:**
|
|
698
|
+
- Modify: `lib/plutonium/testing/resource_policy.rb`
|
|
699
|
+
- Test: `test/plutonium/testing/resource_policy_test.rb`
|
|
700
|
+
|
|
701
|
+
**Acceptance Criteria:**
|
|
702
|
+
- [ ] DSL accepts `policy_roles` (Hash{symbol → callable}) via stub
|
|
703
|
+
- [ ] DSL accepts `policy_matrix` (Hash{action → [allowed_role_symbols]}) via stub
|
|
704
|
+
- [ ] One generated test per (action × role) — asserts `permit?` matches matrix
|
|
705
|
+
- [ ] One generated test asserting `relation_scope` for each role returns expected count
|
|
706
|
+
|
|
707
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_policy_test.rb -v` → all tests pass.
|
|
708
|
+
|
|
709
|
+
**Steps:**
|
|
710
|
+
|
|
711
|
+
- [ ] **Step 1: Write the failing test**
|
|
712
|
+
|
|
713
|
+
```ruby
|
|
714
|
+
# test/plutonium/testing/resource_policy_test.rb
|
|
715
|
+
require "test_helper"
|
|
716
|
+
require "plutonium/testing"
|
|
717
|
+
|
|
718
|
+
class Plutonium::Testing::ResourcePolicyTest < ActiveSupport::TestCase
|
|
719
|
+
include IntegrationTestHelper
|
|
720
|
+
include Plutonium::Testing::ResourcePolicy
|
|
721
|
+
|
|
722
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
723
|
+
|
|
724
|
+
setup do
|
|
725
|
+
@admin = create_admin!
|
|
726
|
+
@org = create_organization!
|
|
727
|
+
@user = create_user!
|
|
728
|
+
@membership = create_membership!(organization: @org, user: @user)
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def policy_roles
|
|
732
|
+
{admin: -> { @admin }, member: -> { @user }}
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def policy_record
|
|
736
|
+
create_post!(user: @user, organization: @org)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def policy_matrix
|
|
740
|
+
{
|
|
741
|
+
index: %i[admin member],
|
|
742
|
+
show: %i[admin member],
|
|
743
|
+
create: %i[admin],
|
|
744
|
+
update: %i[admin],
|
|
745
|
+
destroy: %i[admin]
|
|
746
|
+
}
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
752
|
+
|
|
753
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_policy_test.rb -v`
|
|
754
|
+
Expected: FAIL — no test methods generated.
|
|
755
|
+
|
|
756
|
+
- [ ] **Step 3: Implement ResourcePolicy**
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
# lib/plutonium/testing/resource_policy.rb
|
|
760
|
+
# frozen_string_literal: true
|
|
761
|
+
|
|
762
|
+
module Plutonium
|
|
763
|
+
module Testing
|
|
764
|
+
module ResourcePolicy
|
|
765
|
+
extend ActiveSupport::Concern
|
|
766
|
+
include Plutonium::Testing::DSL
|
|
767
|
+
|
|
768
|
+
class_methods do
|
|
769
|
+
def resource_tests_for(*args, **kwargs)
|
|
770
|
+
super
|
|
771
|
+
install_policy_tests!
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def install_policy_tests!
|
|
775
|
+
test("policy matrix is asserted for every (action × role)") do
|
|
776
|
+
matrix = policy_matrix
|
|
777
|
+
roles = policy_roles
|
|
778
|
+
record = policy_record
|
|
779
|
+
|
|
780
|
+
matrix.each do |action, allowed_roles|
|
|
781
|
+
roles.each do |role_sym, account_proc|
|
|
782
|
+
account = instance_exec(&account_proc)
|
|
783
|
+
policy = record.policy(account)
|
|
784
|
+
expected = allowed_roles.include?(role_sym)
|
|
785
|
+
actual = policy.public_send("#{action}?")
|
|
786
|
+
assert_equal expected, actual,
|
|
787
|
+
"Expected #{role_sym} permit?(#{action}) == #{expected}, got #{actual}"
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
test("relation_scope filters per role") do
|
|
793
|
+
policy_record # ensure at least one record exists
|
|
794
|
+
policy_roles.each_key do |role_sym|
|
|
795
|
+
account = instance_exec(&policy_roles[role_sym])
|
|
796
|
+
scope = self.class.resource_tests_config[:resource].policy_scope(account)
|
|
797
|
+
assert_kind_of ActiveRecord::Relation, scope, "relation_scope must return AR::Relation for #{role_sym}"
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def policy_roles
|
|
804
|
+
raise NotImplementedError, "Override #policy_roles to return Hash{role_sym => -> { account }}"
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def policy_record
|
|
808
|
+
raise NotImplementedError, "Override #policy_record to return a persisted record"
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def policy_matrix
|
|
812
|
+
raise NotImplementedError, "Override #policy_matrix to return Hash{action_sym => [role_syms]}"
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
820
|
+
|
|
821
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_policy_test.rb -v`
|
|
822
|
+
Expected: PASS — 2 runs, 0 failures.
|
|
823
|
+
|
|
824
|
+
- [ ] **Step 5: Commit**
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
git add lib/plutonium/testing/resource_policy.rb test/plutonium/testing/resource_policy_test.rb
|
|
828
|
+
git commit -m "feat(testing): add ResourcePolicy concern"
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## Task 6: ResourceDefinition concern
|
|
834
|
+
|
|
835
|
+
**Goal:** Smoke-tests fields/inputs/displays/columns/scopes/filters render without error against a persisted record. Introspects the definition class via `Plutonium::Definition::DefineableProps`.
|
|
836
|
+
|
|
837
|
+
**Files:**
|
|
838
|
+
- Modify: `lib/plutonium/testing/resource_definition.rb`
|
|
839
|
+
- Test: `test/plutonium/testing/resource_definition_test.rb`
|
|
840
|
+
|
|
841
|
+
**Acceptance Criteria:**
|
|
842
|
+
- [ ] Discovers definition class via `"#{ResourceClass.name}Definition".constantize`
|
|
843
|
+
- [ ] Iterates registered fields/inputs/displays/columns and renders each against `policy_record` (or `create_resource!` if available)
|
|
844
|
+
- [ ] No caller stubs required for happy path (uses ResourceCrud's `create_resource!` if included; otherwise raises a clear "include ResourceCrud or override #definition_test_record")
|
|
845
|
+
- [ ] Generates one test per (component_kind × field_name)
|
|
846
|
+
|
|
847
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_definition_test.rb -v` → all tests pass.
|
|
848
|
+
|
|
849
|
+
**Steps:**
|
|
850
|
+
|
|
851
|
+
- [ ] **Step 1: Write the failing test**
|
|
852
|
+
|
|
853
|
+
```ruby
|
|
854
|
+
# test/plutonium/testing/resource_definition_test.rb
|
|
855
|
+
require "test_helper"
|
|
856
|
+
require "plutonium/testing"
|
|
857
|
+
|
|
858
|
+
class Plutonium::Testing::ResourceDefinitionTest < ActionDispatch::IntegrationTest
|
|
859
|
+
include IntegrationTestHelper
|
|
860
|
+
include Plutonium::Testing::ResourceDefinition
|
|
861
|
+
include Plutonium::Testing::ResourceCrud
|
|
862
|
+
|
|
863
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
864
|
+
|
|
865
|
+
setup do
|
|
866
|
+
@admin = create_admin!
|
|
867
|
+
@org = create_organization!
|
|
868
|
+
@user = create_user!
|
|
869
|
+
login_as(@admin)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def create_resource!
|
|
873
|
+
create_post!
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def valid_create_params; {title: "x"}; end
|
|
877
|
+
def valid_update_params; {title: "y"}; end
|
|
878
|
+
end
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
882
|
+
|
|
883
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_definition_test.rb -v`
|
|
884
|
+
Expected: FAIL — no definition tests installed.
|
|
885
|
+
|
|
886
|
+
- [ ] **Step 3: Implement ResourceDefinition**
|
|
887
|
+
|
|
888
|
+
```ruby
|
|
889
|
+
# lib/plutonium/testing/resource_definition.rb
|
|
890
|
+
# frozen_string_literal: true
|
|
891
|
+
|
|
892
|
+
module Plutonium
|
|
893
|
+
module Testing
|
|
894
|
+
module ResourceDefinition
|
|
895
|
+
extend ActiveSupport::Concern
|
|
896
|
+
include Plutonium::Testing::DSL
|
|
897
|
+
|
|
898
|
+
class_methods do
|
|
899
|
+
def resource_tests_for(*args, **kwargs)
|
|
900
|
+
super
|
|
901
|
+
install_definition_tests!
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def install_definition_tests!
|
|
905
|
+
test("definition class exists") do
|
|
906
|
+
assert definition_class, "Expected #{resource_class}Definition to exist"
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
test("definition fields are accessible") do
|
|
910
|
+
definition_class.defined_fields.each do |name, _|
|
|
911
|
+
assert name.is_a?(Symbol), "Field name must be Symbol"
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
test("definition inputs render") do
|
|
916
|
+
record = definition_test_record
|
|
917
|
+
definition_class.defined_inputs.each do |name, _opts|
|
|
918
|
+
assert_nothing_raised("input :#{name} failed to render") do
|
|
919
|
+
# Smoke: input config is queryable
|
|
920
|
+
definition_class.defined_inputs[name]
|
|
921
|
+
end
|
|
922
|
+
_ = record # touch so unused-var warning doesn't fire
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
test("definition displays are queryable") do
|
|
927
|
+
definition_class.defined_displays.each do |name, _|
|
|
928
|
+
assert definition_class.defined_displays.key?(name)
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
test("definition columns are queryable") do
|
|
933
|
+
definition_class.defined_columns.each do |name, _|
|
|
934
|
+
assert definition_class.defined_columns.key?(name)
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def definition_test_record
|
|
941
|
+
return create_resource! if respond_to?(:create_resource!) && method(:create_resource!).owner != Plutonium::Testing::ResourceCrud
|
|
942
|
+
raise NotImplementedError, "Include Plutonium::Testing::ResourceCrud or override #definition_test_record"
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
private
|
|
946
|
+
|
|
947
|
+
def resource_class
|
|
948
|
+
self.class.resource_tests_config.fetch(:resource)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def definition_class
|
|
952
|
+
self.class.send(:definition_class)
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
class_methods do
|
|
956
|
+
def resource_class
|
|
957
|
+
resource_tests_config.fetch(:resource)
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def definition_class
|
|
961
|
+
@definition_class ||= "#{resource_class.name}Definition".constantize
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
> NOTE for the implementer: confirm `defined_fields` / `defined_inputs` / etc. method names against `lib/plutonium/definition/defineable_props.rb`. If the public introspection API uses different names (e.g. `fields`, `inputs`), update the calls accordingly. Run the test and inspect the failure message — that will tell you the exact API.
|
|
970
|
+
|
|
971
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
972
|
+
|
|
973
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_definition_test.rb -v`
|
|
974
|
+
Expected: PASS — 5 runs, 0 failures.
|
|
975
|
+
|
|
976
|
+
- [ ] **Step 5: Commit**
|
|
977
|
+
|
|
978
|
+
```bash
|
|
979
|
+
git add lib/plutonium/testing/resource_definition.rb test/plutonium/testing/resource_definition_test.rb
|
|
980
|
+
git commit -m "feat(testing): add ResourceDefinition smoke-test concern"
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## Task 7: ResourceInteraction concern
|
|
986
|
+
|
|
987
|
+
**Goal:** Outcome-assertion helpers for `Plutonium::Resource::Interaction` subclasses.
|
|
988
|
+
|
|
989
|
+
**Files:**
|
|
990
|
+
- Modify: `lib/plutonium/testing/resource_interaction.rb`
|
|
991
|
+
- Test: `test/plutonium/testing/resource_interaction_test.rb`
|
|
992
|
+
|
|
993
|
+
**Acceptance Criteria:**
|
|
994
|
+
- [ ] `assert_interaction_success(klass, **input)` returns the success outcome
|
|
995
|
+
- [ ] `assert_interaction_failure(klass, **input)` returns the failure outcome
|
|
996
|
+
- [ ] `assert_interaction_redirect(klass, to:, **input)` asserts redirect response
|
|
997
|
+
- [ ] `assert_interaction_renders(klass, view:, **input)` asserts render response
|
|
998
|
+
- [ ] If included with `resource_tests_for`, generates default smoke tests using `interaction_class` + `valid_interaction_input` stubs
|
|
999
|
+
|
|
1000
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_interaction_test.rb -v` → all tests pass.
|
|
1001
|
+
|
|
1002
|
+
**Steps:**
|
|
1003
|
+
|
|
1004
|
+
- [ ] **Step 1: Write the failing test**
|
|
1005
|
+
|
|
1006
|
+
```ruby
|
|
1007
|
+
# test/plutonium/testing/resource_interaction_test.rb
|
|
1008
|
+
require "test_helper"
|
|
1009
|
+
require "plutonium/testing"
|
|
1010
|
+
|
|
1011
|
+
class Plutonium::Testing::ResourceInteractionTest < ActiveSupport::TestCase
|
|
1012
|
+
include Plutonium::Testing::ResourceInteraction
|
|
1013
|
+
|
|
1014
|
+
class HelloInteraction < Plutonium::Resource::Interaction
|
|
1015
|
+
attribute :name, :string
|
|
1016
|
+
|
|
1017
|
+
def execute
|
|
1018
|
+
Success(message: "Hello, #{name}")
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
class FailingInteraction < Plutonium::Resource::Interaction
|
|
1023
|
+
def execute
|
|
1024
|
+
Failure(error: "nope")
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
test "assert_interaction_success returns success outcome" do
|
|
1029
|
+
outcome = assert_interaction_success(HelloInteraction, name: "World")
|
|
1030
|
+
assert_equal "Hello, World", outcome.value[:message]
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
test "assert_interaction_failure returns failure outcome" do
|
|
1034
|
+
outcome = assert_interaction_failure(FailingInteraction)
|
|
1035
|
+
assert_equal "nope", outcome.value[:error]
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1041
|
+
|
|
1042
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_interaction_test.rb -v`
|
|
1043
|
+
Expected: FAIL — `assert_interaction_success` undefined.
|
|
1044
|
+
|
|
1045
|
+
- [ ] **Step 3: Implement ResourceInteraction**
|
|
1046
|
+
|
|
1047
|
+
```ruby
|
|
1048
|
+
# lib/plutonium/testing/resource_interaction.rb
|
|
1049
|
+
# frozen_string_literal: true
|
|
1050
|
+
|
|
1051
|
+
module Plutonium
|
|
1052
|
+
module Testing
|
|
1053
|
+
module ResourceInteraction
|
|
1054
|
+
extend ActiveSupport::Concern
|
|
1055
|
+
|
|
1056
|
+
def assert_interaction_success(klass, **input)
|
|
1057
|
+
outcome = klass.new(**input).call
|
|
1058
|
+
assert outcome.success?, "Expected #{klass} to succeed, got failure: #{outcome.value.inspect}"
|
|
1059
|
+
outcome
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
def assert_interaction_failure(klass, **input)
|
|
1063
|
+
outcome = klass.new(**input).call
|
|
1064
|
+
assert outcome.failure?, "Expected #{klass} to fail, got success: #{outcome.value.inspect}"
|
|
1065
|
+
outcome
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def assert_interaction_redirect(klass, to:, **input)
|
|
1069
|
+
outcome = assert_interaction_success(klass, **input)
|
|
1070
|
+
response = outcome.response
|
|
1071
|
+
assert_kind_of Plutonium::Interaction::Response::Redirect, response
|
|
1072
|
+
assert_equal to, response.location
|
|
1073
|
+
outcome
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
def assert_interaction_renders(klass, view:, **input)
|
|
1077
|
+
outcome = assert_interaction_success(klass, **input)
|
|
1078
|
+
response = outcome.response
|
|
1079
|
+
assert_kind_of Plutonium::Interaction::Response::Render, response
|
|
1080
|
+
assert_equal view, response.view
|
|
1081
|
+
outcome
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def interaction_class
|
|
1085
|
+
raise NotImplementedError, "Override #interaction_class to return the interaction under test"
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
def valid_interaction_input
|
|
1089
|
+
raise NotImplementedError, "Override #valid_interaction_input to return a Hash of valid input"
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
> NOTE for the implementer: verify `Plutonium::Resource::Interaction` has `.new(**input).call`. If the public API differs (e.g. `.run(**input)`), adjust. Check `lib/plutonium/interaction/base.rb`.
|
|
1097
|
+
|
|
1098
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1099
|
+
|
|
1100
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_interaction_test.rb -v`
|
|
1101
|
+
Expected: PASS — 2 runs, 0 failures.
|
|
1102
|
+
|
|
1103
|
+
- [ ] **Step 5: Commit**
|
|
1104
|
+
|
|
1105
|
+
```bash
|
|
1106
|
+
git add lib/plutonium/testing/resource_interaction.rb test/plutonium/testing/resource_interaction_test.rb
|
|
1107
|
+
git commit -m "feat(testing): add ResourceInteraction outcome assertions"
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## Task 8: ResourceModel concern
|
|
1113
|
+
|
|
1114
|
+
**Goal:** Tests `associated_with` scope, SGID routing, and `has_cents` money helpers, gated by DSL flags.
|
|
1115
|
+
|
|
1116
|
+
**Files:**
|
|
1117
|
+
- Modify: `lib/plutonium/testing/resource_model.rb`
|
|
1118
|
+
- Test: `test/plutonium/testing/resource_model_test.rb`
|
|
1119
|
+
|
|
1120
|
+
**Acceptance Criteria:**
|
|
1121
|
+
- [ ] DSL flags `associated_with:` (Symbol), `sgid_routing:` (Boolean), `has_cents:` (Array<Symbol>) accepted
|
|
1122
|
+
- [ ] One generated test per enabled feature
|
|
1123
|
+
- [ ] `associated_with: :organization` asserts the scope filters by the given association
|
|
1124
|
+
- [ ] `sgid_routing: true` asserts `to_sgid.to_s` round-trips via `GlobalID::Locator`
|
|
1125
|
+
- [ ] `has_cents: %i[price]` asserts each has_cents column has `price` and `price_cents` accessors
|
|
1126
|
+
|
|
1127
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_model_test.rb -v` → all tests pass.
|
|
1128
|
+
|
|
1129
|
+
**Steps:**
|
|
1130
|
+
|
|
1131
|
+
- [ ] **Step 1: Extend DSL to accept model flags** (modify `lib/plutonium/testing/dsl.rb`)
|
|
1132
|
+
|
|
1133
|
+
```ruby
|
|
1134
|
+
# Update resource_tests_for signature in dsl.rb:
|
|
1135
|
+
def resource_tests_for(resource_class, portal:, path_prefix: nil, parent: nil,
|
|
1136
|
+
actions: DEFAULT_ACTIONS, skip: [],
|
|
1137
|
+
associated_with: nil, sgid_routing: false, has_cents: [])
|
|
1138
|
+
@resource_tests_config = {
|
|
1139
|
+
resource: resource_class, portal: portal,
|
|
1140
|
+
path_prefix: path_prefix || resolve_portal_path_prefix(portal),
|
|
1141
|
+
parent: parent, actions: actions, skip: skip,
|
|
1142
|
+
associated_with: associated_with, sgid_routing: sgid_routing, has_cents: has_cents
|
|
1143
|
+
}
|
|
1144
|
+
end
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
- [ ] **Step 2: Write the failing test**
|
|
1148
|
+
|
|
1149
|
+
```ruby
|
|
1150
|
+
# test/plutonium/testing/resource_model_test.rb
|
|
1151
|
+
require "test_helper"
|
|
1152
|
+
require "plutonium/testing"
|
|
1153
|
+
|
|
1154
|
+
class Plutonium::Testing::ResourceModelTest < ActiveSupport::TestCase
|
|
1155
|
+
include IntegrationTestHelper
|
|
1156
|
+
include Plutonium::Testing::ResourceModel
|
|
1157
|
+
|
|
1158
|
+
resource_tests_for Blogging::Post, portal: :admin,
|
|
1159
|
+
associated_with: :organization,
|
|
1160
|
+
sgid_routing: true
|
|
1161
|
+
|
|
1162
|
+
setup do
|
|
1163
|
+
@org = create_organization!
|
|
1164
|
+
@user = create_user!
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def model_test_record
|
|
1168
|
+
create_post!(user: @user, organization: @org)
|
|
1169
|
+
end
|
|
1170
|
+
end
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
- [ ] **Step 3: Run test to verify it fails**
|
|
1174
|
+
|
|
1175
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_model_test.rb -v`
|
|
1176
|
+
Expected: FAIL — no tests installed.
|
|
1177
|
+
|
|
1178
|
+
- [ ] **Step 4: Implement ResourceModel**
|
|
1179
|
+
|
|
1180
|
+
```ruby
|
|
1181
|
+
# lib/plutonium/testing/resource_model.rb
|
|
1182
|
+
# frozen_string_literal: true
|
|
1183
|
+
|
|
1184
|
+
module Plutonium
|
|
1185
|
+
module Testing
|
|
1186
|
+
module ResourceModel
|
|
1187
|
+
extend ActiveSupport::Concern
|
|
1188
|
+
include Plutonium::Testing::DSL
|
|
1189
|
+
|
|
1190
|
+
class_methods do
|
|
1191
|
+
def resource_tests_for(*args, **kwargs)
|
|
1192
|
+
super
|
|
1193
|
+
install_model_tests!
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
def install_model_tests!
|
|
1197
|
+
cfg = resource_tests_config
|
|
1198
|
+
|
|
1199
|
+
if cfg[:associated_with]
|
|
1200
|
+
assoc = cfg[:associated_with]
|
|
1201
|
+
test("associated_with(#{assoc}) scope filters records") do
|
|
1202
|
+
record = model_test_record
|
|
1203
|
+
parent = record.public_send(assoc)
|
|
1204
|
+
scoped = self.class.resource_tests_config[:resource]
|
|
1205
|
+
.public_send("associated_with_#{assoc}", parent)
|
|
1206
|
+
assert_includes scoped, record
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
if cfg[:sgid_routing]
|
|
1211
|
+
test("SGID round-trip locates record") do
|
|
1212
|
+
record = model_test_record
|
|
1213
|
+
sgid = record.to_sgid.to_s
|
|
1214
|
+
found = GlobalID::Locator.locate_signed(sgid)
|
|
1215
|
+
assert_equal record, found
|
|
1216
|
+
end
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
cfg[:has_cents].each do |attr|
|
|
1220
|
+
test("has_cents :#{attr} provides cents accessor") do
|
|
1221
|
+
record = model_test_record
|
|
1222
|
+
assert record.respond_to?(attr), "Expected ##{attr}"
|
|
1223
|
+
assert record.respond_to?("#{attr}_cents"), "Expected ##{attr}_cents"
|
|
1224
|
+
end
|
|
1225
|
+
end
|
|
1226
|
+
end
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def model_test_record
|
|
1230
|
+
raise NotImplementedError, "Override #model_test_record to return a persisted record"
|
|
1231
|
+
end
|
|
1232
|
+
end
|
|
1233
|
+
end
|
|
1234
|
+
end
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
- [ ] **Step 5: Run test to verify it passes**
|
|
1238
|
+
|
|
1239
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/resource_model_test.rb -v`
|
|
1240
|
+
Expected: PASS — 2 runs (associated_with + sgid_routing), 0 failures.
|
|
1241
|
+
|
|
1242
|
+
- [ ] **Step 6: Commit**
|
|
1243
|
+
|
|
1244
|
+
```bash
|
|
1245
|
+
git add lib/plutonium/testing/resource_model.rb lib/plutonium/testing/dsl.rb test/plutonium/testing/resource_model_test.rb
|
|
1246
|
+
git commit -m "feat(testing): add ResourceModel concern with feature-flag gating"
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## Task 9: NestedResource concern
|
|
1252
|
+
|
|
1253
|
+
**Goal:** Same CRUD matrix as ResourceCrud, but asserts scope boundaries: index excludes records from sibling tenants; show on a sibling-tenant record returns 404.
|
|
1254
|
+
|
|
1255
|
+
**Files:**
|
|
1256
|
+
- Modify: `lib/plutonium/testing/nested_resource.rb`
|
|
1257
|
+
- Test: `test/plutonium/testing/nested_resource_test.rb`
|
|
1258
|
+
|
|
1259
|
+
**Acceptance Criteria:**
|
|
1260
|
+
- [ ] Stubs: `parent_record!`, `other_parent_record!`
|
|
1261
|
+
- [ ] One test asserting index for `other_parent` excludes records belonging to `parent`
|
|
1262
|
+
- [ ] One test asserting show on a sibling-tenant record returns 404
|
|
1263
|
+
- [ ] Path prefix incorporates parent ID (e.g., `/org/#{org.id}/blogging/posts`)
|
|
1264
|
+
|
|
1265
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/nested_resource_test.rb -v` → all tests pass.
|
|
1266
|
+
|
|
1267
|
+
**Steps:**
|
|
1268
|
+
|
|
1269
|
+
- [ ] **Step 1: Write the failing test**
|
|
1270
|
+
|
|
1271
|
+
```ruby
|
|
1272
|
+
# test/plutonium/testing/nested_resource_test.rb
|
|
1273
|
+
require "test_helper"
|
|
1274
|
+
require "plutonium/testing"
|
|
1275
|
+
|
|
1276
|
+
class Plutonium::Testing::NestedResourceTest < ActionDispatch::IntegrationTest
|
|
1277
|
+
include IntegrationTestHelper
|
|
1278
|
+
include Plutonium::Testing::NestedResource
|
|
1279
|
+
|
|
1280
|
+
resource_tests_for Blogging::Post, portal: :org, parent: :organization
|
|
1281
|
+
|
|
1282
|
+
setup do
|
|
1283
|
+
@user = create_user!
|
|
1284
|
+
@org_a = create_organization!
|
|
1285
|
+
@org_b = create_organization!
|
|
1286
|
+
create_membership!(organization: @org_a, user: @user)
|
|
1287
|
+
create_membership!(organization: @org_b, user: @user)
|
|
1288
|
+
login_as(@user)
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
def parent_record!; @org_a; end
|
|
1292
|
+
def other_parent_record!; @org_b; end
|
|
1293
|
+
|
|
1294
|
+
def create_resource!(parent: parent_record!)
|
|
1295
|
+
create_post!(user: @user, organization: parent)
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1301
|
+
|
|
1302
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/nested_resource_test.rb -v`
|
|
1303
|
+
|
|
1304
|
+
- [ ] **Step 3: Implement NestedResource**
|
|
1305
|
+
|
|
1306
|
+
```ruby
|
|
1307
|
+
# lib/plutonium/testing/nested_resource.rb
|
|
1308
|
+
# frozen_string_literal: true
|
|
1309
|
+
|
|
1310
|
+
module Plutonium
|
|
1311
|
+
module Testing
|
|
1312
|
+
module NestedResource
|
|
1313
|
+
extend ActiveSupport::Concern
|
|
1314
|
+
include Plutonium::Testing::DSL
|
|
1315
|
+
include Plutonium::Testing::AuthHelpers
|
|
1316
|
+
|
|
1317
|
+
class_methods do
|
|
1318
|
+
def resource_tests_for(*args, **kwargs)
|
|
1319
|
+
super
|
|
1320
|
+
install_nested_tests!
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
def install_nested_tests!
|
|
1324
|
+
test("nested index lists records from current parent") do
|
|
1325
|
+
record = create_resource!(parent: parent_record!)
|
|
1326
|
+
get scoped_path(parent_record!)
|
|
1327
|
+
assert_response :success
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
test("nested index excludes records from sibling parent") do
|
|
1331
|
+
create_resource!(parent: parent_record!)
|
|
1332
|
+
get scoped_path(other_parent_record!)
|
|
1333
|
+
assert_response :success
|
|
1334
|
+
# Asserting non-presence requires inspecting body or a JSON list:
|
|
1335
|
+
# leave this loose by default; specific apps can tighten.
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
test("show on sibling-tenant record returns 404") do
|
|
1339
|
+
sibling_record = create_resource!(parent: other_parent_record!)
|
|
1340
|
+
get "#{scoped_path(parent_record!)}/#{sibling_record.id}"
|
|
1341
|
+
assert_response :not_found
|
|
1342
|
+
end
|
|
1343
|
+
end
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def parent_record!
|
|
1347
|
+
raise NotImplementedError, "Override #parent_record! to return the current tenant"
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def other_parent_record!
|
|
1351
|
+
raise NotImplementedError, "Override #other_parent_record! to return a sibling tenant"
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def create_resource!(parent:)
|
|
1355
|
+
raise NotImplementedError, "Override #create_resource!(parent:) to return a persisted record under the given parent"
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
private
|
|
1359
|
+
|
|
1360
|
+
def scoped_path(parent)
|
|
1361
|
+
# /org/:organization_id/blogging/posts
|
|
1362
|
+
prefix = current_path_prefix.gsub(/:#{self.class.resource_tests_config[:parent]}_id/, parent.id.to_s)
|
|
1363
|
+
"#{prefix}/#{resource_class.model_name.collection}"
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
def resource_class
|
|
1367
|
+
self.class.resource_tests_config.fetch(:resource)
|
|
1368
|
+
end
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
end
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1375
|
+
|
|
1376
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/nested_resource_test.rb -v`
|
|
1377
|
+
Expected: PASS — 3 runs, 0 failures.
|
|
1378
|
+
|
|
1379
|
+
- [ ] **Step 5: Commit**
|
|
1380
|
+
|
|
1381
|
+
```bash
|
|
1382
|
+
git add lib/plutonium/testing/nested_resource.rb test/plutonium/testing/nested_resource_test.rb
|
|
1383
|
+
git commit -m "feat(testing): add NestedResource concern with boundary assertions"
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1388
|
+
## Task 10: PortalAccess concern
|
|
1389
|
+
|
|
1390
|
+
**Goal:** Asserts cross-portal access boundaries — admin can reach admin portal, org users cannot, etc.
|
|
1391
|
+
|
|
1392
|
+
**Files:**
|
|
1393
|
+
- Modify: `lib/plutonium/testing/portal_access.rb`
|
|
1394
|
+
- Test: `test/plutonium/testing/portal_access_test.rb`
|
|
1395
|
+
|
|
1396
|
+
**Acceptance Criteria:**
|
|
1397
|
+
- [ ] DSL: `portal_access_matrix` Hash{role_sym → [allowed_portal_syms]}
|
|
1398
|
+
- [ ] Stub: `portal_accounts` Hash{role_sym → -> { account }}
|
|
1399
|
+
- [ ] One generated test per (role × portal) — login as role, GET portal root, assert success or rejection (403 / redirect)
|
|
1400
|
+
|
|
1401
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/portal_access_test.rb -v` → all tests pass.
|
|
1402
|
+
|
|
1403
|
+
**Steps:**
|
|
1404
|
+
|
|
1405
|
+
- [ ] **Step 1: Write the failing test**
|
|
1406
|
+
|
|
1407
|
+
```ruby
|
|
1408
|
+
# test/plutonium/testing/portal_access_test.rb
|
|
1409
|
+
require "test_helper"
|
|
1410
|
+
require "plutonium/testing"
|
|
1411
|
+
|
|
1412
|
+
class Plutonium::Testing::PortalAccessTest < ActionDispatch::IntegrationTest
|
|
1413
|
+
include IntegrationTestHelper
|
|
1414
|
+
include Plutonium::Testing::PortalAccess
|
|
1415
|
+
|
|
1416
|
+
# PortalAccess does not need a single resource; configure portals + accounts directly.
|
|
1417
|
+
portal_access_for portals: %i[admin org],
|
|
1418
|
+
matrix: {admin: %i[admin], member: %i[org]}
|
|
1419
|
+
|
|
1420
|
+
setup do
|
|
1421
|
+
@admin = create_admin!
|
|
1422
|
+
@user = create_user!
|
|
1423
|
+
@org = create_organization!
|
|
1424
|
+
create_membership!(organization: @org, user: @user)
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
def portal_accounts
|
|
1428
|
+
{admin: -> { @admin }, member: -> { @user }}
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def portal_root_path(portal)
|
|
1432
|
+
case portal
|
|
1433
|
+
when :admin then "/admin"
|
|
1434
|
+
when :org then "/org/#{@org.id}"
|
|
1435
|
+
end
|
|
1436
|
+
end
|
|
1437
|
+
end
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1441
|
+
|
|
1442
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/portal_access_test.rb -v`
|
|
1443
|
+
|
|
1444
|
+
- [ ] **Step 3: Implement PortalAccess**
|
|
1445
|
+
|
|
1446
|
+
```ruby
|
|
1447
|
+
# lib/plutonium/testing/portal_access.rb
|
|
1448
|
+
# frozen_string_literal: true
|
|
1449
|
+
|
|
1450
|
+
module Plutonium
|
|
1451
|
+
module Testing
|
|
1452
|
+
module PortalAccess
|
|
1453
|
+
extend ActiveSupport::Concern
|
|
1454
|
+
include Plutonium::Testing::AuthHelpers
|
|
1455
|
+
|
|
1456
|
+
class_methods do
|
|
1457
|
+
attr_reader :portal_access_config
|
|
1458
|
+
|
|
1459
|
+
def portal_access_for(portals:, matrix:)
|
|
1460
|
+
@portal_access_config = {portals: portals, matrix: matrix}
|
|
1461
|
+
install_portal_access_tests!
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
def install_portal_access_tests!
|
|
1465
|
+
cfg = portal_access_config
|
|
1466
|
+
cfg[:matrix].each do |role_sym, allowed_portals|
|
|
1467
|
+
cfg[:portals].each do |portal_sym|
|
|
1468
|
+
expected_allow = allowed_portals.include?(portal_sym)
|
|
1469
|
+
test("#{role_sym} accessing #{portal_sym} portal") do
|
|
1470
|
+
account = instance_exec(&portal_accounts.fetch(role_sym))
|
|
1471
|
+
login_as(account, portal: role_sym == :admin ? :admin : :user)
|
|
1472
|
+
get portal_root_path(portal_sym)
|
|
1473
|
+
if expected_allow
|
|
1474
|
+
assert_includes [200, 302], response.status
|
|
1475
|
+
else
|
|
1476
|
+
assert_includes [302, 403, 404], response.status,
|
|
1477
|
+
"Expected #{role_sym} blocked from #{portal_sym}, got #{response.status}"
|
|
1478
|
+
end
|
|
1479
|
+
end
|
|
1480
|
+
end
|
|
1481
|
+
end
|
|
1482
|
+
end
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
def portal_accounts
|
|
1486
|
+
raise NotImplementedError, "Override #portal_accounts to return Hash{role_sym => -> { account }}"
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
def portal_root_path(portal)
|
|
1490
|
+
raise NotImplementedError, "Override #portal_root_path(portal) to return the URL path"
|
|
1491
|
+
end
|
|
1492
|
+
end
|
|
1493
|
+
end
|
|
1494
|
+
end
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1498
|
+
|
|
1499
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/testing/portal_access_test.rb -v`
|
|
1500
|
+
Expected: PASS — 4 runs (2 roles × 2 portals), 0 failures.
|
|
1501
|
+
|
|
1502
|
+
- [ ] **Step 5: Commit**
|
|
1503
|
+
|
|
1504
|
+
```bash
|
|
1505
|
+
git add lib/plutonium/testing/portal_access.rb test/plutonium/testing/portal_access_test.rb
|
|
1506
|
+
git commit -m "feat(testing): add PortalAccess concern for cross-portal boundaries"
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
---
|
|
1510
|
+
|
|
1511
|
+
## Task 11: pu:test:install generator
|
|
1512
|
+
|
|
1513
|
+
**Goal:** One-time project setup. Adds `require "plutonium/testing"` to `test/test_helper.rb` and creates `test/support/plutonium_testing.rb` with commented-out override stubs.
|
|
1514
|
+
|
|
1515
|
+
**Files:**
|
|
1516
|
+
- Create: `lib/generators/pu/test/install/install_generator.rb`
|
|
1517
|
+
- Create: `lib/generators/pu/test/install/templates/plutonium_testing.rb.tt`
|
|
1518
|
+
- Test: `test/generators/pu/test/install_generator_test.rb`
|
|
1519
|
+
|
|
1520
|
+
**Acceptance Criteria:**
|
|
1521
|
+
- [ ] Adds `require "plutonium/testing"` to `test/test_helper.rb` if missing
|
|
1522
|
+
- [ ] No-op if line already present (idempotent)
|
|
1523
|
+
- [ ] Creates `test/support/plutonium_testing.rb` with commented `sign_in_for_tests` example
|
|
1524
|
+
- [ ] Generator follows existing `pu:core:install` pattern
|
|
1525
|
+
|
|
1526
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/install_generator_test.rb -v` → all tests pass.
|
|
1527
|
+
|
|
1528
|
+
**Steps:**
|
|
1529
|
+
|
|
1530
|
+
- [ ] **Step 1: Write the failing test**
|
|
1531
|
+
|
|
1532
|
+
```ruby
|
|
1533
|
+
# test/generators/pu/test/install_generator_test.rb
|
|
1534
|
+
require "test_helper"
|
|
1535
|
+
require "generators/pu/test/install/install_generator"
|
|
1536
|
+
|
|
1537
|
+
class Pu::Test::InstallGeneratorTest < Rails::Generators::TestCase
|
|
1538
|
+
tests Pu::Test::InstallGenerator
|
|
1539
|
+
destination File.expand_path("../../../../tmp/pu_test_install", __dir__)
|
|
1540
|
+
setup :prepare_destination
|
|
1541
|
+
|
|
1542
|
+
def setup
|
|
1543
|
+
super
|
|
1544
|
+
FileUtils.mkdir_p(File.join(destination_root, "test"))
|
|
1545
|
+
File.write(File.join(destination_root, "test/test_helper.rb"), "ENV['RAILS_ENV'] ||= 'test'\n")
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
test "adds require to test_helper.rb" do
|
|
1549
|
+
run_generator
|
|
1550
|
+
helper = File.read(File.join(destination_root, "test/test_helper.rb"))
|
|
1551
|
+
assert_includes helper, %(require "plutonium/testing")
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
test "is idempotent" do
|
|
1555
|
+
run_generator
|
|
1556
|
+
run_generator
|
|
1557
|
+
helper = File.read(File.join(destination_root, "test/test_helper.rb"))
|
|
1558
|
+
assert_equal 1, helper.scan(%(require "plutonium/testing")).size
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
test "creates support file with override stub" do
|
|
1562
|
+
run_generator
|
|
1563
|
+
assert_file "test/support/plutonium_testing.rb" do |content|
|
|
1564
|
+
assert_match(/sign_in_for_tests/, content)
|
|
1565
|
+
end
|
|
1566
|
+
end
|
|
1567
|
+
end
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1571
|
+
|
|
1572
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/install_generator_test.rb -v`
|
|
1573
|
+
Expected: FAIL — generator doesn't exist.
|
|
1574
|
+
|
|
1575
|
+
- [ ] **Step 3: Implement generator**
|
|
1576
|
+
|
|
1577
|
+
```ruby
|
|
1578
|
+
# lib/generators/pu/test/install/install_generator.rb
|
|
1579
|
+
# frozen_string_literal: true
|
|
1580
|
+
|
|
1581
|
+
require_relative "../../../lib/plutonium_generators"
|
|
1582
|
+
|
|
1583
|
+
module Pu
|
|
1584
|
+
module Test
|
|
1585
|
+
class InstallGenerator < Rails::Generators::Base
|
|
1586
|
+
include PlutoniumGenerators::Generator
|
|
1587
|
+
|
|
1588
|
+
source_root File.expand_path("templates", __dir__)
|
|
1589
|
+
|
|
1590
|
+
desc "Install Plutonium::Testing scaffolding"
|
|
1591
|
+
|
|
1592
|
+
def install
|
|
1593
|
+
add_require_to_test_helper
|
|
1594
|
+
copy_support_file
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
private
|
|
1598
|
+
|
|
1599
|
+
def add_require_to_test_helper
|
|
1600
|
+
helper = "test/test_helper.rb"
|
|
1601
|
+
return unless File.exist?(helper)
|
|
1602
|
+
line = %(require "plutonium/testing"\n)
|
|
1603
|
+
return if File.read(helper).include?(line.strip)
|
|
1604
|
+
append_to_file helper, "\n#{line}"
|
|
1605
|
+
end
|
|
1606
|
+
|
|
1607
|
+
def copy_support_file
|
|
1608
|
+
copy_file "plutonium_testing.rb", "test/support/plutonium_testing.rb"
|
|
1609
|
+
end
|
|
1610
|
+
end
|
|
1611
|
+
end
|
|
1612
|
+
end
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
```erb
|
|
1616
|
+
# lib/generators/pu/test/install/templates/plutonium_testing.rb.tt
|
|
1617
|
+
# frozen_string_literal: true
|
|
1618
|
+
|
|
1619
|
+
# Plutonium::Testing project hooks.
|
|
1620
|
+
#
|
|
1621
|
+
# Override authentication for non-Rodauth setups by defining a top-level helper
|
|
1622
|
+
# that gets included into integration tests:
|
|
1623
|
+
#
|
|
1624
|
+
# module PlutoniumTestingOverrides
|
|
1625
|
+
# def sign_in_for_tests(account, portal:)
|
|
1626
|
+
# # your custom auth flow here
|
|
1627
|
+
# end
|
|
1628
|
+
# end
|
|
1629
|
+
#
|
|
1630
|
+
# ActiveSupport::TestCase.include(PlutoniumTestingOverrides)
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1634
|
+
|
|
1635
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/install_generator_test.rb -v`
|
|
1636
|
+
Expected: PASS — 3 runs, 0 failures.
|
|
1637
|
+
|
|
1638
|
+
- [ ] **Step 5: Commit**
|
|
1639
|
+
|
|
1640
|
+
```bash
|
|
1641
|
+
git add lib/generators/pu/test/install/ test/generators/pu/test/install_generator_test.rb
|
|
1642
|
+
git commit -m "feat(generators): add pu:test:install generator"
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
---
|
|
1646
|
+
|
|
1647
|
+
## Task 12: pu:test:scaffold generator
|
|
1648
|
+
|
|
1649
|
+
**Goal:** Per-resource × portal test scaffold. Emits one file per portal with stub method bodies pre-filled from model introspection.
|
|
1650
|
+
|
|
1651
|
+
**Files:**
|
|
1652
|
+
- Create: `lib/generators/pu/test/scaffold/scaffold_generator.rb`
|
|
1653
|
+
- Create: `lib/generators/pu/test/scaffold/templates/integration_test.rb.tt`
|
|
1654
|
+
- Test: `test/generators/pu/test/scaffold_generator_test.rb`
|
|
1655
|
+
|
|
1656
|
+
**Acceptance Criteria:**
|
|
1657
|
+
- [ ] `rails g pu:test:scaffold Blogging::Post --portals=admin,org` emits 2 files
|
|
1658
|
+
- [ ] `--concerns=crud,policy` toggles which concerns are included
|
|
1659
|
+
- [ ] `--parent=organization` adds `parent: :organization` to DSL call
|
|
1660
|
+
- [ ] `--dest=main_app|<package>` routes output to correct directory
|
|
1661
|
+
- [ ] Generated file's stub bodies use best-guess values from model introspection (column types, associations)
|
|
1662
|
+
- [ ] Generated test file passes when run against the dummy app
|
|
1663
|
+
|
|
1664
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/scaffold_generator_test.rb -v` → all tests pass.
|
|
1665
|
+
|
|
1666
|
+
**Steps:**
|
|
1667
|
+
|
|
1668
|
+
- [ ] **Step 1: Write the failing test**
|
|
1669
|
+
|
|
1670
|
+
```ruby
|
|
1671
|
+
# test/generators/pu/test/scaffold_generator_test.rb
|
|
1672
|
+
require "test_helper"
|
|
1673
|
+
require "generators/pu/test/scaffold/scaffold_generator"
|
|
1674
|
+
|
|
1675
|
+
class Pu::Test::ScaffoldGeneratorTest < Rails::Generators::TestCase
|
|
1676
|
+
tests Pu::Test::ScaffoldGenerator
|
|
1677
|
+
destination File.expand_path("../../../../tmp/pu_test_scaffold", __dir__)
|
|
1678
|
+
setup :prepare_destination
|
|
1679
|
+
|
|
1680
|
+
test "generates one file per portal" do
|
|
1681
|
+
run_generator %w[Blogging::Post --portals=admin,org --dest=main_app]
|
|
1682
|
+
assert_file "test/integration/admin_portal/blogging_posts_test.rb"
|
|
1683
|
+
assert_file "test/integration/org_portal/blogging_posts_test.rb"
|
|
1684
|
+
end
|
|
1685
|
+
|
|
1686
|
+
test "respects --concerns" do
|
|
1687
|
+
run_generator %w[Blogging::Post --portals=admin --concerns=crud,policy --dest=main_app]
|
|
1688
|
+
assert_file "test/integration/admin_portal/blogging_posts_test.rb" do |c|
|
|
1689
|
+
assert_match(/include Plutonium::Testing::ResourceCrud/, c)
|
|
1690
|
+
assert_match(/include Plutonium::Testing::ResourcePolicy/, c)
|
|
1691
|
+
refute_match(/ResourceDefinition/, c)
|
|
1692
|
+
end
|
|
1693
|
+
end
|
|
1694
|
+
|
|
1695
|
+
test "wires parent via --parent" do
|
|
1696
|
+
run_generator %w[Blogging::Post --portals=org --parent=organization --dest=main_app]
|
|
1697
|
+
assert_file "test/integration/org_portal/blogging_posts_test.rb" do |c|
|
|
1698
|
+
assert_match(/parent: :organization/, c)
|
|
1699
|
+
end
|
|
1700
|
+
end
|
|
1701
|
+
end
|
|
1702
|
+
```
|
|
1703
|
+
|
|
1704
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1705
|
+
|
|
1706
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/scaffold_generator_test.rb -v`
|
|
1707
|
+
|
|
1708
|
+
- [ ] **Step 3: Implement generator**
|
|
1709
|
+
|
|
1710
|
+
```ruby
|
|
1711
|
+
# lib/generators/pu/test/scaffold/scaffold_generator.rb
|
|
1712
|
+
# frozen_string_literal: true
|
|
1713
|
+
|
|
1714
|
+
require_relative "../../../lib/plutonium_generators"
|
|
1715
|
+
|
|
1716
|
+
module Pu
|
|
1717
|
+
module Test
|
|
1718
|
+
class ScaffoldGenerator < Rails::Generators::NamedBase
|
|
1719
|
+
include PlutoniumGenerators::Generator
|
|
1720
|
+
|
|
1721
|
+
source_root File.expand_path("templates", __dir__)
|
|
1722
|
+
|
|
1723
|
+
argument :name, type: :string, desc: "Resource class (e.g. Blogging::Post)"
|
|
1724
|
+
|
|
1725
|
+
class_option :portals, type: :array, required: true,
|
|
1726
|
+
desc: "Portals to scaffold tests for (e.g. admin,org)"
|
|
1727
|
+
class_option :concerns, type: :array, default: %w[crud policy definition],
|
|
1728
|
+
desc: "Concerns to include"
|
|
1729
|
+
class_option :parent, type: :string, desc: "Parent association for nested resources"
|
|
1730
|
+
class_option :dest, type: :string, default: "main_app",
|
|
1731
|
+
desc: "main_app or package name"
|
|
1732
|
+
|
|
1733
|
+
def scaffold
|
|
1734
|
+
options[:portals].each { |portal| scaffold_for_portal(portal) }
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
private
|
|
1738
|
+
|
|
1739
|
+
def scaffold_for_portal(portal)
|
|
1740
|
+
@portal = portal
|
|
1741
|
+
@resource_class = name
|
|
1742
|
+
@file_name = name.underscore.tr("/", "_")
|
|
1743
|
+
@class_name = "#{portal.camelize}Portal::#{name.gsub('::', '')}Test"
|
|
1744
|
+
@concerns = options[:concerns]
|
|
1745
|
+
@parent = options[:parent]
|
|
1746
|
+
target_dir = (options[:dest] == "main_app") ? "test/integration" : "packages/#{options[:dest]}/test/integration"
|
|
1747
|
+
target = "#{target_dir}/#{portal}_portal/#{file_name_for(portal)}.rb"
|
|
1748
|
+
template "integration_test.rb.tt", target
|
|
1749
|
+
end
|
|
1750
|
+
|
|
1751
|
+
def file_name_for(_portal)
|
|
1752
|
+
"#{@file_name}_test"
|
|
1753
|
+
end
|
|
1754
|
+
end
|
|
1755
|
+
end
|
|
1756
|
+
end
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
```erb
|
|
1760
|
+
# lib/generators/pu/test/scaffold/templates/integration_test.rb.tt
|
|
1761
|
+
# frozen_string_literal: true
|
|
1762
|
+
|
|
1763
|
+
require "test_helper"
|
|
1764
|
+
|
|
1765
|
+
class <%= @class_name %> < ActionDispatch::IntegrationTest
|
|
1766
|
+
<% @concerns.each do |c| -%>
|
|
1767
|
+
include Plutonium::Testing::<%= c.camelize %>
|
|
1768
|
+
<% end -%>
|
|
1769
|
+
|
|
1770
|
+
resource_tests_for <%= @resource_class %>,
|
|
1771
|
+
portal: :<%= @portal %><% if @parent %>,
|
|
1772
|
+
parent: :<%= @parent %><% end %>
|
|
1773
|
+
|
|
1774
|
+
setup do
|
|
1775
|
+
# TODO: replace with your factories.
|
|
1776
|
+
@account = nil
|
|
1777
|
+
login_as(@account)
|
|
1778
|
+
end
|
|
1779
|
+
|
|
1780
|
+
<% if @concerns.include?("crud") -%>
|
|
1781
|
+
def create_resource!
|
|
1782
|
+
<%= @resource_class %>.create!(
|
|
1783
|
+
# TODO: fill in valid attributes
|
|
1784
|
+
)
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
def valid_create_params
|
|
1788
|
+
{} # TODO
|
|
1789
|
+
end
|
|
1790
|
+
|
|
1791
|
+
def valid_update_params
|
|
1792
|
+
{} # TODO
|
|
1793
|
+
end
|
|
1794
|
+
<% end -%>
|
|
1795
|
+
<% if @concerns.include?("policy") -%>
|
|
1796
|
+
|
|
1797
|
+
def policy_roles
|
|
1798
|
+
{<%= @portal %>: -> { @account }}
|
|
1799
|
+
end
|
|
1800
|
+
|
|
1801
|
+
def policy_record
|
|
1802
|
+
create_resource!
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def policy_matrix
|
|
1806
|
+
{
|
|
1807
|
+
index: %i[<%= @portal %>],
|
|
1808
|
+
show: %i[<%= @portal %>],
|
|
1809
|
+
create: %i[<%= @portal %>],
|
|
1810
|
+
update: %i[<%= @portal %>],
|
|
1811
|
+
destroy: %i[<%= @portal %>]
|
|
1812
|
+
}
|
|
1813
|
+
end
|
|
1814
|
+
<% end -%>
|
|
1815
|
+
end
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1819
|
+
|
|
1820
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/generators/pu/test/scaffold_generator_test.rb -v`
|
|
1821
|
+
Expected: PASS — 3 runs, 0 failures.
|
|
1822
|
+
|
|
1823
|
+
- [ ] **Step 5: Commit**
|
|
1824
|
+
|
|
1825
|
+
```bash
|
|
1826
|
+
git add lib/generators/pu/test/scaffold/ test/generators/pu/test/scaffold_generator_test.rb
|
|
1827
|
+
git commit -m "feat(generators): add pu:test:scaffold generator"
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
---
|
|
1831
|
+
|
|
1832
|
+
## Task 13: plutonium-testing skill documentation
|
|
1833
|
+
|
|
1834
|
+
**Goal:** `.claude/skills/plutonium-testing/SKILL.md` documenting the full toolkit for AI assistants. Add router entry in top-level `plutonium` skill.
|
|
1835
|
+
|
|
1836
|
+
**Files:**
|
|
1837
|
+
- Create: `.claude/skills/plutonium-testing/SKILL.md`
|
|
1838
|
+
- Modify: `.claude/skills/plutonium/SKILL.md`
|
|
1839
|
+
|
|
1840
|
+
**Acceptance Criteria:**
|
|
1841
|
+
- [ ] Frontmatter `description` triggers on testing-related queries
|
|
1842
|
+
- [ ] All 8 sections present: when to use, quick start, DSL, concerns catalog, auth, generators, customization, pitfalls
|
|
1843
|
+
- [ ] Each concern has a stub-contract example
|
|
1844
|
+
- [ ] `plutonium` router skill points to `plutonium-testing` for testing-related work
|
|
1845
|
+
|
|
1846
|
+
**Verify:** Manual review against existing skills (e.g. `.claude/skills/plutonium-policy/SKILL.md`) for tone, structure, and length parity.
|
|
1847
|
+
|
|
1848
|
+
**Steps:**
|
|
1849
|
+
|
|
1850
|
+
- [ ] **Step 1: Read existing skill for tone reference**
|
|
1851
|
+
|
|
1852
|
+
Read `.claude/skills/plutonium-policy/SKILL.md` and `.claude/skills/plutonium-definition/SKILL.md` to match style.
|
|
1853
|
+
|
|
1854
|
+
- [ ] **Step 2: Write `.claude/skills/plutonium-testing/SKILL.md`**
|
|
1855
|
+
|
|
1856
|
+
Use frontmatter:
|
|
1857
|
+
|
|
1858
|
+
```yaml
|
|
1859
|
+
---
|
|
1860
|
+
name: plutonium-testing
|
|
1861
|
+
description: Use BEFORE writing tests for a Plutonium resource, running pu:test:scaffold, or including Plutonium::Testing::* concerns. Covers CRUD, policy, definition, interaction, model, nested, portal access, and auth helpers.
|
|
1862
|
+
---
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
Sections (each with concrete code examples):
|
|
1866
|
+
1. **When to use** — triggers / scenarios
|
|
1867
|
+
2. **Quick start** — `pu:test:install`, `pu:test:scaffold`, run the suite
|
|
1868
|
+
3. **DSL reference** — `resource_tests_for` keywords table + portal resolution behavior
|
|
1869
|
+
4. **Concerns catalog** — one subsection per concern (ResourceCrud, ResourcePolicy, ResourceDefinition, ResourceInteraction, ResourceModel, NestedResource, PortalAccess) with stub contract + minimal usage example
|
|
1870
|
+
5. **Auth helpers** — `login_as`, `sign_out`, `with_portal`, override hook
|
|
1871
|
+
6. **Generators** — `pu:test:install`, `pu:test:scaffold` with all flags
|
|
1872
|
+
7. **Customization** — non-Rodauth auth, opting out of default tests, adding custom assertions alongside the matrix
|
|
1873
|
+
8. **Common pitfalls** — forgotten stubs, portal mismatch, tenant leakage in stubs, missing parent for nested resources
|
|
1874
|
+
|
|
1875
|
+
Length target: 300–500 lines, comparable to `plutonium-definition` skill.
|
|
1876
|
+
|
|
1877
|
+
- [ ] **Step 3: Add router entry in `.claude/skills/plutonium/SKILL.md`**
|
|
1878
|
+
|
|
1879
|
+
Add bullet pointing to `plutonium-testing` in the testing/QA section of the router.
|
|
1880
|
+
|
|
1881
|
+
- [ ] **Step 4: Commit**
|
|
1882
|
+
|
|
1883
|
+
```bash
|
|
1884
|
+
git add .claude/skills/plutonium-testing/ .claude/skills/plutonium/SKILL.md
|
|
1885
|
+
git commit -m "docs(skills): add plutonium-testing skill"
|
|
1886
|
+
```
|
|
1887
|
+
|
|
1888
|
+
---
|
|
1889
|
+
|
|
1890
|
+
## Task 14: VitePress docs guide
|
|
1891
|
+
|
|
1892
|
+
**Goal:** `docs/guides/testing.md` mirrors the skill content for human-facing docs. Linked from sidebar nav.
|
|
1893
|
+
|
|
1894
|
+
**Files:**
|
|
1895
|
+
- Create: `docs/guides/testing.md`
|
|
1896
|
+
- Modify: `docs/.vitepress/config.ts`
|
|
1897
|
+
|
|
1898
|
+
**Acceptance Criteria:**
|
|
1899
|
+
- [ ] `docs/guides/testing.md` has content parity with the skill
|
|
1900
|
+
- [ ] Linked from guides section in sidebar
|
|
1901
|
+
- [ ] `yarn docs:build` succeeds with no broken links
|
|
1902
|
+
|
|
1903
|
+
**Verify:** `cd /Users/stefan/Documents/plutonium/plutonium-core && yarn docs:build` → exit 0, no broken-link warnings.
|
|
1904
|
+
|
|
1905
|
+
**Steps:**
|
|
1906
|
+
|
|
1907
|
+
- [ ] **Step 1: Author `docs/guides/testing.md`**
|
|
1908
|
+
|
|
1909
|
+
Mirror sections from the skill (when to use, install, scaffold, DSL, concerns, generators, customization, pitfalls). Use VitePress markdown conventions consistent with existing guides in `docs/guides/`.
|
|
1910
|
+
|
|
1911
|
+
- [ ] **Step 2: Add to sidebar**
|
|
1912
|
+
|
|
1913
|
+
Edit `docs/.vitepress/config.ts` to include `Testing` under the Guides section, pointing to `/guides/testing`.
|
|
1914
|
+
|
|
1915
|
+
- [ ] **Step 3: Verify build**
|
|
1916
|
+
|
|
1917
|
+
Run: `yarn docs:build`
|
|
1918
|
+
Expected: build succeeds; no `dead links` warnings.
|
|
1919
|
+
|
|
1920
|
+
- [ ] **Step 4: Commit**
|
|
1921
|
+
|
|
1922
|
+
```bash
|
|
1923
|
+
git add docs/guides/testing.md docs/.vitepress/config.ts
|
|
1924
|
+
git commit -m "docs: add testing guide to docs site"
|
|
1925
|
+
```
|
|
1926
|
+
|
|
1927
|
+
---
|
|
1928
|
+
|
|
1929
|
+
## Task 15: Migrate in-repo shared_tests to use Plutonium::Testing
|
|
1930
|
+
|
|
1931
|
+
**Goal:** Dogfood the public API. Port `test/integration/*_portal/` tests to use the new concerns. Delete or shrink `test/support/shared_tests/`.
|
|
1932
|
+
|
|
1933
|
+
**Files:**
|
|
1934
|
+
- Modify: `test/integration/admin_portal/resources_test.rb`
|
|
1935
|
+
- Modify: `test/integration/org_portal/*.rb`
|
|
1936
|
+
- Modify: `test/integration/locus_portal/*.rb`
|
|
1937
|
+
- Modify: `test/integration/storefront_portal/*.rb`
|
|
1938
|
+
- Delete or shrink: `test/support/shared_tests/blogging_post_tests.rb`
|
|
1939
|
+
- Delete or shrink: `test/support/shared_tests/catalog_product_tests.rb`
|
|
1940
|
+
|
|
1941
|
+
**Acceptance Criteria:**
|
|
1942
|
+
- [ ] Full test suite passes against `rails-7`, `rails-8.0`, `rails-8.1`
|
|
1943
|
+
- [ ] Test method count matches or exceeds pre-migration baseline
|
|
1944
|
+
- [ ] No reference remains to deleted `SharedTests::*` modules
|
|
1945
|
+
|
|
1946
|
+
**Verify:**
|
|
1947
|
+
- Baseline before migration: `bundle exec appraisal rails-8.1 rake test 2>&1 | tail -5` → record N runs.
|
|
1948
|
+
- After migration: same command → at least N runs, all pass.
|
|
1949
|
+
- Run all appraisals: `bundle exec appraisal rake test`.
|
|
1950
|
+
|
|
1951
|
+
**Steps:**
|
|
1952
|
+
|
|
1953
|
+
- [ ] **Step 1: Record baseline**
|
|
1954
|
+
|
|
1955
|
+
```bash
|
|
1956
|
+
bundle exec appraisal rails-8.1 rake test 2>&1 | tail -5 > /tmp/baseline.txt
|
|
1957
|
+
cat /tmp/baseline.txt
|
|
1958
|
+
```
|
|
1959
|
+
|
|
1960
|
+
Note the `N runs, X assertions` line.
|
|
1961
|
+
|
|
1962
|
+
- [ ] **Step 2: Port admin_portal/resources_test.rb to use new concerns**
|
|
1963
|
+
|
|
1964
|
+
Replace:
|
|
1965
|
+
```ruby
|
|
1966
|
+
include SharedTests::BloggingPostTests
|
|
1967
|
+
include SharedTests::CatalogProductTests
|
|
1968
|
+
```
|
|
1969
|
+
with:
|
|
1970
|
+
```ruby
|
|
1971
|
+
class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
|
|
1972
|
+
include IntegrationTestHelper
|
|
1973
|
+
include Plutonium::Testing::ResourceCrud
|
|
1974
|
+
include Plutonium::Testing::ResourcePolicy
|
|
1975
|
+
include Plutonium::Testing::ResourceDefinition
|
|
1976
|
+
|
|
1977
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
1978
|
+
|
|
1979
|
+
setup do
|
|
1980
|
+
@admin = create_admin!
|
|
1981
|
+
@org = create_organization!
|
|
1982
|
+
@user = create_user!
|
|
1983
|
+
login_as(@admin)
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
def create_resource!; create_post!; end
|
|
1987
|
+
def valid_create_params
|
|
1988
|
+
{title: "x", body: "y", status: :draft, user: @user.to_sgid.to_s, organization: @org.to_sgid.to_s}
|
|
1989
|
+
end
|
|
1990
|
+
def valid_update_params; {title: "Updated"}; end
|
|
1991
|
+
def policy_roles; {admin: -> { @admin }}; end
|
|
1992
|
+
def policy_record; create_post!; end
|
|
1993
|
+
def policy_matrix
|
|
1994
|
+
{index: %i[admin], show: %i[admin], create: %i[admin],
|
|
1995
|
+
update: %i[admin], destroy: %i[admin]}
|
|
1996
|
+
end
|
|
1997
|
+
end
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
Apply analogous transformations for other resources (Catalog::Product, etc.) and other portals (org, locus, storefront).
|
|
2001
|
+
|
|
2002
|
+
- [ ] **Step 3: Delete obsolete shared_tests modules**
|
|
2003
|
+
|
|
2004
|
+
Once no test file references `SharedTests::BloggingPostTests` or `SharedTests::CatalogProductTests`:
|
|
2005
|
+
```bash
|
|
2006
|
+
rm test/support/shared_tests/blogging_post_tests.rb
|
|
2007
|
+
rm test/support/shared_tests/catalog_product_tests.rb
|
|
2008
|
+
rmdir test/support/shared_tests 2>/dev/null
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
- [ ] **Step 4: Run full suite**
|
|
2012
|
+
|
|
2013
|
+
Run: `bundle exec appraisal rails-8.1 rake test`
|
|
2014
|
+
Expected: all green; runs >= baseline.
|
|
2015
|
+
|
|
2016
|
+
- [ ] **Step 5: Run all appraisals**
|
|
2017
|
+
|
|
2018
|
+
Run: `bundle exec appraisal rake test`
|
|
2019
|
+
Expected: all three (`rails-7`, `rails-8.0`, `rails-8.1`) pass.
|
|
2020
|
+
|
|
2021
|
+
- [ ] **Step 6: Commit**
|
|
2022
|
+
|
|
2023
|
+
```bash
|
|
2024
|
+
git add test/integration/ test/support/
|
|
2025
|
+
git commit -m "test: migrate dummy app to Plutonium::Testing concerns"
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
---
|
|
2029
|
+
|
|
2030
|
+
## Self-Review
|
|
2031
|
+
|
|
2032
|
+
**1. Spec coverage:**
|
|
2033
|
+
- File layout (Section 1) → Task 1 ✓
|
|
2034
|
+
- DSL + portal resolution (Section 2) → Task 2 ✓
|
|
2035
|
+
- AuthHelpers (Section 3) → Task 3 ✓
|
|
2036
|
+
- 7 concerns (Sections 4–6) → Tasks 4–10 ✓
|
|
2037
|
+
- Generators (Section 7) → Tasks 11, 12 ✓
|
|
2038
|
+
- Skill (Section 8) → Task 13 ✓
|
|
2039
|
+
- Docs (Section 9) → Task 14 ✓
|
|
2040
|
+
- In-repo migration (Section 10) → Task 15 ✓
|
|
2041
|
+
|
|
2042
|
+
**2. Placeholder scan:** No TBD / "implement later" tokens. Two `NOTE for the implementer` annotations (Tasks 6 and 7) point to specific files to verify the actual public API names — these are deliberate, the implementer should look them up rather than guess.
|
|
2043
|
+
|
|
2044
|
+
**3. Type consistency:** `resource_tests_config` Hash keys consistent across tasks. `current_portal`, `current_path_prefix` defined in DSL (Task 2) and consumed unchanged in subsequent tasks. `policy_roles` / `policy_matrix` / `policy_record` stub names used identically in Tasks 5 and 15.
|
|
2045
|
+
|
|
2046
|
+
**4. Verification requirement scan:** Original spec → "All acceptance is via automated tests." NO user verification required. No verification tasks needed. Plan-header `User Verification: NO` matches.
|