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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +4 -0
  3. data/.claude/skills/plutonium-interaction/SKILL.md +23 -0
  4. data/.claude/skills/plutonium-nested-resources/SKILL.md +10 -0
  5. data/.claude/skills/plutonium-testing/SKILL.md +268 -0
  6. data/.yarnrc.yml +1 -0
  7. data/CHANGELOG.md +10 -0
  8. data/app/assets/plutonium.css +1 -1
  9. data/docs/.vitepress/config.ts +6 -0
  10. data/docs/guides/nested-resources.md +10 -0
  11. data/docs/guides/testing.md +154 -0
  12. data/docs/reference/controller/index.md +9 -4
  13. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md +2046 -0
  14. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md.tasks.json +21 -0
  15. data/docs/superpowers/specs/2026-04-14-plutonium-testing-design.md +364 -0
  16. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  17. data/lib/generators/pu/test/install/install_generator.rb +34 -0
  18. data/lib/generators/pu/test/install/templates/plutonium_testing.rb.tt +14 -0
  19. data/lib/generators/pu/test/scaffold/scaffold_generator.rb +55 -0
  20. data/lib/generators/pu/test/scaffold/templates/integration_test.rb.tt +65 -0
  21. data/lib/plutonium/core/controller.rb +18 -1
  22. data/lib/plutonium/testing/auth_helpers.rb +62 -0
  23. data/lib/plutonium/testing/dsl.rb +73 -0
  24. data/lib/plutonium/testing/nested_resource.rb +58 -0
  25. data/lib/plutonium/testing/portal_access.rb +49 -0
  26. data/lib/plutonium/testing/resource_crud.rb +104 -0
  27. data/lib/plutonium/testing/resource_definition.rb +61 -0
  28. data/lib/plutonium/testing/resource_interaction.rb +51 -0
  29. data/lib/plutonium/testing/resource_model.rb +53 -0
  30. data/lib/plutonium/testing/resource_policy.rb +72 -0
  31. data/lib/plutonium/testing.rb +16 -0
  32. data/lib/plutonium/version.rb +1 -1
  33. data/lib/plutonium.rb +2 -0
  34. data/package.json +1 -1
  35. data/yarn.lock +6037 -3893
  36. metadata +22 -2
