plan_my_stuff 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +595 -0
  3. data/CONFIGURATION.md +487 -0
  4. data/README.md +612 -88
  5. data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
  11. data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
  12. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
  13. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
  14. data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
  15. data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
  16. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
  17. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
  18. data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
  19. data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
  20. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
  21. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
  22. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
  23. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
  24. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
  25. data/app/jobs/plan_my_stuff/application_job.rb +8 -0
  26. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
  27. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  28. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
  29. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
  31. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  32. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
  33. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
  34. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
  35. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
  36. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
  37. data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
  38. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  39. data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
  40. data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
  41. data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
  42. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
  43. data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
  44. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
  45. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
  46. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
  47. data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
  50. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
  51. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  52. data/config/routes.rb +43 -15
  53. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
  54. data/lib/plan_my_stuff/application_record.rb +158 -1
  55. data/lib/plan_my_stuff/approval.rb +88 -0
  56. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  57. data/lib/plan_my_stuff/archive.rb +12 -0
  58. data/lib/plan_my_stuff/attachment.rb +83 -0
  59. data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
  60. data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
  61. data/lib/plan_my_stuff/base_metadata.rb +25 -28
  62. data/lib/plan_my_stuff/base_project.rb +502 -0
  63. data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
  64. data/lib/plan_my_stuff/base_project_item.rb +588 -0
  65. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  66. data/lib/plan_my_stuff/cache.rb +197 -0
  67. data/lib/plan_my_stuff/client.rb +139 -64
  68. data/lib/plan_my_stuff/comment.rb +225 -100
  69. data/lib/plan_my_stuff/comment_metadata.rb +68 -5
  70. data/lib/plan_my_stuff/configuration.rb +459 -28
  71. data/lib/plan_my_stuff/custom_fields.rb +96 -12
  72. data/lib/plan_my_stuff/engine.rb +14 -2
  73. data/lib/plan_my_stuff/errors.rb +65 -5
  74. data/lib/plan_my_stuff/graphql/queries.rb +454 -0
  75. data/lib/plan_my_stuff/issue.rb +1097 -166
  76. data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
  77. data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
  78. data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
  79. data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
  80. data/lib/plan_my_stuff/issue_field.rb +126 -0
  81. data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
  82. data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
  83. data/lib/plan_my_stuff/issue_metadata.rb +132 -21
  84. data/lib/plan_my_stuff/label.rb +100 -13
  85. data/lib/plan_my_stuff/link.rb +144 -0
  86. data/lib/plan_my_stuff/markdown.rb +13 -7
  87. data/lib/plan_my_stuff/metadata_parser.rb +51 -12
  88. data/lib/plan_my_stuff/notifications.rb +148 -0
  89. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
  90. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  91. data/lib/plan_my_stuff/pipeline/status.rb +40 -0
  92. data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
  93. data/lib/plan_my_stuff/pipeline.rb +310 -0
  94. data/lib/plan_my_stuff/project.rb +63 -465
  95. data/lib/plan_my_stuff/project_item.rb +3 -409
  96. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  97. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  98. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  99. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  100. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  101. data/lib/plan_my_stuff/reminders.rb +12 -0
  102. data/lib/plan_my_stuff/repo.rb +145 -0
  103. data/lib/plan_my_stuff/test_helpers.rb +265 -25
  104. data/lib/plan_my_stuff/testing_project.rb +292 -0
  105. data/lib/plan_my_stuff/testing_project_item.rb +218 -0
  106. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  107. data/lib/plan_my_stuff/user_resolver.rb +24 -3
  108. data/lib/plan_my_stuff/verifier.rb +10 -0
  109. data/lib/plan_my_stuff/version.rb +2 -2
  110. data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
  111. data/lib/plan_my_stuff.rb +55 -20
  112. data/lib/tasks/plan_my_stuff.rake +331 -0
  113. metadata +99 -4
@@ -3,17 +3,25 @@
3
3
  require 'plan_my_stuff'
4
4
 
5
5
  module PlanMyStuff
6
- # Test support for consuming apps. Provides:
6
+ # Test support for apps that *consume* PlanMyStuff. Provides:
7
7
  # - `PlanMyStuff.test_mode!` to stub all API calls
8
8
  # - Factory-style builders: `build_issue`, `build_comment`, `build_project`
9
9
  # - RSpec matchers: `expect_pms_issue_created`, `expect_pms_comment_created`, `expect_pms_item_moved`
10
10
  #
11
- # Usage:
11
+ # Usage (in a consuming Rails app):
12
12
  # require 'plan_my_stuff/test_helpers'
