jira-auto-tool 0.1.1

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 (139) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +291 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/Guardfile +105 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +159 -0
  9. data/Rakefile +20 -0
  10. data/bin/jira-auto-tool +57 -0
  11. data/bin/jira-auto-tool.bat +2 -0
  12. data/bin/setup +8 -0
  13. data/bin/setup-dev-win.bat +3 -0
  14. data/cucumber.yml +7 -0
  15. data/features/align_sprint_time_in_dates.feature +33 -0
  16. data/features/assign_tickets_to_team_sprints.feature +73 -0
  17. data/features/cache_boards.feature +24 -0
  18. data/features/control_http_request_rate_limit.feature +17 -0
  19. data/features/create_sprints_using_existing_ones_as_reference.feature +79 -0
  20. data/features/list_boards.feature +12 -0
  21. data/features/list_project_fields.feature +89 -0
  22. data/features/list_sprint_prefixes.feature +77 -0
  23. data/features/quarterly_add_sprints_using_existing_ones_as_a_reference.feature +71 -0
  24. data/features/quarterly_create_sprints_until_specific_date.feature +75 -0
  25. data/features/quarterly_rename_sprints.feature +179 -0
  26. data/features/rename_sprints.feature +203 -0
  27. data/features/self_documented_command_line.feature +15 -0
  28. data/features/sprint_filtering.feature +111 -0
  29. data/features/step_definitions/execution_context_steps.rb +33 -0
  30. data/features/step_definitions/jira_board_steps.rb +102 -0
  31. data/features/step_definitions/jira_ticket_steps.rb +63 -0
  32. data/features/support/10.setup_cucumber.rb +10 -0
  33. data/features/support/env.rb +25 -0
  34. data/features/support/hooks.rb +25 -0
  35. data/features/support/setup_rspec.rb +14 -0
  36. data/features/support/setup_simplecov.rb +5 -0
  37. data/features/update_sprint_end_date_and_shift_following_ones.feature +52 -0
  38. data/lib/jira/auto/tool/board/cache.rb +67 -0
  39. data/lib/jira/auto/tool/board/unavailable_board.rb +36 -0
  40. data/lib/jira/auto/tool/board.rb +105 -0
  41. data/lib/jira/auto/tool/board_controller/options.rb +32 -0
  42. data/lib/jira/auto/tool/board_controller.rb +88 -0
  43. data/lib/jira/auto/tool/common_options.rb +37 -0
  44. data/lib/jira/auto/tool/config/options.rb +19 -0
  45. data/lib/jira/auto/tool/config.rb +64 -0
  46. data/lib/jira/auto/tool/fetch_custom_field_options.rb +47 -0
  47. data/lib/jira/auto/tool/field.rb +59 -0
  48. data/lib/jira/auto/tool/field_controller.rb +50 -0
  49. data/lib/jira/auto/tool/field_option.rb +35 -0
  50. data/lib/jira/auto/tool/get_createmeta_for_project.rb +24 -0
  51. data/lib/jira/auto/tool/helpers/environment_based_value.rb +96 -0
  52. data/lib/jira/auto/tool/helpers/option_parser.rb +16 -0
  53. data/lib/jira/auto/tool/helpers/overridable_time.rb +18 -0
  54. data/lib/jira/auto/tool/helpers/pagination.rb +50 -0
  55. data/lib/jira/auto/tool/jira_http_options.rb +20 -0
  56. data/lib/jira/auto/tool/next_sprint_creator.rb +60 -0
  57. data/lib/jira/auto/tool/performer/options.rb +76 -0
  58. data/lib/jira/auto/tool/performer/planning_increment_sprint_creator.rb +42 -0
  59. data/lib/jira/auto/tool/performer/prefix_sprint_updater.rb +42 -0
  60. data/lib/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator.rb +60 -0
  61. data/lib/jira/auto/tool/performer/quarterly_sprint_renamer.rb +19 -0
  62. data/lib/jira/auto/tool/performer/sprint_end_date_updater.rb +55 -0
  63. data/lib/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator.rb +19 -0
  64. data/lib/jira/auto/tool/performer/sprint_renamer/next_name_generator.rb +47 -0
  65. data/lib/jira/auto/tool/performer/sprint_renamer.rb +55 -0
  66. data/lib/jira/auto/tool/performer/sprint_time_in_dates_aligner.rb +52 -0
  67. data/lib/jira/auto/tool/project/options.rb +22 -0
  68. data/lib/jira/auto/tool/project/ticket_fields.rb +70 -0
  69. data/lib/jira/auto/tool/project.rb +40 -0
  70. data/lib/jira/auto/tool/rate_limited_jira_client.rb +50 -0
  71. data/lib/jira/auto/tool/request_builder/field_context_fetcher.rb +78 -0
  72. data/lib/jira/auto/tool/request_builder/field_option_fetcher.rb +54 -0
  73. data/lib/jira/auto/tool/request_builder/get.rb +29 -0
  74. data/lib/jira/auto/tool/request_builder/sprint_creator.rb +112 -0
  75. data/lib/jira/auto/tool/request_builder/sprint_state_updater.rb +60 -0
  76. data/lib/jira/auto/tool/request_builder.rb +89 -0
  77. data/lib/jira/auto/tool/setup_logging.rb +35 -0
  78. data/lib/jira/auto/tool/sprint/name.rb +105 -0
  79. data/lib/jira/auto/tool/sprint/prefix.rb +66 -0
  80. data/lib/jira/auto/tool/sprint.rb +183 -0
  81. data/lib/jira/auto/tool/sprint_controller/options.rb +61 -0
  82. data/lib/jira/auto/tool/sprint_controller.rb +152 -0
  83. data/lib/jira/auto/tool/sprint_state_controller.rb +58 -0
  84. data/lib/jira/auto/tool/team.rb +23 -0
  85. data/lib/jira/auto/tool/team_sprint_prefix_mapper/options.rb +27 -0
  86. data/lib/jira/auto/tool/team_sprint_prefix_mapper.rb +62 -0
  87. data/lib/jira/auto/tool/team_sprint_ticket_dispatcher.rb +76 -0
  88. data/lib/jira/auto/tool/ticket.rb +110 -0
  89. data/lib/jira/auto/tool/until_date.rb +68 -0
  90. data/lib/jira/auto/tool/version.rb +9 -0
  91. data/lib/jira/auto/tool.rb +216 -0
  92. data/sig/jira/sprint/tool.rbs +8 -0
  93. data/spec/jira/auto/tool/board/cache_spec.rb +179 -0
  94. data/spec/jira/auto/tool/board/unavailable_board_spec.rb +34 -0
  95. data/spec/jira/auto/tool/board_controller/options_spec.rb +52 -0
  96. data/spec/jira/auto/tool/board_controller_spec.rb +154 -0
  97. data/spec/jira/auto/tool/board_spec.rb +163 -0
  98. data/spec/jira/auto/tool/common_options_spec.rb +49 -0
  99. data/spec/jira/auto/tool/config_spec.rb +108 -0
  100. data/spec/jira/auto/tool/field_controller_spec.rb +121 -0
  101. data/spec/jira/auto/tool/field_option_spec.rb +42 -0
  102. data/spec/jira/auto/tool/field_spec.rb +99 -0
  103. data/spec/jira/auto/tool/helpers/environment_based_value_spec.rb +21 -0
  104. data/spec/jira/auto/tool/helpers/option_parser_spec.rb +21 -0
  105. data/spec/jira/auto/tool/helpers/overridable_time_spec.rb +43 -0
  106. data/spec/jira/auto/tool/helpers/pagination_spec.rb +72 -0
  107. data/spec/jira/auto/tool/jira_http_options_spec.rb +32 -0
  108. data/spec/jira/auto/tool/next_sprint_creator_spec.rb +85 -0
  109. data/spec/jira/auto/tool/performer/option_spec.rb +55 -0
  110. data/spec/jira/auto/tool/performer/planning_increment_sprint_creator_spec.rb +62 -0
  111. data/spec/jira/auto/tool/performer/prefix_sprint_updater_spec.rb +35 -0
  112. data/spec/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator_spec.rb +175 -0
  113. data/spec/jira/auto/tool/performer/quarterly_sprint_renamer_spec.rb +239 -0
  114. data/spec/jira/auto/tool/performer/sprint_end_date_updater_spec.rb +90 -0
  115. data/spec/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator_spec.rb +12 -0
  116. data/spec/jira/auto/tool/performer/sprint_renamer/next_name_generator_spec.rb +129 -0
  117. data/spec/jira/auto/tool/performer/sprint_renamer_spec.rb +240 -0
  118. data/spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb +132 -0
  119. data/spec/jira/auto/tool/project/ticket_fields_spec.rb +390 -0
  120. data/spec/jira/auto/tool/project_spec.rb +31 -0
  121. data/spec/jira/auto/tool/rate_limited_jira_client_spec.rb +82 -0
  122. data/spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb +54 -0
  123. data/spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb +64 -0
  124. data/spec/jira/auto/tool/request_builder/get_spec.rb +40 -0
  125. data/spec/jira/auto/tool/request_builder/sprint_creator_spec.rb +179 -0
  126. data/spec/jira/auto/tool/request_builder/sprint_state_updater_spec.rb +31 -0
  127. data/spec/jira/auto/tool/request_builder_spec.rb +73 -0
  128. data/spec/jira/auto/tool/sprint/name_spec.rb +101 -0
  129. data/spec/jira/auto/tool/sprint/prefix_spec.rb +207 -0
  130. data/spec/jira/auto/tool/sprint_controller_spec.rb +406 -0
  131. data/spec/jira/auto/tool/sprint_spec.rb +309 -0
  132. data/spec/jira/auto/tool/team_spec.rb +21 -0
  133. data/spec/jira/auto/tool/team_sprint_prefix_mapper_spec.rb +97 -0
  134. data/spec/jira/auto/tool/team_sprint_ticket_dispatcher_spec.rb +232 -0
  135. data/spec/jira/auto/tool/ticket_spec.rb +116 -0
  136. data/spec/jira/auto/tool/until_date_spec.rb +80 -0
  137. data/spec/jira/auto/tool_spec.rb +458 -0
  138. data/spec/spec_helper.rb +42 -0
  139. metadata +368 -0
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ module Auto
5
+ # rubocop:disable Metrics/ClassLength
6
+ class Tool
7
+ # rubocop:disable RSpec/NestedGroups
8
+ RSpec.describe Sprint do
9
+ let(:jira_client) { instance_double(JIRA::Client) }
10
+ let(:tool) { instance_double(Tool, jira_client: jira_client) }
11
+ let(:jira_sprint) do
12
+ jira_resource_double(JIRA::Resource::Sprint,
13
+ id: 40_820,
14
+ name: "Food_Supply_24.4.5",
15
+ startDate: "2024-12-27 13:00 UTC", endDate: "2024-12-31 13:00 UTC",
16
+ state: state, originBoardId: 4096,
17
+ client: jira_client,
18
+ attrs: {})
19
+ end
20
+
21
+ let(:state) { "future" }
22
+
23
+ context "when using its attributes" do
24
+ let(:sprint) do
25
+ described_class.new(tool, jira_sprint)
26
+ end
27
+
28
+ describe "#jira_client" do
29
+ it { expect(sprint.jira_client).to eq(jira_client) }
30
+ end
31
+
32
+ describe "#id" do
33
+ it { expect(sprint.id).to eq(40_820) }
34
+ end
35
+
36
+ describe "#name" do
37
+ it { expect(sprint.name).to eq("Food_Supply_24.4.5") }
38
+ end
39
+
40
+ describe "#length_in_days" do
41
+ it { expect(sprint.length_in_days).to eq(4) }
42
+ end
43
+
44
+ shared_examples "an optional date" do |date_method, jira_field:, expected_value:|
45
+ it { expect(sprint.send(date_method)).to eq(Time.parse(expected_value).utc) }
46
+ it { expect(sprint.send("#{date_method}?")).to be_truthy }
47
+
48
+ context "when the sprint has no date" do
49
+ before do
50
+ allow(jira_sprint).to receive(:respond_to?).with(jira_field).and_return(false)
51
+ end
52
+
53
+ it("handles missing date information") { expect(sprint.send(date_method)).to eq(Sprint::UNDEFINED_DATE) }
54
+
55
+ it { expect(sprint.send("#{date_method}?")).to be_falsy }
56
+ end
57
+ end
58
+
59
+ describe "#start_date" do
60
+ it_behaves_like "an optional date",
61
+ :start_date, jira_field: :startDate, expected_value: "2024-12-27 13:00 UTC"
62
+ end
63
+
64
+ describe "#start_date=" do
65
+ it "can set the date attribute" do
66
+ sprint.start_date = "2025-02-08 12:45 +0100"
67
+
68
+ expect(jira_sprint.attrs["startDate"]).to eq("2025-02-08T11:45:00Z")
69
+ end
70
+ end
71
+
72
+ describe "#end_date=" do
73
+ it "can set the date attribute" do
74
+ sprint.end_date = "2025-02-08 12:45 +0100"
75
+
76
+ expect(jira_sprint.attrs["endDate"]).to eq("2025-02-08T11:45:00Z")
77
+ end
78
+ end
79
+
80
+ describe "#end_date" do
81
+ it_behaves_like "an optional date",
82
+ :end_date, jira_field: :endDate, expected_value: "2024-12-31 13:00 UTC"
83
+ end
84
+
85
+ describe "#missing_dates?" do
86
+ def new_sprint(start_date: Sprint::UNDEFINED_DATE, end_date: Sprint::UNDEFINED_DATE)
87
+ allow(sprint).to receive_messages(start_date: start_date, end_date: end_date)
88
+
89
+ sprint
90
+ end
91
+
92
+ it { expect(new_sprint).to be_missing_dates }
93
+ it { expect(new_sprint(start_date: Time.now)).to be_missing_dates }
94
+ it { expect(new_sprint(end_date: Time.now.tomorrow)).to be_missing_dates }
95
+
96
+ it { expect(new_sprint(start_date: Time.now, end_date: Time.now + 14.days)).not_to be_missing_dates }
97
+ end
98
+
99
+ describe "#state" do
100
+ it { expect(sprint.state).to eq("future") }
101
+ end
102
+
103
+ describe "#closed?" do
104
+ context "when the sprint state is closed" do
105
+ let(:state) { "closed" }
106
+
107
+ it { expect(sprint).to be_closed }
108
+ end
109
+
110
+ context "when the sprint state is active" do
111
+ let(:state) { "active" }
112
+
113
+ it { expect(sprint).not_to be_closed }
114
+ end
115
+
116
+ context "when the sprint state is future" do
117
+ let(:state) { "future" }
118
+
119
+ it { expect(sprint).not_to be_closed }
120
+ end
121
+ end
122
+
123
+ describe "#origin_board_id" do
124
+ it { expect(sprint.origin_board_id).to eq(4096) }
125
+ end
126
+
127
+ describe "#board" do
128
+ let(:expected_board) { instance_double(Board, name: "Food_Supply_24.4.5") }
129
+
130
+ before do
131
+ allow(sprint).to receive(:origin_board_id).and_return(8192)
132
+
133
+ allow(Board).to receive(:find_by_id).with(tool, 8192).and_return(expected_board)
134
+ end
135
+
136
+ it { expect(sprint.board).to eq(expected_board) }
137
+ end
138
+
139
+ describe "#renamed_to" do
140
+ let(:jira_sprint) { jira_resource_double(JIRA::Resource::Sprint, name: "Food_Supply_24.4.5", state: state) }
141
+ let(:state) { "future" }
142
+
143
+ before do
144
+ allow(jira_sprint).to receive_messages(save!: nil)
145
+ end
146
+
147
+ it "saves the sprint with the new name" do
148
+ allow(sprint).to receive_messages(jira_sprint: jira_sprint)
149
+ attrs = { name: "Food_Supply_25.4.1" }
150
+ allow(jira_sprint).to receive_messages(attrs: attrs, save!: nil)
151
+ allow(attrs).to receive(:[]=).with("name", "Food_Supply_25.4.1")
152
+
153
+ sprint.rename_to("Food_Supply_25.4.1")
154
+
155
+ expect(jira_sprint).to have_received(:save!)
156
+ end
157
+
158
+ it "ignores renaming to the same name" do
159
+ sprint.rename_to("Food_Supply_24.4.5")
160
+
161
+ expect(jira_sprint).not_to have_received(:save!)
162
+ end
163
+
164
+ context "when the sprint is closed" do
165
+ let(:state) { "closed" }
166
+
167
+ it "ignores renaming a closed sprint" do
168
+ sprint.rename_to("Food_Supply_25.4.1")
169
+
170
+ expect(jira_sprint).not_to have_received(:save!)
171
+ end
172
+ end
173
+ end
174
+
175
+ describe "#save" do
176
+ it "saves the sprint" do
177
+ allow(jira_sprint).to receive_messages(save!: nil, attrs: { "name" => "Food_Supply_25.4.1" })
178
+
179
+ # allow(sprint).to receive_messages()
180
+
181
+ sprint.save
182
+
183
+ expect(jira_sprint).to have_received(:save!)
184
+ end
185
+ end
186
+
187
+ describe "#to_table_row" do
188
+ let(:board) do
189
+ instance_double(Board, to_table_row: ["Food Supply Team Board", :url_to_suppply_team_board, "FOOD"])
190
+ end
191
+
192
+ before do
193
+ allow(sprint).to receive_messages(board: board)
194
+ end
195
+
196
+ it do
197
+ expect(sprint.to_table_row)
198
+ .to eq([40_820, "Food_Supply_24.4.5", 4, "2024-12-27 13:00 UTC", "2024-12-31 13:00 UTC",
199
+ "Food Supply Team Board", :url_to_suppply_team_board, "FOOD"])
200
+ end
201
+
202
+ it "can exclude the board information" do
203
+ expect(sprint.to_table_row(without_board_information: true))
204
+ .to eq([40_820, "Food_Supply_24.4.5", 4, "2024-12-27 13:00 UTC", "2024-12-31 13:00 UTC"])
205
+ end
206
+ end
207
+
208
+ describe ".to_table_row_field_names" do
209
+ it {
210
+ expect(described_class.to_table_row_field_names).to eq(%i[id name length_in_days start_date end_date])
211
+ }
212
+ end
213
+
214
+ describe ".to_table_row_header" do
215
+ before do
216
+ allow(Board).to receive_messages(to_table_row_header: ["Name", "UI URL", "Project Key"])
217
+ end
218
+
219
+ it do
220
+ expect(described_class.to_table_row_header)
221
+ .to eq(["Id", "Name", "Length In Days", "Start Date", "End Date",
222
+ "Board Name", "Board UI URL", "Board Project Key"])
223
+ end
224
+
225
+ it "can exclude the board information" do
226
+ expect(described_class.to_table_row_header(without_board_information: true))
227
+ .to eq(["Id", "Name", "Length In Days", "Start Date", "End Date"])
228
+ end
229
+ end
230
+ end
231
+
232
+ describe "#<=>" do
233
+ def new_sprint_named(name, start_date: "2024-12-30 13:00 UTC", end_date: "2025-01-14 13:00 UTC")
234
+ # rubocop:disable RSpec/VerifiedDoubles
235
+ described_class.new(
236
+ tool,
237
+ double(JIRA::Resource::Sprint, name: name, startDate: start_date, endDate: end_date,
238
+ originBoardId: 2048, inspect: name.inspect)
239
+ )
240
+ # rubocop:enable RSpec/VerifiedDoubles
241
+ end
242
+
243
+ let(:abc_sprint) { new_sprint_named("abc name_24.4.9") }
244
+ let(:sprint_with_missing_date) do
245
+ new_sprint_named("name that should not be last_24.4.9",
246
+ end_date: Sprint::UNDEFINED_DATE.utc.to_s)
247
+ end
248
+
249
+ it { expect(sprint_with_missing_date).to be < abc_sprint }
250
+
251
+ it { expect(abc_sprint).to eq new_sprint_named("abc name_24.4.9") }
252
+ it { expect(abc_sprint).to be < new_sprint_named("xyz name_24.4.9") }
253
+ it { expect(new_sprint_named("def name_24.3.9")).to be > abc_sprint }
254
+
255
+ it { expect(new_sprint_named("foo_bar_24.4.9")).to be < new_sprint_named("foo_bar_24.4.10") }
256
+
257
+ it do
258
+ expect(new_sprint_named("foo_bar_24.4.9", end_date: "2025-01-14 13:00 UTC"))
259
+ .to be < new_sprint_named("foo_bar_24.4.9", end_date: "2025-01-15 13:00 UTC")
260
+ end
261
+
262
+ context "when sprint name does not respect the naming convention" do
263
+ it { expect(abc_sprint).to be > new_sprint_named("1st sprint") }
264
+ it { expect(abc_sprint).to be < new_sprint_named("name not following the naming conventions") }
265
+ end
266
+ end
267
+
268
+ describe "#comparison_fields" do
269
+ let(:sprint) { described_class.new(tool, jira_sprint) }
270
+
271
+ before do
272
+ allow(Sprint::Name).to receive_messages(respects_naming_convention?: respects_naming_convention)
273
+ end
274
+
275
+ context "when respecting the naming convention" do
276
+ let(:respects_naming_convention) { true }
277
+
278
+ it { expect(sprint.send(:comparison_fields, sprint)).to eq(%i[start_date end_date parsed_name]) }
279
+ end
280
+
281
+ context "when not respecting the naming convention" do
282
+ let(:respects_naming_convention) { false }
283
+
284
+ it { expect(sprint.send(:comparison_fields, sprint)).to eq(%i[start_date end_date name]) }
285
+ end
286
+ end
287
+
288
+ describe ".date_for_save" do
289
+ let(:now) { Time.now }
290
+
291
+ it { expect(described_class.date_for_save(now)).to eq(now.utc.iso8601) }
292
+
293
+ it do
294
+ expect(described_class.date_for_save("2025-02-08 15:15:15 +0100").force_encoding("UTF-8"))
295
+ .to eq("2025-02-08T14:15:15Z")
296
+ end
297
+
298
+ it do
299
+ expect { described_class.date_for_save(nil) }
300
+ .to raise_error(ArgumentError, "nil (NilClass), date must be a Time, Date, DateTime or a String")
301
+ end
302
+ end
303
+ end
304
+ # rubocop:enable RSpec/NestedGroups
305
+ end
306
+
307
+ # rubocop:enable Metrics/ClassLength
308
+ end
309
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+
5
+ require "jira/auto/tool/team"
6
+
7
+ module Jira
8
+ module Auto
9
+ class Tool
10
+ class Team
11
+ RSpec.describe Team do
12
+ let(:team) { described_class.new(field_option) }
13
+ let(:field_option) { instance_double(FieldOption, id: 123, value: "Test Team") }
14
+
15
+ it { expect(team.name).to eq("Test Team") }
16
+ it { expect(team.id).to eq(123) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class TeamSprintPrefixMapper
9
+ RSpec.describe TeamSprintPrefixMapper do
10
+ let(:mapper) do
11
+ described_class.new(teams, unclosed_sprint_prefixes)
12
+ end
13
+
14
+ let(:teams) { ["A16 CRM", "A16 Logistic", "A16 Platform", "A32 64 Sys-Team"] }
15
+
16
+ let(:expected_team_sprint_prefix_mappings) do
17
+ {
18
+ "A16 CRM" => "ART-16_CRM",
19
+ "A16 Platform" => "ART-16_Platform",
20
+ "A32 64 Sys-Team" => "ART-32-64_Sys-Team"
21
+ }
22
+ end
23
+
24
+ let(:unclosed_sprint_prefixes) do
25
+ %w[ART-16_CRM ART-16_Platform ART-32-64_Sys-Team].collect do |name|
26
+ instance_double(Sprint::Prefix, name: name)
27
+ end
28
+ end
29
+
30
+ describe "#sprint_prefixes" do
31
+ it { expect(mapper.sprint_prefixes).to eq unclosed_sprint_prefixes }
32
+ end
33
+
34
+ describe "#teams" do
35
+ it { expect(mapper.teams).to eq teams }
36
+ end
37
+
38
+ describe "#team_sprint_prefix_mappings" do
39
+ it { expect(mapper.team_sprint_prefix_mappings).to eq(expected_team_sprint_prefix_mappings) }
40
+ end
41
+
42
+ describe "#fetch_for" do
43
+ it { expect(mapper.fetch_for("A16 CRM")).to eq "ART-16_CRM" }
44
+ it { expect(mapper.fetch_for("A32 64 Sys-Team")).to eq "ART-32-64_Sys-Team" }
45
+
46
+ it "returns nil if the prefix is not found" do
47
+ expect(mapper.fetch_for("team with no related prefix")).to be_nil
48
+ end
49
+ end
50
+
51
+ describe "#fetch_for!" do
52
+ it "raises an error if the prefix is not found" do
53
+ expect { mapper.fetch_for!("team with no related prefix") }
54
+ .to raise_error(NoMatchingSprintPrefixError,
55
+ /#{Regexp.escape(
56
+ "No matching sprint prefix for team 'team with no related prefix' in "
57
+ )}
58
+ #{Regexp.escape(expected_team_sprint_prefix_mappings.inspect)}/x)
59
+ end
60
+ end
61
+
62
+ describe "#map_prefix_name_to_team_name" do
63
+ it { expect(mapper.map_prefix_name_to_team_name("ART-16_CRM")).to eq "A16 CRM" }
64
+ it { expect(mapper.map_prefix_name_to_team_name("ART-32-64_Sys-Team")).to eq "A32 64 Sys-Team" }
65
+
66
+ it "raises an error if the prefix is not found" do
67
+ expect { mapper.map_prefix_name_to_team_name("team-unrelated-prefix") }.not_to raise_error
68
+ end
69
+ end
70
+
71
+ describe "#list_mappings" do
72
+ let(:expected_mapping_output) do
73
+ <<~EOMAPPING
74
+ +-----------------------------------------------------+
75
+ | Team Sprint Mappings |
76
+ +-----------------+-----------------------------------+
77
+ | Team | Sprint Prefix |
78
+ +-----------------+-----------------------------------+
79
+ | A16 CRM | ART-16_CRM |
80
+ | A16 Logistic | !!__no matching sprint prefix__!! |
81
+ | A16 Platform | ART-16_Platform |
82
+ | A32 64 Sys-Team | ART-32-64_Sys-Team |
83
+ +-----------------+-----------------------------------+
84
+ EOMAPPING
85
+ end
86
+
87
+ it "lists the mappings" do
88
+ allow(mapper).to receive_messages(team_sprint_prefix_mappings: expected_team_sprint_prefix_mappings)
89
+
90
+ expect { mapper.list_mappings }.to output(expected_mapping_output).to_stdout
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+ require "jira/auto/tool/team_sprint_ticket_dispatcher"
5
+
6
+ module Jira
7
+ module Auto
8
+ class Tool
9
+ # rubocop:disable Metrics/ClassLength, RSpec/MultipleMemoizedHelpers
10
+ class TeamSprintTicketDispatcher
11
+ RSpec.describe TeamSprintTicketDispatcher do
12
+ let(:jira_client) { instance_double(JIRA::Client) }
13
+ let(:ticket_for_1st_team) { build_ticket("Ticket1", "Team1", key: "ART-10040") }
14
+ let(:ticket_for_2nd_team) { build_ticket("Ticket2", "Team2", key: "ART-10041") }
15
+ let(:another_ticket_for_1st_team) { build_ticket("Ticket3", "Team1", key: "ART-10042") }
16
+ let(:another_ticket_for_2nd_team) { build_ticket("Ticket4", "Team2", key: "ART-10043") }
17
+ let(:tickets) do
18
+ [ticket_for_1st_team, ticket_for_2nd_team, another_ticket_for_1st_team, another_ticket_for_2nd_team]
19
+ end
20
+ let(:first_team) { "Team1" }
21
+ let(:second_team) { "Team2" }
22
+
23
+ def build_ticket(summary, implementation_team, expected_start_date = "undefined")
24
+ instance_double(Ticket,
25
+ summary: summary,
26
+ implementation_team: implementation_team,
27
+ expected_start_date: expected_start_date)
28
+ end
29
+
30
+ def build_sprint_prefix(name, sprints = [])
31
+ instance_double(Sprint::Prefix, name: name, sprints: sprints)
32
+ end
33
+
34
+ context "when dispatching tickets" do
35
+ let(:dispatcher) do
36
+ described_class.new(jira_client, tickets, sprint_prefixes)
37
+ end
38
+
39
+ let(:sprint_prefix_for_1st_team) { build_sprint_prefix("ART_Team1") }
40
+ let(:sprint_prefix_for_2nd_team) { build_sprint_prefix("ART_Team2") }
41
+ let(:sprint_prefixes) { [sprint_prefix_for_1st_team, sprint_prefix_for_2nd_team] }
42
+
43
+ describe "#teams" do
44
+ it { expect(dispatcher.teams).to eq(%w[Team1 Team2]) }
45
+ end
46
+
47
+ describe "#team_sprint_prefix_mapper" do
48
+ it { expect(dispatcher.team_sprint_prefix_mapper).to be_a(TeamSprintPrefixMapper) }
49
+ end
50
+
51
+ describe "#sprint_prefix_for" do
52
+ it { expect(dispatcher.sprint_prefix_for(first_team)).to eq("ART_Team1") }
53
+ it { expect(dispatcher.sprint_prefix_for(second_team)).to eq("ART_Team2") }
54
+ it { expect(dispatcher.sprint_prefix_for("team w/o sprints")).to be_nil }
55
+ end
56
+
57
+ describe "#dispatch_tickets" do
58
+ before do
59
+ # allow(team_sprint_prefix_mapper).to receive(:fetch_for).with("Team1").once.and_return("ART_Team1")
60
+ # allow(team_sprint_prefix_mapper).to receive(:fetch_for).with("Team2").once.and_return("ART_Team2")
61
+
62
+ allow(ticket_for_1st_team).to receive_messages(key: "ART-10040")
63
+ allow(another_ticket_for_1st_team).to receive_messages(key: "ART-10042")
64
+
65
+ allow(ticket_for_2nd_team).to receive_messages(key: "ART-10042")
66
+ allow(another_ticket_for_2nd_team).to receive_messages(key: "ART-10044")
67
+
68
+ allow(dispatcher).to receive_messages(dispatch_tickets_to_prefix_sprints: nil)
69
+ end
70
+
71
+ it "dispatches 1st team tickets to the expected prefix" do
72
+ dispatcher.dispatch_tickets
73
+
74
+ expect(dispatcher)
75
+ .to have_received(:dispatch_tickets_to_prefix_sprints).with("ART_Team1",
76
+ [ticket_for_1st_team,
77
+ another_ticket_for_1st_team]).once
78
+ end
79
+
80
+ it "dispatches 2nd team tickets to the expected prefix" do
81
+ dispatcher.dispatch_tickets
82
+
83
+ expect(dispatcher)
84
+ .to have_received(:dispatch_tickets_to_prefix_sprints).with("ART_Team2",
85
+ [ticket_for_2nd_team,
86
+ another_ticket_for_2nd_team]).once
87
+ end
88
+ end
89
+
90
+ describe "#dispatch_tickets_to_prefix_sprints" do
91
+ before do
92
+ allow(ticket_for_1st_team).to receive_messages(:sprint= => nil)
93
+ allow(another_ticket_for_1st_team).to receive_messages(:sprint= => nil)
94
+ end
95
+
96
+ let(:ticket_planned_after_last_available_sprint) { build_ticket("Ticket5", "Team1") }
97
+
98
+ it "updates each ticket with matched sprint" do
99
+ allow(dispatcher).to receive(:match_ticket_to_prefix_sprint)
100
+ .with(sprint_prefix_for_1st_team, ticket_for_1st_team).and_return(:first_sprint)
101
+
102
+ allow(dispatcher).to receive(:match_ticket_to_prefix_sprint)
103
+ .with(sprint_prefix_for_1st_team, another_ticket_for_1st_team).and_return(:second_sprint)
104
+
105
+ dispatcher.dispatch_tickets_to_prefix_sprints("ART_Team1",
106
+ [ticket_for_1st_team,
107
+ another_ticket_for_1st_team])
108
+
109
+ expect(ticket_for_1st_team).to have_received(:sprint=).with(:first_sprint)
110
+ expect(another_ticket_for_1st_team).to have_received(:sprint=).with(:second_sprint)
111
+ end
112
+
113
+ it "does not update tickets that do not match a sprint" do
114
+ allow(ticket_planned_after_last_available_sprint).to receive_messages(:sprint= => nil)
115
+
116
+ allow(dispatcher).to receive(:match_ticket_to_prefix_sprint)
117
+ .with(sprint_prefix_for_1st_team, ticket_planned_after_last_available_sprint)
118
+ .and_return(nil)
119
+
120
+ dispatcher.dispatch_tickets_to_prefix_sprints("ART_Team1",
121
+ [ticket_planned_after_last_available_sprint])
122
+
123
+ expect(ticket_planned_after_last_available_sprint).not_to have_received(:sprint=)
124
+ end
125
+ end
126
+ end
127
+
128
+ context "when iterating over tickets" do
129
+ let(:dispatcher) { described_class.new(jira_client, tickets, nil) }
130
+
131
+ describe "#per_team_tickets" do
132
+ it "succeeds" do
133
+ expect { |block| dispatcher.per_team_tickets(&block) }
134
+ .to yield_successive_args(
135
+ [first_team, [ticket_for_1st_team, another_ticket_for_1st_team]],
136
+ [second_team, [ticket_for_2nd_team, another_ticket_for_2nd_team]]
137
+ )
138
+ end
139
+ end
140
+ end
141
+
142
+ context "when matching tickets to prefix sprints" do
143
+ let(:dispatcher) { described_class.new(jira_client, nil, nil) }
144
+
145
+ describe "#dispatch_to_prefix" do
146
+ RSpec::Matchers.define :be_dispatched_to_sprint do |expected_sprint|
147
+ match do |ticket|
148
+ matched_sprint = dispatcher.match_ticket_to_prefix_sprint(sprint_prefix, ticket)
149
+
150
+ log.debug do
151
+ <<-EOMATCHINFO
152
+ ticket: #{ticket.expected_start_date}
153
+ matched sprint: #{matched_sprint&.name} #{matched_sprint&.start_date}" }
154
+ expected sprint: #{expected_sprint&.name} #{expected_sprint&.start_date}"
155
+ EOMATCHINFO
156
+ end
157
+
158
+ matched_sprint && matched_sprint.name == expected_sprint.name
159
+ end
160
+
161
+ failure_message do |ticket|
162
+ build_message(expected_sprint, ticket,
163
+ "but it did not match any sprint or matched an incorrect sprint")
164
+ end
165
+
166
+ failure_message_when_negated do |ticket_date|
167
+ build_message(expected_sprint, ticket_date,
168
+ "but it matched the expected sprint",
169
+ "not ")
170
+ end
171
+
172
+ def build_message(expected_sprint, ticket, message, condition = "")
173
+ "expected ticket with start date #{ticket.expected_start_date} #{condition}to dispatch to sprint " \
174
+ "#{expected_sprint} (name = #{expected_sprint.name}, " \
175
+ "start date = #{expected_sprint.start_date}), " +
176
+ message
177
+ end
178
+ end
179
+
180
+ def build_sprint(name, start_date, end_date)
181
+ instance_double(Sprint,
182
+ name: name,
183
+ start_date: Time.parse(start_date),
184
+ end_date: Time.parse(end_date))
185
+ end
186
+
187
+ def ticket_due_to_start_on(start_date)
188
+ build_ticket("ticket_name for #{start_date}", "Team1", start_date)
189
+ end
190
+
191
+ let(:sprint_prefix) do
192
+ build_sprint_prefix("a sprint prefix",
193
+ [active_sprint, coming_sprint, next_sprint, last_available_sprint])
194
+ end
195
+
196
+ let(:active_sprint) { build_sprint("sprint1", "2025-01-01", "2025-01-04") }
197
+ let(:coming_sprint) { build_sprint("sprint2", "2025-01-04", "2025-01-07") }
198
+ let(:next_sprint) { build_sprint("sprint3", "2025-01-07", "2025-01-10") }
199
+ let(:last_available_sprint) { build_sprint("sprint4", "2025-01-10", "2025-01-13") }
200
+
201
+ let(:overdue_ticket) { ticket_due_to_start_on("2024-12-25") }
202
+ let(:ticket_to_start_at_beginning_of_coming_sprint) { ticket_due_to_start_on("2025-01-04") }
203
+ let(:ticket_to_start_at_end_of_coming_sprint) { ticket_due_to_start_on("2025-01-07") }
204
+ let(:ticket_planned_after_last_available_sprint) { ticket_due_to_start_on("2025-01-14") }
205
+
206
+ it { expect(overdue_ticket).to be_dispatched_to_sprint(active_sprint) }
207
+
208
+ it { expect(ticket_to_start_at_beginning_of_coming_sprint).to be_dispatched_to_sprint(coming_sprint) }
209
+
210
+ it { expect(ticket_to_start_at_end_of_coming_sprint).not_to be_dispatched_to_sprint(coming_sprint) }
211
+ it { expect(ticket_to_start_at_end_of_coming_sprint).to be_dispatched_to_sprint(next_sprint) }
212
+
213
+ it do
214
+ expect(ticket_planned_after_last_available_sprint)
215
+ .not_to be_dispatched_to_sprint(last_available_sprint)
216
+ end
217
+
218
+ it "does not match a ticket planned after the last sprint" do
219
+ expect(dispatcher.match_ticket_to_prefix_sprint(
220
+ sprint_prefix, ticket_planned_after_last_available_sprint
221
+ ))
222
+ .to be_nil
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ # rubocop:enable Metrics/ClassLength, RSpec/MultipleMemoizedHelpers
230
+ end
231
+ end
232
+ end