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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +291 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Guardfile +105 -0
- data/LICENSE.txt +21 -0
- data/README.md +159 -0
- data/Rakefile +20 -0
- data/bin/jira-auto-tool +57 -0
- data/bin/jira-auto-tool.bat +2 -0
- data/bin/setup +8 -0
- data/bin/setup-dev-win.bat +3 -0
- data/cucumber.yml +7 -0
- data/features/align_sprint_time_in_dates.feature +33 -0
- data/features/assign_tickets_to_team_sprints.feature +73 -0
- data/features/cache_boards.feature +24 -0
- data/features/control_http_request_rate_limit.feature +17 -0
- data/features/create_sprints_using_existing_ones_as_reference.feature +79 -0
- data/features/list_boards.feature +12 -0
- data/features/list_project_fields.feature +89 -0
- data/features/list_sprint_prefixes.feature +77 -0
- data/features/quarterly_add_sprints_using_existing_ones_as_a_reference.feature +71 -0
- data/features/quarterly_create_sprints_until_specific_date.feature +75 -0
- data/features/quarterly_rename_sprints.feature +179 -0
- data/features/rename_sprints.feature +203 -0
- data/features/self_documented_command_line.feature +15 -0
- data/features/sprint_filtering.feature +111 -0
- data/features/step_definitions/execution_context_steps.rb +33 -0
- data/features/step_definitions/jira_board_steps.rb +102 -0
- data/features/step_definitions/jira_ticket_steps.rb +63 -0
- data/features/support/10.setup_cucumber.rb +10 -0
- data/features/support/env.rb +25 -0
- data/features/support/hooks.rb +25 -0
- data/features/support/setup_rspec.rb +14 -0
- data/features/support/setup_simplecov.rb +5 -0
- data/features/update_sprint_end_date_and_shift_following_ones.feature +52 -0
- data/lib/jira/auto/tool/board/cache.rb +67 -0
- data/lib/jira/auto/tool/board/unavailable_board.rb +36 -0
- data/lib/jira/auto/tool/board.rb +105 -0
- data/lib/jira/auto/tool/board_controller/options.rb +32 -0
- data/lib/jira/auto/tool/board_controller.rb +88 -0
- data/lib/jira/auto/tool/common_options.rb +37 -0
- data/lib/jira/auto/tool/config/options.rb +19 -0
- data/lib/jira/auto/tool/config.rb +64 -0
- data/lib/jira/auto/tool/fetch_custom_field_options.rb +47 -0
- data/lib/jira/auto/tool/field.rb +59 -0
- data/lib/jira/auto/tool/field_controller.rb +50 -0
- data/lib/jira/auto/tool/field_option.rb +35 -0
- data/lib/jira/auto/tool/get_createmeta_for_project.rb +24 -0
- data/lib/jira/auto/tool/helpers/environment_based_value.rb +96 -0
- data/lib/jira/auto/tool/helpers/option_parser.rb +16 -0
- data/lib/jira/auto/tool/helpers/overridable_time.rb +18 -0
- data/lib/jira/auto/tool/helpers/pagination.rb +50 -0
- data/lib/jira/auto/tool/jira_http_options.rb +20 -0
- data/lib/jira/auto/tool/next_sprint_creator.rb +60 -0
- data/lib/jira/auto/tool/performer/options.rb +76 -0
- data/lib/jira/auto/tool/performer/planning_increment_sprint_creator.rb +42 -0
- data/lib/jira/auto/tool/performer/prefix_sprint_updater.rb +42 -0
- data/lib/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator.rb +60 -0
- data/lib/jira/auto/tool/performer/quarterly_sprint_renamer.rb +19 -0
- data/lib/jira/auto/tool/performer/sprint_end_date_updater.rb +55 -0
- data/lib/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator.rb +19 -0
- data/lib/jira/auto/tool/performer/sprint_renamer/next_name_generator.rb +47 -0
- data/lib/jira/auto/tool/performer/sprint_renamer.rb +55 -0
- data/lib/jira/auto/tool/performer/sprint_time_in_dates_aligner.rb +52 -0
- data/lib/jira/auto/tool/project/options.rb +22 -0
- data/lib/jira/auto/tool/project/ticket_fields.rb +70 -0
- data/lib/jira/auto/tool/project.rb +40 -0
- data/lib/jira/auto/tool/rate_limited_jira_client.rb +50 -0
- data/lib/jira/auto/tool/request_builder/field_context_fetcher.rb +78 -0
- data/lib/jira/auto/tool/request_builder/field_option_fetcher.rb +54 -0
- data/lib/jira/auto/tool/request_builder/get.rb +29 -0
- data/lib/jira/auto/tool/request_builder/sprint_creator.rb +112 -0
- data/lib/jira/auto/tool/request_builder/sprint_state_updater.rb +60 -0
- data/lib/jira/auto/tool/request_builder.rb +89 -0
- data/lib/jira/auto/tool/setup_logging.rb +35 -0
- data/lib/jira/auto/tool/sprint/name.rb +105 -0
- data/lib/jira/auto/tool/sprint/prefix.rb +66 -0
- data/lib/jira/auto/tool/sprint.rb +183 -0
- data/lib/jira/auto/tool/sprint_controller/options.rb +61 -0
- data/lib/jira/auto/tool/sprint_controller.rb +152 -0
- data/lib/jira/auto/tool/sprint_state_controller.rb +58 -0
- data/lib/jira/auto/tool/team.rb +23 -0
- data/lib/jira/auto/tool/team_sprint_prefix_mapper/options.rb +27 -0
- data/lib/jira/auto/tool/team_sprint_prefix_mapper.rb +62 -0
- data/lib/jira/auto/tool/team_sprint_ticket_dispatcher.rb +76 -0
- data/lib/jira/auto/tool/ticket.rb +110 -0
- data/lib/jira/auto/tool/until_date.rb +68 -0
- data/lib/jira/auto/tool/version.rb +9 -0
- data/lib/jira/auto/tool.rb +216 -0
- data/sig/jira/sprint/tool.rbs +8 -0
- data/spec/jira/auto/tool/board/cache_spec.rb +179 -0
- data/spec/jira/auto/tool/board/unavailable_board_spec.rb +34 -0
- data/spec/jira/auto/tool/board_controller/options_spec.rb +52 -0
- data/spec/jira/auto/tool/board_controller_spec.rb +154 -0
- data/spec/jira/auto/tool/board_spec.rb +163 -0
- data/spec/jira/auto/tool/common_options_spec.rb +49 -0
- data/spec/jira/auto/tool/config_spec.rb +108 -0
- data/spec/jira/auto/tool/field_controller_spec.rb +121 -0
- data/spec/jira/auto/tool/field_option_spec.rb +42 -0
- data/spec/jira/auto/tool/field_spec.rb +99 -0
- data/spec/jira/auto/tool/helpers/environment_based_value_spec.rb +21 -0
- data/spec/jira/auto/tool/helpers/option_parser_spec.rb +21 -0
- data/spec/jira/auto/tool/helpers/overridable_time_spec.rb +43 -0
- data/spec/jira/auto/tool/helpers/pagination_spec.rb +72 -0
- data/spec/jira/auto/tool/jira_http_options_spec.rb +32 -0
- data/spec/jira/auto/tool/next_sprint_creator_spec.rb +85 -0
- data/spec/jira/auto/tool/performer/option_spec.rb +55 -0
- data/spec/jira/auto/tool/performer/planning_increment_sprint_creator_spec.rb +62 -0
- data/spec/jira/auto/tool/performer/prefix_sprint_updater_spec.rb +35 -0
- data/spec/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator_spec.rb +175 -0
- data/spec/jira/auto/tool/performer/quarterly_sprint_renamer_spec.rb +239 -0
- data/spec/jira/auto/tool/performer/sprint_end_date_updater_spec.rb +90 -0
- data/spec/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator_spec.rb +12 -0
- data/spec/jira/auto/tool/performer/sprint_renamer/next_name_generator_spec.rb +129 -0
- data/spec/jira/auto/tool/performer/sprint_renamer_spec.rb +240 -0
- data/spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb +132 -0
- data/spec/jira/auto/tool/project/ticket_fields_spec.rb +390 -0
- data/spec/jira/auto/tool/project_spec.rb +31 -0
- data/spec/jira/auto/tool/rate_limited_jira_client_spec.rb +82 -0
- data/spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb +54 -0
- data/spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb +64 -0
- data/spec/jira/auto/tool/request_builder/get_spec.rb +40 -0
- data/spec/jira/auto/tool/request_builder/sprint_creator_spec.rb +179 -0
- data/spec/jira/auto/tool/request_builder/sprint_state_updater_spec.rb +31 -0
- data/spec/jira/auto/tool/request_builder_spec.rb +73 -0
- data/spec/jira/auto/tool/sprint/name_spec.rb +101 -0
- data/spec/jira/auto/tool/sprint/prefix_spec.rb +207 -0
- data/spec/jira/auto/tool/sprint_controller_spec.rb +406 -0
- data/spec/jira/auto/tool/sprint_spec.rb +309 -0
- data/spec/jira/auto/tool/team_spec.rb +21 -0
- data/spec/jira/auto/tool/team_sprint_prefix_mapper_spec.rb +97 -0
- data/spec/jira/auto/tool/team_sprint_ticket_dispatcher_spec.rb +232 -0
- data/spec/jira/auto/tool/ticket_spec.rb +116 -0
- data/spec/jira/auto/tool/until_date_spec.rb +80 -0
- data/spec/jira/auto/tool_spec.rb +458 -0
- data/spec/spec_helper.rb +42 -0
- 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
|