13
13
  #
14
14
  # RSpec.configure do |config|
15
15
  # config.include PlanMyStuff::TestHelpers
16
16
  # end
17
+ #
18
+ # Not the harness for PlanMyStuff's own unit specs. Gem-internal specs go
19
+ # through VCR cassettes against a real GitHub sandbox (see `spec/support/vcr.rb`
20
+ # and the `:pms_vcr_configured` shared context). Calling `PlanMyStuff.test_mode!`
21
+ # inside the gem would monkey-patch the very class methods the cassettes were
22
+ # recorded against, defeating the VCR coverage. The builder utilities
23
+ # (`build_issue`, `build_comment`, `build_project`, `stub_approvals`) are safe
24
+ # to use inside the gem as plain factories - they don't replace any methods.
17
25
  module TestHelpers
18
26
  # Recorded actions during test mode, keyed by type:
19
27
  # :issue_created, :comment_created, :item_moved
@@ -58,8 +66,8 @@ module PlanMyStuff
58
66
  custom_fields: {},
59
67
  }.merge(metadata))
60
68
 
61
- issue.instance_variable_set(:@metadata, issue_metadata)
62
- issue.instance_variable_set(:@persisted, true)
69
+ issue.metadata = issue_metadata
70
+ issue.__send__(:persisted!)
63
71
 
64
72
  body_comment = build_comment(
65
73
  body: body,
@@ -98,8 +106,8 @@ module PlanMyStuff
98
106
  custom_fields: {},
99
107
  }.merge(metadata))
100
108
 
101
- comment.instance_variable_set(:@metadata, comment_metadata)
102
- comment.instance_variable_set(:@persisted, true)
109
+ comment.metadata = comment_metadata
110
+ comment.__send__(:persisted!)
103
111
  comment
104
112
  end
105
113
 
@@ -124,7 +132,7 @@ module PlanMyStuff
124
132
  statuses: status_options,
125
133
  fields: [{ id: 'field_status', name: 'Status', options: status_options }],
126
134
  )
127
- project.instance_variable_set(:@persisted, true)
135
+ project.__send__(:persisted!)
128
136
 
129
137
  project.items = items.map do |item_hash|
