plan_my_stuff 0.1.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 +7 -0
- data/LICENSE +28 -0
- data/README.md +284 -0
- data/app/controllers/plan_my_stuff/application_controller.rb +76 -0
- data/app/controllers/plan_my_stuff/comments_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +145 -0
- data/app/controllers/plan_my_stuff/labels_controller.rb +30 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/projects_controller.rb +17 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +16 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +12 -0
- data/app/views/plan_my_stuff/issues/index.html.erb +37 -0
- data/app/views/plan_my_stuff/issues/new.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +41 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +23 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +58 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +13 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +101 -0
- data/config/routes.rb +25 -0
- data/lib/generators/plan_my_stuff/install/install_generator.rb +38 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +106 -0
- data/lib/generators/plan_my_stuff/views/views_generator.rb +22 -0
- data/lib/plan_my_stuff/application_record.rb +39 -0
- data/lib/plan_my_stuff/base_metadata.rb +136 -0
- data/lib/plan_my_stuff/client.rb +143 -0
- data/lib/plan_my_stuff/comment.rb +360 -0
- data/lib/plan_my_stuff/comment_metadata.rb +56 -0
- data/lib/plan_my_stuff/configuration.rb +139 -0
- data/lib/plan_my_stuff/custom_fields.rb +65 -0
- data/lib/plan_my_stuff/engine.rb +11 -0
- data/lib/plan_my_stuff/errors.rb +87 -0
- data/lib/plan_my_stuff/issue.rb +486 -0
- data/lib/plan_my_stuff/issue_metadata.rb +111 -0
- data/lib/plan_my_stuff/label.rb +59 -0
- data/lib/plan_my_stuff/markdown.rb +83 -0
- data/lib/plan_my_stuff/metadata_parser.rb +53 -0
- data/lib/plan_my_stuff/project.rb +504 -0
- data/lib/plan_my_stuff/project_item.rb +414 -0
- data/lib/plan_my_stuff/test_helpers.rb +501 -0
- data/lib/plan_my_stuff/user_resolver.rb +61 -0
- data/lib/plan_my_stuff/verifier.rb +102 -0
- data/lib/plan_my_stuff/version.rb +19 -0
- data/lib/plan_my_stuff.rb +69 -0
- data/lib/tasks/plan_my_stuff.rake +23 -0
- metadata +126 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'plan_my_stuff'
|
|
4
|
+
|
|
5
|
+
module PlanMyStuff
|
|
6
|
+
# Test support for consuming apps. Provides:
|
|
7
|
+
# - `PlanMyStuff.test_mode!` to stub all API calls
|
|
8
|
+
# - Factory-style builders: `build_issue`, `build_comment`, `build_project`
|
|
9
|
+
# - RSpec matchers: `expect_pms_issue_created`, `expect_pms_comment_created`, `expect_pms_item_moved`
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# require 'plan_my_stuff/test_helpers'
|
|
13
|
+
#
|
|
14
|
+
# RSpec.configure do |config|
|
|
15
|
+
# config.include PlanMyStuff::TestHelpers
|
|
16
|
+
# end
|
|
17
|
+
module TestHelpers
|
|
18
|
+
# Recorded actions during test mode, keyed by type:
|
|
19
|
+
# :issue_created, :comment_created, :item_moved
|
|
20
|
+
thread_mattr_accessor :recorded_actions
|
|
21
|
+
self.recorded_actions = []
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Builds a fake persisted Issue without hitting the API.
|
|
25
|
+
#
|
|
26
|
+
# @param title [String]
|
|
27
|
+
# @param body [String]
|
|
28
|
+
# @param state [Symbol, String]
|
|
29
|
+
# @param number [Integer]
|
|
30
|
+
# @param repo [String]
|
|
31
|
+
# @param labels [Array<String>]
|
|
32
|
+
# @param metadata [Hash] raw metadata fields to merge
|
|
33
|
+
#
|
|
34
|
+
# @return [PlanMyStuff::Issue]
|
|
35
|
+
#
|
|
36
|
+
def build_issue(
|
|
37
|
+
title: 'Test Issue',
|
|
38
|
+
body: 'Test body',
|
|
39
|
+
state: :open,
|
|
40
|
+
number: 1,
|
|
41
|
+
repo: 'TestOrg/TestRepo',
|
|
42
|
+
labels: [],
|
|
43
|
+
metadata: {}
|
|
44
|
+
)
|
|
45
|
+
issue = PlanMyStuff::Issue.new(
|
|
46
|
+
title: title,
|
|
47
|
+
body: body,
|
|
48
|
+
state: state.to_s,
|
|
49
|
+
number: number,
|
|
50
|
+
repo: repo,
|
|
51
|
+
labels: labels,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
issue_metadata = PlanMyStuff::IssueMetadata.from_hash({
|
|
55
|
+
schema_version: PlanMyStuff::BaseMetadata::SCHEMA_VERSION,
|
|
56
|
+
gem_version: PlanMyStuff::VERSION::STRING,
|
|
57
|
+
visibility: 'public',
|
|
58
|
+
custom_fields: {},
|
|
59
|
+
}.merge(metadata))
|
|
60
|
+
|
|
61
|
+
issue.instance_variable_set(:@metadata, issue_metadata)
|
|
62
|
+
issue.instance_variable_set(:@persisted, true)
|
|
63
|
+
|
|
64
|
+
body_comment = build_comment(
|
|
65
|
+
body: body,
|
|
66
|
+
issue: issue,
|
|
67
|
+
metadata: { issue_body: true },
|
|
68
|
+
)
|
|
69
|
+
issue.instance_variable_set(:@comments, [body_comment])
|
|
70
|
+
|
|
71
|
+
issue
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Builds a fake persisted Comment without hitting the API.
|
|
75
|
+
#
|
|
76
|
+
# @param body [String]
|
|
77
|
+
# @param visibility [Symbol, String]
|
|
78
|
+
# @param id [Integer]
|
|
79
|
+
# @param issue [PlanMyStuff::Issue, nil] parent issue (auto-built if nil)
|
|
80
|
+
# @param metadata [Hash] raw metadata fields to merge
|
|
81
|
+
#
|
|
82
|
+
# @return [PlanMyStuff::Comment]
|
|
83
|
+
#
|
|
84
|
+
def build_comment(body: 'Test comment', visibility: :public, id: 1, issue: nil, metadata: {})
|
|
85
|
+
issue ||= build_issue
|
|
86
|
+
|
|
87
|
+
comment = PlanMyStuff::Comment.new(
|
|
88
|
+
body: body,
|
|
89
|
+
id: id,
|
|
90
|
+
issue: issue,
|
|
91
|
+
)
|
|
92
|
+
comment.visibility = visibility.to_sym
|
|
93
|
+
|
|
94
|
+
comment_metadata = PlanMyStuff::CommentMetadata.from_hash({
|
|
95
|
+
schema_version: PlanMyStuff::BaseMetadata::SCHEMA_VERSION,
|
|
96
|
+
gem_version: PlanMyStuff::VERSION::STRING,
|
|
97
|
+
visibility: visibility.to_s,
|
|
98
|
+
custom_fields: {},
|
|
99
|
+
}.merge(metadata))
|
|
100
|
+
|
|
101
|
+
comment.instance_variable_set(:@metadata, comment_metadata)
|
|
102
|
+
comment.instance_variable_set(:@persisted, true)
|
|
103
|
+
comment
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Builds a fake persisted Project without hitting the API.
|
|
107
|
+
#
|
|
108
|
+
# @param title [String]
|
|
109
|
+
# @param number [Integer]
|
|
110
|
+
# @param statuses [Array<String>] status names (auto-assigned IDs)
|
|
111
|
+
# @param items [Array<Hash>] item data hashes
|
|
112
|
+
#
|
|
113
|
+
# @return [PlanMyStuff::Project]
|
|
114
|
+
#
|
|
115
|
+
def build_project(title: 'Test Project', number: 1, statuses: [], items: [])
|
|
116
|
+
status_options = statuses.each_with_index.map do |name, i|
|
|
117
|
+
{ id: "status_option_#{i}", name: name }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
project = PlanMyStuff::Project.new(
|
|
121
|
+
id: "PVT_fake_#{number}",
|
|
122
|
+
number: number,
|
|
123
|
+
title: title,
|
|
124
|
+
statuses: status_options,
|
|
125
|
+
fields: [{ id: 'field_status', name: 'Status', options: status_options }],
|
|
126
|
+
)
|
|
127
|
+
project.instance_variable_set(:@persisted, true)
|
|
128
|
+
|
|
129
|
+
project.items = items.map do |item_hash|
|
|
130
|
+
PlanMyStuff::ProjectItem.build(
|
|
131
|
+
{
|
|
132
|
+
id: item_hash[:id] || "PVTI_fake_#{rand(10_000)}",
|
|
133
|
+
title: item_hash[:title] || 'Untitled',
|
|
134
|
+
number: item_hash[:number],
|
|
135
|
+
url: item_hash[:url],
|
|
136
|
+
state: item_hash[:state],
|
|
137
|
+
status: item_hash[:status],
|
|
138
|
+
field_values: item_hash[:field_values] || {},
|
|
139
|
+
},
|
|
140
|
+
project: project,
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
project
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# RSpec matchers included when the module is included in a test config.
|
|
149
|
+
# Each matcher asserts that a matching action was recorded during test mode.
|
|
150
|
+
|
|
151
|
+
# @param filters [Hash] attribute filters (e.g. repo:, title:)
|
|
152
|
+
def expect_pms_issue_created(**filters)
|
|
153
|
+
expect_pms_action(:issue_created, 'issue to be created', **filters)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @param filters [Hash] attribute filters (e.g. number:, repo:)
|
|
157
|
+
def expect_pms_issue_found(**filters)
|
|
158
|
+
expect_pms_action(:issue_found, 'issue to be found', **filters)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @param filters [Hash] attribute filters
|
|
162
|
+
def expect_pms_issues_listed(**filters)
|
|
163
|
+
expect_pms_action(:issues_listed, 'issues to be listed', **filters)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param filters [Hash] attribute filters (e.g. number:, state:)
|
|
167
|
+
def expect_pms_issue_updated(**filters)
|
|
168
|
+
expect_pms_action(:issue_updated, 'issue to be updated', **filters)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @param filters [Hash] attribute filters (e.g. number:, user_ids:)
|
|
172
|
+
def expect_pms_viewers_added(**filters)
|
|
173
|
+
expect_pms_action(:viewers_added, 'viewers to be added', **filters)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @param filters [Hash] attribute filters (e.g. number:, user_ids:)
|
|
177
|
+
def expect_pms_viewers_removed(**filters)
|
|
178
|
+
expect_pms_action(:viewers_removed, 'viewers to be removed', **filters)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @param filters [Hash] attribute filters (e.g. issue_number:, visibility:)
|
|
182
|
+
def expect_pms_comment_created(**filters)
|
|
183
|
+
expect_pms_action(:comment_created, 'comment to be created', **filters)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @param filters [Hash] attribute filters (e.g. issue_number:, pms_only:)
|
|
187
|
+
def expect_pms_comments_listed(**filters)
|
|
188
|
+
expect_pms_action(:comments_listed, 'comments to be listed', **filters)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param filters [Hash] attribute filters (e.g. id:, body:)
|
|
192
|
+
def expect_pms_comment_updated(**filters)
|
|
193
|
+
expect_pms_action(:comment_updated, 'comment to be updated', **filters)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @param filters [Hash] attribute filters
|
|
197
|
+
def expect_pms_projects_listed(**filters)
|
|
198
|
+
expect_pms_action(:projects_listed, 'projects to be listed', **filters)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @param filters [Hash] attribute filters (e.g. number:)
|
|
202
|
+
def expect_pms_project_found(**filters)
|
|
203
|
+
expect_pms_action(:project_found, 'project to be found', **filters)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @param filters [Hash] attribute filters (e.g. project:, status:)
|
|
207
|
+
def expect_pms_item_moved(**filters)
|
|
208
|
+
expect_pms_action(:item_moved, 'item to be moved', **filters)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @param filters [Hash] attribute filters (e.g. project:, title:)
|
|
212
|
+
def expect_pms_item_created(**filters)
|
|
213
|
+
expect_pms_action(:item_created, 'item to be created', **filters)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @param filters [Hash] attribute filters (e.g. project:, assignee:)
|
|
217
|
+
def expect_pms_item_assigned(**filters)
|
|
218
|
+
expect_pms_action(:item_assigned, 'item to be assigned', **filters)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
# @return [void]
|
|
224
|
+
def expect_pms_action(type, description, **filters)
|
|
225
|
+
match = PlanMyStuff::TestHelpers.recorded_actions.find do |action|
|
|
226
|
+
action[:type] == type && matches_filters?(action[:params], filters)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
expect(match).to(
|
|
230
|
+
be_truthy,
|
|
231
|
+
"Expected PMS #{description} with #{filters.inspect}, " \
|
|
232
|
+
"but recorded actions were: #{format_actions(type)}",
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @return [Boolean]
|
|
237
|
+
def matches_filters?(params, filters)
|
|
238
|
+
filters.all? do |key, value|
|
|
239
|
+
params.key?(key) && params[key].to_s == value.to_s
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# @return [String]
|
|
244
|
+
def format_actions(type)
|
|
245
|
+
actions = PlanMyStuff::TestHelpers.recorded_actions.select { |a| a[:type] == type }
|
|
246
|
+
return '(none)' if actions.empty?
|
|
247
|
+
|
|
248
|
+
actions.map { |a| a[:params].inspect }.join(', ')
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class << self
|
|
253
|
+
# Activates test mode: stubs all API-calling class methods on Issue,
|
|
254
|
+
# Comment, and ProjectItem so no real HTTP requests are made.
|
|
255
|
+
# Records all actions for assertion matchers.
|
|
256
|
+
#
|
|
257
|
+
# @return [void]
|
|
258
|
+
#
|
|
259
|
+
def test_mode!
|
|
260
|
+
TestHelpers.recorded_actions = []
|
|
261
|
+
return if @_test_mode
|
|
262
|
+
|
|
263
|
+
@_test_mode_originals = {}
|
|
264
|
+
stub_issue_class_methods!
|
|
265
|
+
stub_comment_class_methods!
|
|
266
|
+
stub_project_class_methods!
|
|
267
|
+
stub_project_item_class_methods!
|
|
268
|
+
@_test_mode = true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Restores original class methods overwritten by test_mode!
|
|
272
|
+
#
|
|
273
|
+
# @return [void]
|
|
274
|
+
#
|
|
275
|
+
def exit_test_mode!
|
|
276
|
+
return unless @_test_mode
|
|
277
|
+
|
|
278
|
+
(@_test_mode_originals || {}).each do |klass, methods|
|
|
279
|
+
methods.each do |name, original|
|
|
280
|
+
klass.define_singleton_method(name, original)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
@_test_mode_originals = nil
|
|
285
|
+
@_test_mode = false
|
|
286
|
+
TestHelpers.recorded_actions = []
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
# Saves the original method so it can be restored by exit_test_mode!
|
|
292
|
+
#
|
|
293
|
+
# @param klass [Class]
|
|
294
|
+
# @param method_name [Symbol]
|
|
295
|
+
#
|
|
296
|
+
# @return [void]
|
|
297
|
+
#
|
|
298
|
+
def save_original(klass, method_name)
|
|
299
|
+
@_test_mode_originals[klass] ||= {}
|
|
300
|
+
@_test_mode_originals[klass][method_name] = klass.method(method_name)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# @return [void]
|
|
304
|
+
def stub_issue_class_methods!
|
|
305
|
+
issue_mod = PlanMyStuff::Issue
|
|
306
|
+
%i[create! find list update! add_viewers remove_viewers].each { |m| save_original(issue_mod, m) }
|
|
307
|
+
|
|
308
|
+
issue_mod.define_singleton_method(:create!) do |**params|
|
|
309
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
310
|
+
type: :issue_created,
|
|
311
|
+
params: {
|
|
312
|
+
title: params[:title],
|
|
313
|
+
body: params[:body],
|
|
314
|
+
repo: params[:repo],
|
|
315
|
+
labels: params[:labels] || [],
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
PlanMyStuff::TestHelpers.build_issue(
|
|
320
|
+
title: params[:title],
|
|
321
|
+
body: params[:body],
|
|
322
|
+
repo: params[:repo]&.to_s || 'TestOrg/TestRepo',
|
|
323
|
+
labels: params[:labels] || [],
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
issue_mod.define_singleton_method(:find) do |number, repo: nil|
|
|
328
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
329
|
+
type: :issue_found,
|
|
330
|
+
params: { number: number, repo: repo },
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
PlanMyStuff::TestHelpers.build_issue(number: number, repo: repo&.to_s || 'TestOrg/TestRepo')
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
issue_mod.define_singleton_method(:list) do |**params|
|
|
337
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
338
|
+
type: :issues_listed,
|
|
339
|
+
params: params,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
[]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
issue_mod.define_singleton_method(:update!) do |**params|
|
|
346
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
347
|
+
type: :issue_updated,
|
|
348
|
+
params: params,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
nil
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
issue_mod.define_singleton_method(:add_viewers) do |**params|
|
|
355
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
356
|
+
type: :viewers_added,
|
|
357
|
+
params: params,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
issue_mod.define_singleton_method(:remove_viewers) do |**params|
|
|
364
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
365
|
+
type: :viewers_removed,
|
|
366
|
+
params: params,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# @return [void]
|
|
374
|
+
def stub_comment_class_methods!
|
|
375
|
+
comment_mod = PlanMyStuff::Comment
|
|
376
|
+
%i[create! list update!].each { |m| save_original(comment_mod, m) }
|
|
377
|
+
|
|
378
|
+
comment_mod.define_singleton_method(:create!) do |**params|
|
|
379
|
+
issue_number = params[:issue].respond_to?(:number) ? params[:issue].number : nil
|
|
380
|
+
|
|
381
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
382
|
+
type: :comment_created,
|
|
383
|
+
params: {
|
|
384
|
+
issue_number: issue_number,
|
|
385
|
+
body: params[:body],
|
|
386
|
+
visibility: (params[:visibility] || :public).to_sym,
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
PlanMyStuff::TestHelpers.build_comment(
|
|
391
|
+
body: params[:body],
|
|
392
|
+
visibility: params[:visibility] || :public,
|
|
393
|
+
issue: params[:issue],
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
comment_mod.define_singleton_method(:list) do |**params|
|
|
398
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
399
|
+
type: :comments_listed,
|
|
400
|
+
params: {
|
|
401
|
+
issue_number: params[:issue].respond_to?(:number) ? params[:issue].number : nil,
|
|
402
|
+
pms_only: params[:pms_only] || false,
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
[]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
comment_mod.define_singleton_method(:update!) do |**params|
|
|
410
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
411
|
+
type: :comment_updated,
|
|
412
|
+
params: params,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
nil
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# @return [void]
|
|
420
|
+
def stub_project_class_methods!
|
|
421
|
+
project_mod = PlanMyStuff::Project
|
|
422
|
+
%i[list find].each { |m| save_original(project_mod, m) }
|
|
423
|
+
|
|
424
|
+
project_mod.define_singleton_method(:list) do |**params|
|
|
425
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
426
|
+
type: :projects_listed,
|
|
427
|
+
params: params,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
[]
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
project_mod.define_singleton_method(:find) do |number, **params|
|
|
434
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
435
|
+
type: :project_found,
|
|
436
|
+
params: { number: number }.merge(params),
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
PlanMyStuff::TestHelpers.build_project(number: number)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# @return [void]
|
|
444
|
+
def stub_project_item_class_methods!
|
|
445
|
+
item_mod = PlanMyStuff::ProjectItem
|
|
446
|
+
%i[move_item create! assign].each { |m| save_original(item_mod, m) }
|
|
447
|
+
|
|
448
|
+
item_mod.define_singleton_method(:move_item) do |**params|
|
|
449
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
450
|
+
type: :item_moved,
|
|
451
|
+
params: {
|
|
452
|
+
project: params[:project_number],
|
|
453
|
+
item_id: params[:item_id],
|
|
454
|
+
status: params[:status],
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
nil
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
item_mod.define_singleton_method(:create!) do |issue_or_title, **params|
|
|
461
|
+
title = issue_or_title.respond_to?(:title) ? issue_or_title.title : issue_or_title.to_s
|
|
462
|
+
number = issue_or_title.respond_to?(:number) ? issue_or_title.number : nil
|
|
463
|
+
|
|
464
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
465
|
+
type: :item_created,
|
|
466
|
+
params: {
|
|
467
|
+
project: params[:project_number],
|
|
468
|
+
title: title,
|
|
469
|
+
number: number,
|
|
470
|
+
draft: params[:draft] || false,
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
project = PlanMyStuff::TestHelpers.build_project(number: params[:project_number] || 1)
|
|
475
|
+
PlanMyStuff::ProjectItem.build(
|
|
476
|
+
{
|
|
477
|
+
id: "PVTI_fake_#{rand(10_000)}",
|
|
478
|
+
title: title,
|
|
479
|
+
number: number,
|
|
480
|
+
status: nil,
|
|
481
|
+
field_values: {},
|
|
482
|
+
},
|
|
483
|
+
project: project,
|
|
484
|
+
)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
item_mod.define_singleton_method(:assign) do |**params|
|
|
488
|
+
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
489
|
+
type: :item_assigned,
|
|
490
|
+
params: {
|
|
491
|
+
project: params[:project_number],
|
|
492
|
+
item_id: params[:item_id],
|
|
493
|
+
assignee: params[:assignee],
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
nil
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Resolves user objects from user objects or integer IDs, and extracts
|
|
5
|
+
# display names, IDs, and support status using configuration methods.
|
|
6
|
+
module UserResolver
|
|
7
|
+
class << self
|
|
8
|
+
# Resolves a user param (object or integer ID) into a user object.
|
|
9
|
+
#
|
|
10
|
+
# @param user [Object, Integer] user object or user_id integer
|
|
11
|
+
#
|
|
12
|
+
# @return [Object] the resolved user object
|
|
13
|
+
#
|
|
14
|
+
def resolve(user)
|
|
15
|
+
return if user.nil?
|
|
16
|
+
|
|
17
|
+
return user unless user.is_a?(Integer)
|
|
18
|
+
|
|
19
|
+
config = PlanMyStuff.configuration
|
|
20
|
+
klass = Object.const_get(config.user_class)
|
|
21
|
+
klass.find_by!(config.user_id_method => user)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Extracts the display name from a user object.
|
|
25
|
+
#
|
|
26
|
+
# @param user [Object] user object (not an integer ID)
|
|
27
|
+
#
|
|
28
|
+
# @return [String]
|
|
29
|
+
#
|
|
30
|
+
def display_name(user)
|
|
31
|
+
user.public_send(PlanMyStuff.configuration.display_name_method)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extracts the app-side user ID from a user object.
|
|
35
|
+
#
|
|
36
|
+
# @param user [Object] user object (not an integer ID)
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer]
|
|
39
|
+
#
|
|
40
|
+
def user_id(user)
|
|
41
|
+
user.public_send(PlanMyStuff.configuration.user_id_method)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Checks whether a user is support staff.
|
|
45
|
+
#
|
|
46
|
+
# @param user [Object] user object (not an integer ID)
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
#
|
|
50
|
+
def support?(user)
|
|
51
|
+
method = PlanMyStuff.configuration.support_method
|
|
52
|
+
|
|
53
|
+
if method.is_a?(Proc)
|
|
54
|
+
method.call(user)
|
|
55
|
+
else
|
|
56
|
+
user.public_send(method)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Verifies PlanMyStuff configuration by making API calls to GitHub.
|
|
5
|
+
# Checks token validity, org access, repo access, and project access.
|
|
6
|
+
class Verifier
|
|
7
|
+
Result = Struct.new(:name, :passed, :message)
|
|
8
|
+
|
|
9
|
+
# @return [Array<Result>]
|
|
10
|
+
attr_reader :results
|
|
11
|
+
|
|
12
|
+
# @return [Verifier]
|
|
13
|
+
def initialize
|
|
14
|
+
@results = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Runs all verification checks and returns self.
|
|
18
|
+
#
|
|
19
|
+
# @return [Verifier]
|
|
20
|
+
#
|
|
21
|
+
def run
|
|
22
|
+
check_token
|
|
23
|
+
check_organization
|
|
24
|
+
check_repos
|
|
25
|
+
check_default_project
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def passed?
|
|
31
|
+
results.all?(&:passed)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# @return [void]
|
|
37
|
+
def check_token
|
|
38
|
+
user = PlanMyStuff.client.rest(:user)
|
|
39
|
+
@results << Result.new('Token', true, "Authenticated as #{user.login}")
|
|
40
|
+
rescue => e
|
|
41
|
+
@results << Result.new('Token', false, e.message)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [void]
|
|
45
|
+
def check_organization
|
|
46
|
+
org = PlanMyStuff.configuration.organization
|
|
47
|
+
PlanMyStuff.client.rest(:organization, org)
|
|
48
|
+
@results << Result.new('Organization', true, "Access to #{org} confirmed")
|
|
49
|
+
rescue => e
|
|
50
|
+
@results << Result.new('Organization', false, "#{org}: #{e.message}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [void]
|
|
54
|
+
def check_repos
|
|
55
|
+
config = PlanMyStuff.configuration
|
|
56
|
+
|
|
57
|
+
if config.repos.empty? && config.default_repo.nil?
|
|
58
|
+
@results << Result.new('Repos', true, 'No repos configured (skipped)')
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
config.repos.each do |key, full_name|
|
|
63
|
+
check_single_repo(key, full_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return if config.default_repo.nil? || config.repos.key?(config.default_repo)
|
|
67
|
+
|
|
68
|
+
@results << Result.new(
|
|
69
|
+
'Default repo',
|
|
70
|
+
false,
|
|
71
|
+
"default_repo #{config.default_repo.inspect} not found in repos hash",
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [void]
|
|
76
|
+
def check_single_repo(key, full_name)
|
|
77
|
+
PlanMyStuff.client.rest(:repository, full_name)
|
|
78
|
+
@results << Result.new("Repo :#{key}", true, "Access to #{full_name} confirmed")
|
|
79
|
+
rescue => e
|
|
80
|
+
@results << Result.new("Repo :#{key}", false, "#{full_name}: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [void]
|
|
84
|
+
def check_default_project
|
|
85
|
+
project_number = PlanMyStuff.configuration.default_project_number
|
|
86
|
+
|
|
87
|
+
if project_number.nil?
|
|
88
|
+
@results << Result.new('Project', true, 'No default project configured (skipped)')
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
project = PlanMyStuff::Project.find(project_number)
|
|
93
|
+
@results << Result.new(
|
|
94
|
+
'Project',
|
|
95
|
+
true,
|
|
96
|
+
"Access to project ##{project_number} (#{project.title}) confirmed",
|
|
97
|
+
)
|
|
98
|
+
rescue => e
|
|
99
|
+
@results << Result.new('Project', false, "Project ##{project_number}: #{e.message}")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module VERSION
|
|
5
|
+
MAJOR = 0
|
|
6
|
+
MINOR = 1
|
|
7
|
+
TINY = 0
|
|
8
|
+
|
|
9
|
+
# Set PRE to nil unless it's a pre-release (beta, rc, etc.)
|
|
10
|
+
PRE = nil
|
|
11
|
+
|
|
12
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.').freeze
|
|
13
|
+
|
|
14
|
+
# :nodoc:
|
|
15
|
+
def self.to_s
|
|
16
|
+
STRING
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|