@@ -0,0 +1,21 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-04-14-plutonium-testing.md",
3
+ "tasks": [
4
+ {"id": 1, "subject": "Create Plutonium::Testing module skeleton and autoload entry", "status": "pending", "planTaskNumber": 1},
5
+ {"id": 2, "subject": "Implement shared DSL and portal resolution", "status": "pending", "blockedBy": [1], "planTaskNumber": 2},
6
+ {"id": 10, "subject": "Implement portal-scoped AuthHelpers", "status": "pending", "blockedBy": [2], "planTaskNumber": 3},
7
+ {"id": 3, "subject": "Implement ResourceCrud concern", "status": "pending", "blockedBy": [2, 10], "planTaskNumber": 4},
8
+ {"id": 4, "subject": "Implement ResourcePolicy concern", "status": "pending", "blockedBy": [2], "planTaskNumber": 5},
9
+ {"id": 5, "subject": "Implement ResourceDefinition concern", "status": "pending", "blockedBy": [2, 3], "planTaskNumber": 6},
10
+ {"id": 6, "subject": "Implement ResourceInteraction concern", "status": "pending", "blockedBy": [1], "planTaskNumber": 7},
11
+ {"id": 7, "subject": "Implement ResourceModel concern", "status": "pending", "blockedBy": [2], "planTaskNumber": 8},
12
+ {"id": 8, "subject": "Implement NestedResource concern", "status": "pending", "blockedBy": [2, 10], "planTaskNumber": 9},
13
+ {"id": 9, "subject": "Implement PortalAccess concern", "status": "pending", "blockedBy": [10], "planTaskNumber": 10},
14
+ {"id": 11, "subject": "Implement pu:test:install generator", "status": "pending", "blockedBy": [1], "planTaskNumber": 11},
15
+ {"id": 12, "subject": "Implement pu:test:scaffold generator", "status": "pending", "blockedBy": [1], "planTaskNumber": 12},
16
+ {"id": 14, "subject": "Write plutonium-testing skill documentation", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 13},
17
+ {"id": 15, "subject": "Add VitePress docs guide for testing", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 14},
18
+ {"id": 13, "subject": "Migrate in-repo shared_tests to use Plutonium::Testing concerns", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "planTaskNumber": 15}
19
+ ],
20
+ "lastUpdated": "2026-04-14T00:00:00Z"
21
+ }
@@ -0,0 +1,364 @@
1
+ # Plutonium::Testing — Design Spec
2
+
3
+ **Date:** 2026-04-14
4
+ **Status:** Approved (pre-implementation)
5
+ **Audience:** End-app developers using Plutonium
6
+
7
+ ---
8
+
9
+ ## Goal
10
+
11
+ Ship `Plutonium::Testing` — a public, opt-in collection of Minitest concerns that give Plutonium app developers default test coverage for resources, policies, definitions, interactions, models, nested scoping, portal access, and authentication. Pair the module with a `plutonium-testing` skill and a Rails generator (`pu:test:scaffold`) that produces ready-to-run test files per (resource × portal) pairing.
12
+
13
+ ## Non-Goals
14
+
15
+ - RSpec support (Minitest only for first cut).
16
+ - A test data builder / factory layer (out of scope; callers wire their own factories or fixtures).
17
+ - Replacing Plutonium's own internal `test/support/shared_tests/` ahead of the public API landing — the migration is the *last* implementation step and serves as dogfooding.
18
+
19
+ ## Approach
20
+
21
+ - **Loading model:** opt-in `require "plutonium/testing"` in the consumer's `test/test_helper.rb`. No autoload, no production cost. Mirrors `ActiveSupport::Testing::*` conventions.
22
+ - **Granularity:** one concern per category. Callers `include` exactly what they need. No umbrella module.
23
+ - **DSL + stub methods:** declarative config (`resource_tests_for Post, portal: :admin`) for shape; abstract stub methods (raising `NotImplementedError`) for test data the caller must provide.
24
+ - **Portal-centric:** the `portal:` symbol is the single unit of configuration. It resolves to path prefix, default sign-in helper, expected scoping, and allowed action set by introspecting the mounted portal engine. Same resource × different portal = different test file.
25
+
26
+ ---
27
+
28
+ ## Architecture
29
+
30
+ ### File Layout
31
+
32
+ ```
33
+ lib/plutonium/
34
+ testing.rb # top-level require; loads submodules
35
+ testing/
36
+ dsl.rb # shared `resource_tests_for` + portal resolution
37
+ auth_helpers.rb # login_as / sign_out / with_portal (portal-aware)
38
+ resource_crud.rb # integration CRUD matrix
39
+ resource_policy.rb # permit? × action × role; relation_scope filtering
40
+ resource_definition.rb # fields/inputs/displays/columns/scopes smoke
41
+ resource_interaction.rb # interaction outcome assertions
42
+ resource_model.rb # associated_with, SGID, has_cents
43
+ nested_resource.rb # tenant-scoped CRUD + boundary assertions
44
+ portal_access.rb # cross-portal access boundaries
45
+
46
+ lib/generators/pu/test/
47
+ install/install_generator.rb # one-time setup
48
+ install/templates/plutonium_testing.rb.tt
49
+ scaffold/scaffold_generator.rb # per-resource × portal scaffold
50
+ scaffold/templates/integration_test.rb.tt
51
+ scaffold/templates/policy_test.rb.tt
52
+ scaffold/templates/definition_test.rb.tt
53
+
54
+ test/plutonium/testing/ # tests for the testing module itself
55
+ dsl_test.rb
56
+ auth_helpers_test.rb
57
+ resource_crud_test.rb
58
+ resource_policy_test.rb
59
+ resource_definition_test.rb
60
+ resource_interaction_test.rb
61
+ resource_model_test.rb
62
+ nested_resource_test.rb
63
+ portal_access_test.rb
64
+
65
+ test/generators/pu/test/
66
+ install_generator_test.rb
67
+ scaffold_generator_test.rb
68
+
69
+ .claude/skills/plutonium-testing/
70
+ SKILL.md
71
+
72
+ docs/guides/testing.md
73
+ ```
74
+
75
+ ### Loading
76
+
77
+ `lib/plutonium/testing.rb` is the entry point:
78
+
79
+ ```ruby
80
+ require "plutonium/testing/dsl"
81
+ require "plutonium/testing/auth_helpers"
82
+ require "plutonium/testing/resource_crud"
83
+ require "plutonium/testing/resource_policy"
84
+ require "plutonium/testing/resource_definition"
85
+ require "plutonium/testing/resource_interaction"
86
+ require "plutonium/testing/resource_model"
87
+ require "plutonium/testing/nested_resource"
88
+ require "plutonium/testing/portal_access"
89
+ ```
90
+
91
+ Consumers add `require "plutonium/testing"` to `test/test_helper.rb` (the `pu:test:install` generator does this for them).
92
+
93
+ ---
94
+
95
+ ## DSL
96
+
97
+ `Plutonium::Testing::DSL` is included by every concern. Provides one class-level method:
98
+
99
+ ```ruby
100
+ resource_tests_for ResourceClass,
101
+ portal: :admin, # required: portal symbol
102
+ path_prefix: "/admin", # optional: explicit override
103
+ parent: :organization, # optional: nested-resource parent
104
+ actions: %i[index show new create edit update destroy], # optional: opt-in set
105
+ skip: %i[destroy] # optional: opt-out individual tests
106
+ ```
107
+
108
+ The portal symbol drives:
109
+
110
+ | Derived from portal | Example for `:admin` | Example for `:org` |
111
+ |---|---|---|
112
+ | `path_prefix` | `/admin` | `/org/:organization_id` |
113
+ | Default sign-in | admin Rodauth strategy | org-member Rodauth strategy |
114
+ | Expected entity scoping | unscoped | scoped to `@organization` |
115
+ | Allowed action set | from definition | from definition |
116
+
117
+ DSL stores the current portal in a test-class-local attr; `AuthHelpers` reads it as default when no `portal:` kwarg is given.
118
+
119
+ ### Cross-portal tests
120
+
121
+ For tests that touch multiple portals (`PortalAccess` concern), `portal:` is an explicit kwarg on helper calls:
122
+
123
+ ```ruby
124
+ login_as(@admin, portal: :admin)
125
+ login_as(@user, portal: :org)
126
+ with_portal(:org) { get "/org/posts" } # block form
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Concerns
132
+
133
+ ### `Plutonium::Testing::ResourceCrud`
134
+
135
+ Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource.
136
+
137
+ **Stubs (caller must implement):**
138
+ ```ruby
139
+ def create_resource! # -> persisted record
140
+ def valid_create_params # -> Hash for POST
141
+ def valid_update_params # -> Hash for PATCH
142
+ ```
143
+
144
+ Sign-in is automatic from the portal's auth strategy unless the caller overrides `sign_in_for_tests(account, portal:)`.
145
+
146
+ ### `Plutonium::Testing::ResourcePolicy`
147
+
148
+ Asserts the `permit?` matrix across action × role and `relation_scope` filtering.
149
+
150
+ **Stubs:**
151
+ ```ruby
152
+ def policy_roles # -> { admin: -> { @admin }, member: -> { @user } }
153
+ def policy_record # -> record instance under test
154
+ def policy_matrix # -> { index: %i[admin member], destroy: %i[admin] }
155
+ ```
156
+
157
+ ### `Plutonium::Testing::ResourceDefinition`
158
+
159
+ Smoke-tests that all registered fields/inputs/displays/columns/scopes/filters render without error against a persisted record. Introspects the definition class via `Plutonium::Definition::DefineableProps`. **No caller stubs required** for the happy path.
160
+
161
+ ### `Plutonium::Testing::ResourceInteraction`
162
+
163
+ Outcome-assertion helpers for `Plutonium::Resource::Interaction` subclasses.
164
+
165
+ **Helpers:**
166
+ - `assert_interaction_success(interaction_class, **input)`
167
+ - `assert_interaction_failure(interaction_class, **input)`
168
+ - `assert_interaction_redirect(interaction_class, to:, **input)`
169
+ - `assert_interaction_renders(interaction_class, view:, **input)`
170
+
171
+ **Stubs:**
172
+ ```ruby
173
+ def interaction_class
174
+ def valid_interaction_input
175
+ ```
176
+
177
+ ### `Plutonium::Testing::ResourceModel`
178
+
179
+ Covers `associated_with` scope behavior, SGID routing, and `has_cents` money helpers. DSL flags select which features to test:
180
+
181
+ ```ruby
182
+ resource_tests_for Post, portal: :admin,
183
+ associated_with: :organization,
184
+ sgid_routing: true,
185
+ has_cents: %i[price]
186
+ ```
187
+
188
+ Only generates test methods for enabled features.
189
+
190
+ ### `Plutonium::Testing::NestedResource`
191
+
192
+ Same CRUD matrix as `ResourceCrud` but asserts scope boundaries: index excludes records from sibling tenants; show on a sibling-tenant record returns 404.
193
+
194
+ **Stubs:**
195
+ ```ruby
196
+ def parent_record! # -> persisted parent/tenant
197
+ def other_parent_record! # -> a different tenant for boundary tests
198
+ ```
199
+
200
+ ### `Plutonium::Testing::PortalAccess`
201
+
202
+ Cross-portal access boundaries.
203
+
204
+ **DSL:**
205
+ ```ruby
206
+ portal_access_matrix \
207
+ admin: %i[admin_portal],
208
+ member: %i[org_portal storefront_portal]
209
+ ```
210
+
211
+ Asserts unauthorized portals return 403 or redirect to login.
212
+
213
+ ---
214
+
215
+ ## Auth Helpers
216
+
217
+ `Plutonium::Testing::AuthHelpers` is included transitively by every concern.
218
+
219
+ **Public API:**
220
+ ```ruby
221
+ login_as(account) # uses portal from DSL
222
+ login_as(account, portal: :admin) # explicit override
223
+ sign_out # uses portal from DSL
224
+ sign_out(portal: :admin)
225
+ current_account # uses portal from DSL
226
+ current_account(portal: :admin)
227
+ with_portal(:org) { ... } # scoped portal switch
228
+ ```
229
+
230
+ **Implementation:**
231
+ - Looks up the portal's declared auth strategy from the mounted portal engine config.
232
+ - For Rodauth (stock Plutonium): fakes the session cookie directly; no full login round-trip.
233
+ - **Override hook** for non-Rodauth apps: caller defines `sign_in_for_tests(account, portal:)` and `AuthHelpers` defers to it.
234
+
235
+ The `pu:test:install` generator scaffolds a commented-out override in `test/support/plutonium_testing.rb`.
236
+
237
+ ---
238
+
239
+ ## Generators
240
+
241
+ ### `pu:test:install`
242
+
243
+ One-time project setup. Idempotent.
244
+
245
+ - Adds `require "plutonium/testing"` to `test/test_helper.rb` (no-op if present).
246
+ - Creates `test/support/plutonium_testing.rb` with commented-out override stubs for non-Rodauth auth.
247
+
248
+ ### `pu:test:scaffold`
249
+
250
+ Per-resource scaffold. Produces one file per (resource × portal).
251
+
252
+ ```bash
253
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
254
+ rails g pu:test:scaffold Blogging::Post --portals=admin --concerns=crud,policy,definition
255
+ rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --dest=blogging
256
+ ```
257
+
258
+ **Flags:**
259
+ - `--portals=admin,org` (required) — emits one file per portal.
260
+ - `--concerns=crud,policy,definition,nested,model,interaction,portal_access` (default: `crud,policy,definition`).
261
+ - `--parent=organization` — wires `NestedResource` parent stub.
262
+ - `--dest=main_app|package_name` — output destination (matches `pu:res:scaffold` convention).
263
+
264
+ **Example output** (`test/integration/admin_portal/blogging_posts_test.rb`):
265
+
266
+ ```ruby
267
+ require "test_helper"
268
+
269
+ class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
270
+ include Plutonium::Testing::ResourceCrud
271
+ include Plutonium::Testing::ResourcePolicy
272
+ include Plutonium::Testing::ResourceDefinition
273
+
274
+ resource_tests_for Blogging::Post, portal: :admin
275
+
276
+ setup do
277
+ @admin = create_admin! # TODO: replace with your factory
278
+ login_as(@admin)
279
+ end
280
+
281
+ def create_resource!
282
+ Blogging::Post.create!(title: "X", body: "...") # TODO: adjust
283
+ end
284
+
285
+ def valid_create_params
286
+ { title: "New", body: "..." } # TODO: adjust
287
+ end
288
+
289
+ def valid_update_params
290
+ { title: "Updated" } # TODO: adjust
291
+ end
292
+
293
+ def policy_roles
294
+ { admin: -> { @admin } } # TODO: add other roles
295
+ end
296
+
297
+ def policy_record
298
+ create_resource!
299
+ end
300
+
301
+ def policy_matrix
302
+ { index: %i[admin], show: %i[admin], create: %i[admin],
303
+ update: %i[admin], destroy: %i[admin] } # TODO: tighten
304
+ end
305
+ end
306
+ ```
307
+
308
+ Stub method bodies are pre-filled with best-guess values from model introspection (column types, associations).
309
+
310
+ ---
311
+
312
+ ## Skill
313
+
314
+ `.claude/skills/plutonium-testing/SKILL.md` follows the existing skill conventions in `.claude/skills/`.
315
+
316
+ **Frontmatter `description`:**
317
+ > Use BEFORE writing tests for a Plutonium resource, running `pu:test:scaffold`, or including `Plutonium::Testing::*` concerns. Covers the full testing toolkit: CRUD, policy, definition, interaction, model, nested, portal access, and auth helpers.
318
+
319
+ **Sections:**
320
+ 1. When to use
321
+ 2. Quick start (install + scaffold + first run)
322
+ 3. DSL reference (`resource_tests_for` keywords + portal resolution table)
323
+ 4. Concerns catalog (one section per concern with stub contract + example)
324
+ 5. Auth helpers
325
+ 6. Generator reference (`pu:test:install`, `pu:test:scaffold` flags)
326
+ 7. Customization & escape hatches (non-Rodauth auth, skipping defaults, custom assertions)
327
+ 8. Common pitfalls (forgotten stubs, portal mismatch, tenant leakage)
328
+
329
+ The top-level `plutonium` router skill gets one new entry pointing to `plutonium-testing`.
330
+
331
+ ---
332
+
333
+ ## Docs
334
+
335
+ `docs/guides/testing.md` mirrors the skill content for human-facing documentation. Linked from the guides section in `docs/.vitepress/config.ts`.
336
+
337
+ ---
338
+
339
+ ## In-Repo Adoption (Dogfooding)
340
+
341
+ After all concerns and generators land, port the dummy app's tests:
342
+
343
+ - `test/integration/admin_portal/resources_test.rb` and the other portal test files migrate to `Plutonium::Testing::*` concerns.
344
+ - `test/support/shared_tests/blogging_post_tests.rb` and `catalog_product_tests.rb` are deleted or shrunk to anything that doesn't fit the generic concerns.
345
+
346
+ Acceptance: zero coverage loss (compare test method counts before/after) and the entire suite still passes across `rails-7`, `rails-8.0`, `rails-8.1` appraisals.
347
+
348
+ ---
349
+
350
+ ## Implementation Sequence
351
+
352
+ 1. Module skeleton + entry point (`lib/plutonium/testing.rb`).
353
+ 2. Shared DSL + portal resolution.
354
+ 3. Auth helpers.
355
+ 4. Each concern (parallelizable after #2 and #3): ResourceCrud, ResourcePolicy, ResourceDefinition, ResourceInteraction, ResourceModel, NestedResource, PortalAccess.
356
+ 5. Generators: `pu:test:install`, then `pu:test:scaffold`.
357
+ 6. Skill + docs.
358
+ 7. In-repo migration (last — dogfoods the public API).
359
+
360
+ ---
361
+
362
+ ## Open Questions
363
+
364
+ None at design time. Implementation may surface portal-resolution edge cases (engines mounted at non-standard paths, multiple engines per account type) that warrant follow-up.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.3)
4
+ plutonium (0.46.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/plutonium_generators"
4
+
5
+ module Pu
6
+ module Test
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include PlutoniumGenerators::Generator
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Install Plutonium::Testing scaffolding"
13
+
14
+ def install
15
+ add_require_to_test_helper
16
+ copy_support_file
17
+ end
18
+
19
+ private
20
+
21
+ def add_require_to_test_helper
22
+ helper = "test/test_helper.rb"
23
+ return unless File.exist?(helper)
24
+ line = %(require "plutonium/testing")
25
+ return if File.read(helper).include?(line)
26
+ append_to_file helper, "\n#{line}\n"
27
+ end
28
+
29
+ def copy_support_file
30
+ copy_file "plutonium_testing.rb", "test/support/plutonium_testing.rb"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Plutonium::Testing project hooks.
4
+ #
5
+ # Override authentication for non-Rodauth setups by defining a top-level helper
6
+ # that gets included into integration tests:
7
+ #
8
+ # module PlutoniumTestingOverrides
9
+ # def sign_in_for_tests(account, portal:)
10
+ # # your custom auth flow here
11
+ # end
12
+ # end
13
+ #
14
+ # ActiveSupport::TestCase.include(PlutoniumTestingOverrides)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/plutonium_generators"
4
+
5
+ module Pu
6
+ module Test
7
+ class ScaffoldGenerator < Rails::Generators::NamedBase
8
+ include PlutoniumGenerators::Generator
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Scaffold Plutonium::Testing tests for a resource across one or more portals"
13
+
14
+ class_option :portals, type: :array, required: true,
15
+ desc: "Portals to scaffold tests for (e.g. admin,org)"
16
+ class_option :concerns, type: :array, default: %w[crud policy definition],
17
+ desc: "Concerns to include (crud,policy,definition,nested,model,interaction)"
18
+ class_option :parent, type: :string, desc: "Parent association for nested resources"
19
+ class_option :dest, type: :string, default: "main_app",
20
+ desc: "main_app or package name"
21
+
22
+ def scaffold
23
+ options[:portals].each { |portal| scaffold_for_portal(portal) }
24
+ end
25
+
26
+ private
27
+
28
+ CONCERN_MAP = {
29
+ "crud" => "ResourceCrud",
30
+ "policy" => "ResourcePolicy",
31
+ "definition" => "ResourceDefinition",
32
+ "model" => "ResourceModel",
33
+ "interaction" => "ResourceInteraction",
34
+ "nested" => "NestedResource",
35
+ "portal_access" => "PortalAccess"
36
+ }.freeze
37
+
38
+ def concern_module_name(concern)
39
+ CONCERN_MAP.fetch(concern) { concern.camelize }
40
+ end
41
+
42
+ def scaffold_for_portal(portal)
43
+ @portal = portal
44
+ @resource_class = name
45
+ @file_name = name.underscore.tr("/", "_")
46
+ @class_name = "#{portal.camelize}Portal::#{name.tr("::", "")}Test"
47
+ @concerns = options[:concerns]
48
+ @parent = options[:parent]
49
+ target_dir = (options[:dest] == "main_app") ? "test/integration" : "packages/#{options[:dest]}/test/integration"
50
+ target = "#{target_dir}/#{portal}_portal/#{@file_name}_test.rb"
51
+ template "integration_test.rb.tt", target
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class <%= @class_name %> < ActionDispatch::IntegrationTest
6
+ <% @concerns.each do |c| -%>
7
+ include Plutonium::Testing::<%= concern_module_name(c) %>
8
+ <% end -%>
9
+
10
+ resource_tests_for <%= @resource_class %>,
11
+ portal: :<%= @portal %><% if @parent %>,
12
+ parent: :<%= @parent %><% end %>
13
+
14
+ setup do
15
+ # TODO: replace with your factories.
16
+ @account = nil
17
+ login_as(@account)
18
+ end
19
+
20
+ <% if @concerns.include?("crud") -%>
21
+ def create_resource!
22
+ <%= @resource_class %>.create!(
23
+ # TODO: fill in valid attributes
24
+ )
25
+ end
26
+
27
+ def valid_create_params
28
+ {} # TODO
29
+ end
30
+
31
+ def valid_update_params
32
+ {} # TODO
33
+ end
34
+ <% end -%>
35
+ <% if @concerns.include?("policy") -%>
36
+
37
+ def policy_roles
38
+ {<%= @portal %>: -> { @account }}
39
+ end
40
+
41
+ def policy_record
42
+ create_resource!
43
+ end
44
+
45
+ def policy_matrix
46
+ {
47
+ index: %i[<%= @portal %>],
48
+ show: %i[<%= @portal %>],
49
+ create: %i[<%= @portal %>],
50
+ update: %i[<%= @portal %>],
51
+ destroy: %i[<%= @portal %>]
52
+ }
53
+ end
54
+ <% end -%>
55
+ <% if @concerns.include?("nested") && @parent -%>
56
+
57
+ def parent_record!
58
+ # TODO: return the current tenant for this test
59
+ end
60
+
61
+ def other_parent_record!
62
+ # TODO: return a sibling tenant
63
+ end
64
+ <% end -%>
65
+ end
@@ -87,11 +87,17 @@ module Plutonium
87
87
  #
88
88
  # @return [Hash] args to pass to `url_for`
89
89
  #
90
- def resource_url_args_for(*args, action: nil, parent: nil, association: nil, package: nil, **kwargs)
90
+ def resource_url_args_for(*args, action: nil, parent: nil, association: nil, package: nil, interaction: nil, **kwargs)
91
91
  element = args.first
92
92
 
93
93
  raise ArgumentError, "parent is required when using symbol association name" if element.is_a?(Symbol) && parent.nil?
94
94
 
95
+ if interaction
96
+ raise ArgumentError, "cannot pass both `interaction:` and `action:`" if action
97
+ action = interactive_action_type_for(element, ids: kwargs[:ids])
98
+ kwargs[:interactive_action] = interaction
99
+ end
100
+
95
101
  # For nested resources, use named route helpers to avoid Rails param recall ambiguity
96
102
  if parent.present?
97
103
  assoc_name = if element.is_a?(Symbol)
@@ -137,6 +143,17 @@ module Plutonium
137
143
 
138
144
  private
139
145
 
146
+ # Determine the interactive action type for the given element.
147
+ # Records → :interactive_record_action, classes/symbols with :ids → :interactive_bulk_action,
148
+ # otherwise :interactive_resource_action.
149
+ def interactive_action_type_for(element, ids: nil)
150
+ if element.is_a?(Class) || element.is_a?(Symbol) || element.nil?
151
+ ids.present? ? :interactive_bulk_action : :interactive_resource_action
152
+ else
153
+ :interactive_record_action
154
+ end
155
+ end
156
+
140
157
  def build_nested_resource_url_args(element, parent:, association_name:, route_config:, action: nil, **kwargs)
141
158
  prefix = Plutonium::Routing::NESTED_ROUTE_PREFIX
142
159
  is_singular = route_config[:route_type] == :resource
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Testing
5
+ module AuthHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ def login_as(account, portal: nil)
9
+ portal ||= current_portal
10
+ if respond_to?(:sign_in_for_tests)
11
+ sign_in_for_tests(account, portal: portal)
12
+ else
13
+ default_rodauth_login(account, portal: portal)
14
+ end
15
+ instance_variable_set(:"@__current_account_#{portal}", account)
16
+ end
17
+
18
+ def sign_out(portal: nil)
19
+ portal ||= current_portal
20
+ post logout_path_for(portal)
21
+ follow_redirect! if response.redirect?
22
+ instance_variable_set(:"@__current_account_#{portal}", nil)
23
+ end
24
+
25
+ def current_account(portal: nil)
26
+ portal ||= current_portal
27
+ instance_variable_get(:"@__current_account_#{portal}")
28
+ end
29
+
30
+ def with_portal(portal)
31
+ prev = @__portal_override
32
+ @__portal_override = portal
33
+ yield
34
+ ensure
35
+ @__portal_override = prev
36
+ end
37
+
38
+ private
39
+
40
+ def default_rodauth_login(account, portal:)
41
+ post login_path_for(portal), params: {email: account.email, password: "password123"}
42
+ follow_redirect! if response.redirect?
43
+ end
44
+
45
+ def login_path_for(portal)
46
+ "/#{account_table_for(portal)}/login"
47
+ end
48
+
49
+ def logout_path_for(portal)
50
+ "/#{account_table_for(portal)}/logout"
51
+ end
52
+
53
+ def account_table_for(portal)
54
+ case portal
55
+ when :admin then "admins"
56
+ when :user, :org then "users"
57
+ else portal.to_s.pluralize
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end