130
138
  PlanMyStuff::ProjectItem.build(
@@ -143,6 +151,54 @@ module PlanMyStuff
143
151
 
144
152
  project
145
153
  end
154
+
155
+ # Sets approvals on an in-memory +Issue+ without hitting the API.
156
+ # Accepts user objects or integer user_ids.
157
+ #
158
+ # @param issue [PlanMyStuff::Issue]
159
+ # @param approved [Array<Object, Integer>] approvers who have approved
160
+ # @param pending [Array<Object, Integer>] approvers who have not yet approved
161
+ #
162
+ # @return [Array<PlanMyStuff::Approval>] the records written to +issue.metadata.approvals+
163
+ #
164
+ def stub_approvals(issue, approved: [], pending: [])
165
+ records =
166
+ pending.map { |u| PlanMyStuff::Approval.new(user_id: extract_user_id(u), status: 'pending') } +
167
+ approved.map do |u|
168
+ PlanMyStuff::Approval.new(user_id: extract_user_id(u), status: 'approved', approved_at: Time.current)
169
+ end
170
+ issue.metadata.approvals = records
171
+ records
172
+ end
173
+
174
+ # @return [Integer]
175
+ def extract_user_id(user)
176
+ user.is_a?(Integer) ? user : PlanMyStuff::UserResolver.user_id(user)
177
+ end
178
+ end
179
+
180
+ # Captures +*.plan_my_stuff+ +ActiveSupport::Notifications+ events fired
181
+ # inside the given block. Lets consuming apps assert that their code
182
+ # triggers gem events without threading subscriptions through every spec.
183
+ #
184
+ # Usage:
185
+ #
186
+ # events = PlanMyStuff::TestHelpers::Notifications.capture do
187
+ # PlanMyStuff::Issue.create!(...)
188
+ # end
189
+ # events.first[:name] # => "issue_created.plan_my_stuff"
190
+ # events.first[:payload] # => { issue:, user:, timestamp:, ... }
191
+ #
192
+ module Notifications
193
+ module_function
194
+
195
+ # @return [Array<Hash>] events as +{ name: String, payload: Hash }+
196
+ def capture(&)
197
+ events = []
198
+ callback = -> (name, _start, _finish, _id, payload) { events << { name: name, payload: payload } }
199
+ ActiveSupport::Notifications.subscribed(callback, /\.plan_my_stuff\z/, &)
200
+ events
201
+ end
146
202
  end
147
203
 
148
204
  # RSpec matchers included when the module is included in a test config.
@@ -218,6 +274,36 @@ module PlanMyStuff
218
274
  expect_pms_action(:item_assigned, 'item to be assigned', **filters)
219
275
  end
220
276
 
277
+ # @param filters [Hash] attribute filters (e.g. item_id:)
278
+ def expect_pms_pipeline_taken(**filters)
279
+ expect_pms_action(:pipeline_started, 'pipeline item to be taken', **filters)
280
+ end
281
+
282
+ # @param filters [Hash] attribute filters (e.g. item_id:)
283
+ def expect_pms_pipeline_in_review(**filters)
284
+ expect_pms_action(:pipeline_in_review, 'pipeline item marked in review', **filters)
285
+ end
286
+
287
+ # @param filters [Hash] attribute filters (e.g. item_id:)
288
+ def expect_pms_pipeline_testing(**filters)
289
+ expect_pms_action(:pipeline_testing, 'pipeline item moved to testing', **filters)
290
+ end
291
+
292
+ # @param filters [Hash] attribute filters (e.g. item_id:)
293
+ def expect_pms_pipeline_ready_for_release(**filters)
294
+ expect_pms_action(:pipeline_ready_for_release, 'pipeline item marked ready for release', **filters)
295
+ end
296
+
297
+ # @param filters [Hash] attribute filters (e.g. project_number:, commit_sha:)
298
+ def expect_pms_pipeline_deployment_started(**filters)
299
+ expect_pms_action(:pipeline_deployment_started, 'pipeline deployment started', **filters)
300
+ end
301
+
302
+ # @param filters [Hash] attribute filters (e.g. item_id:, deployment_id:)
303
+ def expect_pms_pipeline_deployment_completed(**filters)
304
+ expect_pms_action(:pipeline_deployment_completed, 'pipeline deployment completed', **filters)
305
+ end
306
+
221
307
  private
222
308
 
223
309
  # @return [void]
@@ -228,7 +314,7 @@ module PlanMyStuff
228
314
 
229
315
  expect(match).to(
230
316
  be_truthy,
231
- "Expected PMS #{description} with #{filters.inspect}, " \
317
+ "Expected PlanMyStuff #{description} with #{filters.inspect}, " \
232
318
  "but recorded actions were: #{format_actions(type)}",
233
319
  )
234
320
  end
@@ -257,7 +343,7 @@ module PlanMyStuff
257
343
  # @return [void]
258
344
  #
259
345
  def test_mode!
260
- TestHelpers.recorded_actions = []
346
+ PlanMyStuff::TestHelpers.recorded_actions = []
261
347
  return if @_test_mode
262
348
 
263
349
  @_test_mode_originals = {}
@@ -265,10 +351,11 @@ module PlanMyStuff
265
351
  stub_comment_class_methods!
266
352
  stub_project_class_methods!
267
353
  stub_project_item_class_methods!
354
+ stub_pipeline_methods!
268
355
  @_test_mode = true
269
356
  end
270
357
 
271
- # Restores original class methods overwritten by test_mode!
358
+ # Restores original class / instance methods overwritten by test_mode!
272
359
  #
273
360
  # @return [void]
274
361
  #
@@ -276,19 +363,24 @@ module PlanMyStuff
276
363
  return unless @_test_mode
277
364
 
278
365
  (@_test_mode_originals || {}).each do |klass, methods|
279
- methods.each do |name, original|
280
- klass.define_singleton_method(name, original)
366
+ methods.each do |key, original|
367
+ kind, name = key
368
+ if kind == :instance
369
+ klass.define_method(name, original)
370
+ else
371
+ klass.define_singleton_method(name, original)
372
+ end
281
373
  end
282
374
  end
283
375
 
284
376
  @_test_mode_originals = nil
285
377
  @_test_mode = false
286
- TestHelpers.recorded_actions = []
378
+ PlanMyStuff::TestHelpers.recorded_actions = []
287
379
  end
288
380
 
289
381
  private
290
382
 
291
- # Saves the original method so it can be restored by exit_test_mode!
383
+ # Saves the original class method so it can be restored by exit_test_mode!
292
384
  #
293
385
  # @param klass [Class]
294
386
  # @param method_name [Symbol]
@@ -297,13 +389,26 @@ module PlanMyStuff
297
389
  #
298
390
  def save_original(klass, method_name)
299
391
  @_test_mode_originals[klass] ||= {}
300
- @_test_mode_originals[klass][method_name] = klass.method(method_name)
392
+ @_test_mode_originals[klass][[:class, method_name]] = klass.method(method_name)
393
+ end
394
+
395
+ # Saves the original instance method so it can be restored by exit_test_mode!
396
+ #
397
+ # @param klass [Class]
398
+ # @param method_name [Symbol]
399
+ #
400
+ # @return [void]
401
+ #
402
+ def save_instance_original(klass, method_name)
403
+ @_test_mode_originals[klass] ||= {}
404
+ @_test_mode_originals[klass][[:instance, method_name]] = klass.instance_method(method_name)
301
405
  end
302
406
 
303
407
  # @return [void]
304
408
  def stub_issue_class_methods!
305
409
  issue_mod = PlanMyStuff::Issue
306
- %i[create! find list update! add_viewers remove_viewers].each { |m| save_original(issue_mod, m) }
410
+ %i[create! find list update!].each { |m| save_original(issue_mod, m) }
411
+ %i[add_viewers! remove_viewers!].each { |m| save_instance_original(issue_mod, m) }
307
412
 
308
413
  issue_mod.define_singleton_method(:create!) do |**params|
309
414
  PlanMyStuff::TestHelpers.recorded_actions << {
@@ -316,10 +421,17 @@ module PlanMyStuff
316
421
  },
317
422
  }
318
423
 
424
+ resolved_repo =
425
+ if params[:repo]
426
+ PlanMyStuff::Repo.resolve!(params[:repo]).full_name
427
+ else
428
+ 'TestOrg/TestRepo'
429
+ end
430
+
319
431
  PlanMyStuff::TestHelpers.build_issue(
320
432
  title: params[:title],
321
433
  body: params[:body],
322
- repo: params[:repo]&.to_s || 'TestOrg/TestRepo',
434
+ repo: resolved_repo,
323
435
  labels: params[:labels] || [],
324
436
  )
325
437
  end
@@ -330,7 +442,8 @@ module PlanMyStuff
330
442
  params: { number: number, repo: repo },
331
443
  }
332
444
 
333
- PlanMyStuff::TestHelpers.build_issue(number: number, repo: repo&.to_s || 'TestOrg/TestRepo')
445
+ resolved_repo = repo ? PlanMyStuff::Repo.resolve!(repo).full_name : 'TestOrg/TestRepo'
446
+ PlanMyStuff::TestHelpers.build_issue(number: number, repo: resolved_repo)
334
447
  end
335
448
 
336
449
  issue_mod.define_singleton_method(:list) do |**params|
@@ -351,19 +464,19 @@ module PlanMyStuff
351
464
  nil
352
465
  end
353
466
 
354
- issue_mod.define_singleton_method(:add_viewers) do |**params|
467
+ issue_mod.define_method(:add_viewers!) do |user_ids:, user: nil|
355
468
  PlanMyStuff::TestHelpers.recorded_actions << {
356
469
  type: :viewers_added,
357
- params: params,
470
+ params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
358
471
  }
359
472
 
360
473
  nil
361
474
  end
362
475
 
363
- issue_mod.define_singleton_method(:remove_viewers) do |**params|
476
+ issue_mod.define_method(:remove_viewers!) do |user_ids:, user: nil|
364
477
  PlanMyStuff::TestHelpers.recorded_actions << {
365
478
  type: :viewers_removed,
366
- params: params,
479
+ params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
367
480
  }
368
481
 
369
482
  nil
@@ -443,9 +556,9 @@ module PlanMyStuff
443
556
  # @return [void]
444
557
  def stub_project_item_class_methods!
445
558
  item_mod = PlanMyStuff::ProjectItem
446
- %i[move_item create! assign].each { |m| save_original(item_mod, m) }
559
+ %i[move_item! create! assign!].each { |m| save_original(item_mod, m) }
447
560
 
448
- item_mod.define_singleton_method(:move_item) do |**params|
561
+ item_mod.define_singleton_method(:move_item!) do |**params|
449
562
  PlanMyStuff::TestHelpers.recorded_actions << {
450
563
  type: :item_moved,
451
564
  params: {
@@ -484,7 +597,7 @@ module PlanMyStuff
484
597
  )
485
598
  end
486
599
 
487
- item_mod.define_singleton_method(:assign) do |**params|
600
+ item_mod.define_singleton_method(:assign!) do |**params|
488
601
  PlanMyStuff::TestHelpers.recorded_actions << {
489
602
  type: :item_assigned,
490
603
  params: {
@@ -497,5 +610,132 @@ module PlanMyStuff
497
610
  nil
498
611
  end
499
612
  end
613
+
614
+ # @return [void]
615
+ def stub_pipeline_methods!
616
+ pipeline_mod = PlanMyStuff::Pipeline
617
+ %i[
618
+ take! mark_in_review! request_testing! mark_ready_for_release! start_deployment! complete_deployment!
619
+ ].each { |m| save_original(pipeline_mod, m) }
620
+
621
+ pipeline_mod.define_singleton_method(:take!) do |project_item, user: nil|
622
+ PlanMyStuff::TestHelpers.recorded_actions << {
623
+ type: :pipeline_started,
624
+ params: {
625
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
626
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
627
+ user: user,
628
+ },
629
+ }
630
+
631
+ nil
632
+ end
633
+
634
+ pipeline_mod.define_singleton_method(:mark_in_review!) do |project_item|
635
+ PlanMyStuff::TestHelpers.recorded_actions << {
636
+ type: :pipeline_in_review,
637
+ params: {
638
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
639
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
640
+ },
641
+ }
642
+
643
+ nil
644
+ end
645
+
646
+ pipeline_mod.define_singleton_method(:request_testing!) do |project_item, user: nil|
647
+ field_name = PlanMyStuff.configuration.pipeline_testing_field_name
648
+ value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active)
649
+
650
+ PlanMyStuff::TestHelpers.recorded_actions << {
651
+ type: :pipeline_testing,
652
+ params: {
653
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
654
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
655
+ field_name: field_name,
656
+ value: value,
657
+ user: user,
658
+ },
659
+ }
660
+
661
+ if project_item.respond_to?(:field_values) && project_item.field_values.is_a?(Hash)
662
+ project_item.field_values[field_name] = value
663
+ end
664
+
665
+ nil
666
+ end
667
+
668
+ pipeline_mod.define_singleton_method(:mark_ready_for_release!) do |project_item|
669
+ PlanMyStuff::TestHelpers.recorded_actions << {
670
+ type: :pipeline_ready_for_release,
671
+ params: {
672
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
673
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
674
+ },
675
+ }
676
+
677
+ nil
678
+ end
679
+
680
+ pipeline_mod.define_singleton_method(:start_deployment!) do |project_number: nil, commit_sha: nil|
681
+ number =
682
+ project_number || PlanMyStuff.configuration.pipeline_project_number ||
683
+ PlanMyStuff.configuration.default_project_number
684
+
685
+ PlanMyStuff::TestHelpers.recorded_actions << {
686
+ type: :pipeline_deployment_started,
687
+ params: {
688
+ project_number: number,
689
+ commit_sha: commit_sha,
690
+ },
691
+ }
692
+
693
+ []
694
+ end
695
+
696
+ pipeline_mod.define_singleton_method(:complete_deployment!) do |project_item, deployment_id: nil|
697
+ PlanMyStuff::TestHelpers.recorded_actions << {
698
+ type: :pipeline_deployment_completed,
699
+ params: {
700
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
701
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
702
+ deployment_id: deployment_id,
703
+ },
704
+ }
705
+
706
+ nil
707
+ end
708
+ end
709
+ end
710
+ end
711
+
712
+ if defined?(RSpec::Matchers)
713
+ # Asserts that a +<name>.plan_my_stuff+ event was fired inside the block.
714
+ # Chain +.with(key: value, ...)+ to match on a payload subset.
715
+ #
716
+ # expect { Issue.create!(...) }.to(have_fired_event('issue_created.plan_my_stuff'))
717
+ # expect { Issue.create!(...) }.to(have_fired_event('issue_created.plan_my_stuff').with(user: alice))
718
+ #
719
+ RSpec::Matchers.define(:have_fired_event) do |event_name|
720
+ supports_block_expectations
721
+
722
+ chain(:with) { |expected_payload| @expected_payload = expected_payload }
723
+
724
+ match do |block|
725
+ events = PlanMyStuff::TestHelpers::Notifications.capture(&block)
726
+ matching = events.select { |e| e[:name] == event_name }
727
+ @fired_events = events
728
+ next false if matching.empty?
729
+ next true if @expected_payload.nil?
730
+
731
+ matching.any? { |e| @expected_payload.all? { |k, v| e[:payload][k] == v } }
732
+ end
733
+
734
+ failure_message do
735
+ fired_names = @fired_events.pluck(:name)
736
+ base = "expected block to fire #{event_name.inspect}"
737
+ base += " with payload including #{@expected_payload.inspect}" if @expected_payload
738
+ "#{base}, but fired: #{fired_names.empty? ? '(none)' : fired_names.inspect}"
739
+ end
500
740
  end
501
741
  end