ticket-replicator 1.1.0 → 1.2.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 +4 -4
- data/.ruby-version +1 -0
- data/README.md +2 -1
- data/config/examples/ticket-replicator.mappings.yml +3 -1
- data/features/load_tickets_in_jira.feature +20 -3
- data/features/setup_ticket_replicator.feature +3 -1
- data/features/transform-solution-manager-tickets-into-jira-loadable-tickets.feature +56 -1
- data/lib/ticket/replicator/file_transformer.rb +1 -0
- data/lib/ticket/replicator/row_loader.rb +31 -23
- data/lib/ticket/replicator/row_transformer.rb +24 -2
- data/lib/ticket/replicator/version.rb +1 -1
- data/spec/ticket/replicator/file_transformer_spec.rb +3 -5
- data/spec/ticket/replicator/row_loader_spec.rb +126 -39
- data/spec/ticket/replicator/row_transformer_spec.rb +268 -228
- metadata +2 -1
@@ -2,300 +2,340 @@
|
|
2
2
|
|
3
3
|
require "ticket/replicator/row_transformer"
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
8
|
+
class RowTransformer
|
9
|
+
RSpec.describe RowTransformer do
|
10
|
+
let(:transformer) { described_class.send(:new, extracted_row) }
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
describe ".run_on" do
|
13
|
+
let(:extracted_row) { :a_row_to_transform }
|
10
14
|
|
11
|
-
|
12
|
-
|
15
|
+
it "transforms the given file" do
|
16
|
+
expect(described_class).to receive(:new).with(:a_row_to_transform).and_return(transformer)
|
13
17
|
|
14
|
-
|
18
|
+
expect(transformer).to receive(:run)
|
15
19
|
|
16
|
-
|
17
|
-
|
18
|
-
|
20
|
+
described_class.run_on(:a_row_to_transform)
|
21
|
+
end
|
22
|
+
end
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
describe "#run" do
|
25
|
+
let(:extracted_row) do
|
26
|
+
{
|
27
|
+
id: "123",
|
28
|
+
status: "Open",
|
29
|
+
priority: "4 - Low",
|
30
|
+
team: "Source Team",
|
31
|
+
summary: "summary"
|
32
|
+
}
|
33
|
+
end
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
it "transforms a row" do
|
36
|
+
allow(transformer).to receive(:transformed_id).and_return("123")
|
37
|
+
allow(transformer).to receive(:transformed_status).and_return("Open")
|
38
|
+
allow(transformer).to receive(:transformed_resolution).and_return("")
|
39
|
+
allow(transformer).to receive(:transformed_priority).and_return("Low")
|
40
|
+
allow(transformer).to receive(:transformed_summary).and_return("transformed summary (123)")
|
41
|
+
allow(transformer).to receive(:transformed_source_ticket_url).and_return("url/to/source/ticket/123")
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
expect(transformer.run)
|
44
|
+
.to eq(["123", "Open", "", "Low", "transformed summary (123)", "url/to/source/ticket/123"])
|
45
|
+
end
|
46
|
+
end
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
48
|
+
describe "#mappings" do
|
49
|
+
let(:mappings_yaml) do
|
50
|
+
<<~'YAML'
|
51
|
+
field_mapping:
|
52
|
+
id: Defect
|
53
|
+
summary: Defect (2)
|
54
|
+
id_extraction_regex: "\\((?<id>\\d+)\\)$"
|
55
|
+
status_mapping:
|
56
|
+
defaults_to: keep_original_value
|
57
|
+
Open: Open
|
58
|
+
Closed: Closed
|
59
|
+
resolution_mapping:
|
60
|
+
Closed: Done
|
61
|
+
Rejected: Won't Fix
|
62
|
+
team_mapping:
|
63
|
+
Source Team: Transformed Team
|
64
|
+
Front End: Web Team
|
65
|
+
priority_mapping:
|
66
|
+
1 - Critical: Critical
|
67
|
+
2 - High: High
|
68
|
+
3 - Medium: Medium
|
69
|
+
4 - Low: Low
|
70
|
+
YAML
|
71
|
+
end
|
67
72
|
|
68
|
-
|
73
|
+
let(:extracted_row) { {} }
|
74
|
+
|
75
|
+
let(:file_io) { instance_double(IO) }
|
76
|
+
|
77
|
+
it "loads the mappings from the mappings.yml file" do
|
78
|
+
expect(IO).to receive(:open).with("config/ticket-replicator.mappings.yml",
|
79
|
+
"r:bom|utf-8").and_yield(mappings_yaml)
|
80
|
+
|
81
|
+
expect(transformer.mappings)
|
82
|
+
.to eq(
|
83
|
+
"field_mapping" => { "id" => "Defect", "summary" => "Defect (2)" },
|
84
|
+
"id_extraction_regex" => "\\((?<id>\\d+)\\)$",
|
85
|
+
"status_mapping" => { "defaults_to" => "keep_original_value", "Open" => "Open",
|
86
|
+
"Closed" => "Closed" },
|
87
|
+
"resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" },
|
88
|
+
"team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" },
|
89
|
+
"priority_mapping" =>
|
90
|
+
{ "1 - Critical" => "Critical", "2 - High" => "High", "3 - Medium" => "Medium",
|
91
|
+
"4 - Low" => "Low" }
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
69
95
|
|
70
|
-
|
96
|
+
describe "#ticket_replicator_source_ticket_url" do
|
97
|
+
let(:extracted_row) { {} }
|
71
98
|
|
72
|
-
|
73
|
-
|
99
|
+
context "when the source ticket url is set in the environment" do
|
100
|
+
before do
|
101
|
+
allow(ENV)
|
102
|
+
.to receive(:fetch).with("TICKET_REPLICATOR_SOURCE_TICKET_URL").and_return("url/to/source/ticket")
|
103
|
+
end
|
74
104
|
|
75
|
-
|
76
|
-
|
77
|
-
"field_mapping" => { "id" => "Defect", "summary" => "Defect (2)" },
|
78
|
-
"status_mapping" => { "defaults_to" => "keep_original_value", "Open" => "Open", "Closed" => "Closed" },
|
79
|
-
"resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" },
|
80
|
-
"team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" },
|
81
|
-
"priority_mapping" =>
|
82
|
-
{ "1 - Critical" => "Critical", "2 - High" => "High", "3 - Medium" => "Medium", "4 - Low" => "Low" }
|
83
|
-
)
|
84
|
-
end
|
85
|
-
end
|
105
|
+
it { expect(transformer.ticket_replicator_source_ticket_url).to eq("url/to/source/ticket") }
|
106
|
+
end
|
86
107
|
|
87
|
-
|
88
|
-
|
108
|
+
context "when the source ticket url is not set in the environment" do
|
109
|
+
it do
|
110
|
+
expect { transformer.ticket_replicator_source_ticket_url }
|
111
|
+
.to raise_error("TICKET_REPLICATOR_SOURCE_TICKET_URL: not set in environment!")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
89
115
|
|
90
|
-
|
91
|
-
|
92
|
-
allow(ENV)
|
93
|
-
.to receive(:fetch).with("TICKET_REPLICATOR_SOURCE_TICKET_URL").and_return("url/to/source/ticket")
|
94
|
-
end
|
116
|
+
describe "#source_ticket_url_builder" do
|
117
|
+
let(:extracted_row) { {} }
|
95
118
|
|
96
|
-
|
97
|
-
|
119
|
+
before do
|
120
|
+
allow(transformer)
|
121
|
+
.to receive_messages(ticket_replicator_source_ticket_url: ticket_replicator_source_ticket_url)
|
122
|
+
end
|
98
123
|
|
99
|
-
|
100
|
-
|
101
|
-
expect { transformer.ticket_replicator_source_ticket_url }
|
102
|
-
.to raise_error("TICKET_REPLICATOR_SOURCE_TICKET_URL: not set in environment!")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
124
|
+
context "when the URL has no syntax error" do
|
125
|
+
let(:ticket_replicator_source_ticket_url) { "http://url/to/source/ticket/<%= source_ticket_id %>" }
|
106
126
|
|
107
|
-
|
108
|
-
|
127
|
+
it { expect(transformer.source_ticket_url_builder("123")).to eq("http://url/to/source/ticket/123") }
|
128
|
+
end
|
109
129
|
|
110
|
-
|
111
|
-
|
112
|
-
end
|
130
|
+
context "when the URL has a syntax error" do
|
131
|
+
let(:ticket_replicator_source_ticket_url) { "http://url/to/source/ticket/<%= missing_variable %>" }
|
113
132
|
|
114
|
-
|
115
|
-
|
133
|
+
it { expect { transformer.source_ticket_url_builder("123") }.to raise_error(StandardError) }
|
134
|
+
end
|
135
|
+
end
|
116
136
|
|
117
|
-
|
118
|
-
|
137
|
+
describe "using mappings" do
|
138
|
+
before { allow(transformer).to receive_messages(mappings: mappings) }
|
139
|
+
|
140
|
+
describe "#remapped_field_extracted_row" do
|
141
|
+
let(:mappings) do
|
142
|
+
{
|
143
|
+
"field_mapping" => {
|
144
|
+
"id" => "Defect", "priority" => "Defect Priority", "resolution" => "Defect (2)",
|
145
|
+
"status" => "Defect status", "summary" => "Defect (2)"
|
146
|
+
}
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
let(:extracted_row) do
|
151
|
+
{
|
152
|
+
"defect (2)" => "a summary", "defect priority" => "High", "defect status" => "Open",
|
153
|
+
"defect team (2)" => "system team", "defect" => "123"
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
it do
|
158
|
+
expect(transformer.remapped_field_extracted_row)
|
159
|
+
.to eq({ "id" => "123", "priority" => "High", "resolution" => "a summary",
|
160
|
+
"status" => "Open", "summary" => "a summary" })
|
161
|
+
end
|
162
|
+
end
|
119
163
|
|
120
|
-
|
121
|
-
|
164
|
+
# rubocop:disable Metrics/BlockLength
|
165
|
+
context "when remapped field extracted row used" do
|
166
|
+
let(:extracted_row) { { :extracted_row => "should not be used in this context but the remapped version" } }
|
122
167
|
|
123
|
-
|
124
|
-
|
125
|
-
|
168
|
+
before do
|
169
|
+
allow(transformer).to receive_messages(remapped_field_extracted_row: remapped_field_extracted_row)
|
170
|
+
end
|
126
171
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
let(:mappings) do
|
132
|
-
{
|
133
|
-
"field_mapping" => {
|
134
|
-
"id" => "Defect", "priority" => "Defect Priority", "resolution" => "Defect (2)",
|
135
|
-
"status" => "Defect status", "summary" => "Defect (2)"
|
136
|
-
}
|
137
|
-
}
|
138
|
-
end
|
172
|
+
describe "#transformed_id" do
|
173
|
+
context "when using the ID value as is" do
|
174
|
+
let(:mappings) { {} }
|
175
|
+
let(:remapped_field_extracted_row) { { "id" => "123" } }
|
139
176
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
}
|
145
|
-
end
|
177
|
+
it "returns the ID" do
|
178
|
+
expect(transformer.transformed_id).to eq("123")
|
179
|
+
end
|
180
|
+
end
|
146
181
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
"status" => "Open", "summary" => "a summary" })
|
151
|
-
end
|
152
|
-
end
|
182
|
+
context "when extracting the ID from the string" do
|
183
|
+
let(:remapped_field_extracted_row) { { "id" => "also (85)73 in summary (123456)" } }
|
184
|
+
let(:mappings) { { "id_extraction_regex" => "\\((?<id>\\d+)\\)$" } }
|
153
185
|
|
154
|
-
|
155
|
-
context "when remapped field extracted row used" do
|
156
|
-
let(:extracted_row) { { :extracted_row => "should not be used in this context but the remapped version" } }
|
186
|
+
it { expect(transformer.transformed_id).to eq("123456") }
|
157
187
|
|
158
|
-
|
188
|
+
context "when the regex does not match" do
|
189
|
+
let(:remapped_field_extracted_row) { { "id" => "summary (non numeric)" } }
|
159
190
|
|
160
|
-
|
161
|
-
|
162
|
-
|
191
|
+
it "raises an error" do
|
192
|
+
expect { transformer.transformed_id }
|
193
|
+
.to raise_error(FileTransformer::TransformError,
|
194
|
+
%(No match found for :id in "summary (non numeric)") +
|
195
|
+
" using /\\((?<id>\\d+)\\)$/")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
163
200
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
end
|
201
|
+
describe "#transformed_source_ticket_url" do
|
202
|
+
let(:remapped_field_extracted_row) { { "id" => "123" } }
|
203
|
+
let(:mappings) { {} }
|
168
204
|
|
169
|
-
|
170
|
-
|
171
|
-
|
205
|
+
before do
|
206
|
+
allow(transformer)
|
207
|
+
.to receive_messages(
|
208
|
+
transformed_id: "123",
|
209
|
+
ticket_replicator_source_ticket_url: "url/to/source/ticket/<%= source_ticket_id %>"
|
210
|
+
)
|
211
|
+
end
|
172
212
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
end
|
213
|
+
it "returns the source ticket url" do
|
214
|
+
expect(transformer.transformed_source_ticket_url).to eq("url/to/source/ticket/123")
|
215
|
+
end
|
216
|
+
end
|
178
217
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
end
|
218
|
+
describe "#transformed_summary" do
|
219
|
+
let(:remapped_field_extracted_row) { { "id" => "426", "summary" => "a summary" } }
|
220
|
+
let(:mappings) { {} }
|
183
221
|
|
184
|
-
|
185
|
-
|
186
|
-
|
222
|
+
before do
|
223
|
+
allow(ReplicatedSummary).to receive(:build).with("426", "a summary").and_return("replicated_summary")
|
224
|
+
end
|
187
225
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
226
|
+
it "returns the summary" do
|
227
|
+
expect(transformer.transformed_summary).to eq("replicated_summary")
|
228
|
+
end
|
229
|
+
end
|
192
230
|
|
193
|
-
|
194
|
-
|
195
|
-
end
|
196
|
-
end
|
231
|
+
shared_examples "an unmapped value handler" do |field_name, source_field|
|
232
|
+
source_field ||= field_name
|
197
233
|
|
198
|
-
|
199
|
-
source_field ||= field_name
|
234
|
+
let(:remapped_field_extracted_row) { { source_field.to_s => an_original_value } }
|
200
235
|
|
201
|
-
|
236
|
+
let(:an_original_value) { "any #{field_name} #{rand}" }
|
202
237
|
|
203
|
-
|
238
|
+
let(:transformed_value) { transformer.send("transformed_#{field_name}") }
|
204
239
|
|
205
|
-
|
240
|
+
context "when defaulting to original value" do
|
241
|
+
let(:mappings) { { "#{field_name}_mapping" => { "defaults_to" => "keep_original_value" } } }
|
206
242
|
|
207
|
-
|
208
|
-
|
243
|
+
it { expect(transformed_value).to eq(an_original_value) }
|
244
|
+
end
|
209
245
|
|
210
|
-
|
211
|
-
|
246
|
+
context "when defaulting to unset value" do
|
247
|
+
let(:mappings) { { "#{field_name}_mapping" => { "defaults_to" => "unset_value" } } }
|
212
248
|
|
213
|
-
|
214
|
-
|
249
|
+
it { expect(transformed_value).to eq(nil) }
|
250
|
+
end
|
215
251
|
|
216
|
-
|
217
|
-
|
252
|
+
context "when defaulting to blank value" do
|
253
|
+
let(:mappings) { { "#{field_name}_mapping" => { "defaults_to" => "blank_value" } } }
|
218
254
|
|
219
|
-
|
220
|
-
|
255
|
+
it { expect(transformed_value).to eq("") }
|
256
|
+
end
|
221
257
|
|
222
|
-
|
223
|
-
|
258
|
+
context "when the value is unexpected" do
|
259
|
+
let(:mappings) do
|
260
|
+
{ "#{field_name}_mapping" => { "extracted value" => "transformed value" } }
|
261
|
+
end
|
224
262
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
263
|
+
it do
|
264
|
+
expect { transformed_value }
|
265
|
+
.to raise_error(RuntimeError,
|
266
|
+
"No mapping found for #{field_name.inspect} = #{an_original_value.inspect} " \
|
267
|
+
"in #{mappings["#{field_name}_mapping"].inspect}")
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
229
271
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
"in #{mappings["#{field_name}_mapping"].inspect}")
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|
272
|
+
describe "#transformed_status" do
|
273
|
+
let(:mappings) do
|
274
|
+
{ "status_mapping" => { "Open" => "Open", "Closed" => "Closed" } }
|
275
|
+
end
|
238
276
|
|
239
|
-
|
240
|
-
let(:mappings) do
|
241
|
-
{ "status_mapping" => { "Open" => "Open", "Closed" => "Closed" } }
|
242
|
-
end
|
277
|
+
let(:remapped_field_extracted_row) { { "status" => "Open" } }
|
243
278
|
|
244
|
-
|
279
|
+
it "returns the status" do
|
280
|
+
expect(transformer.transformed_status).to eq("Open")
|
281
|
+
end
|
245
282
|
|
246
|
-
|
247
|
-
|
248
|
-
end
|
283
|
+
it_behaves_like "an unmapped value handler", :status
|
284
|
+
end
|
249
285
|
|
250
|
-
|
251
|
-
|
286
|
+
describe "#transformed_resolution" do
|
287
|
+
let(:mappings) { { "resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" } } }
|
252
288
|
|
253
|
-
|
254
|
-
|
289
|
+
context "when the status is Closed" do
|
290
|
+
let(:remapped_field_extracted_row) { { "status" => "Closed" } }
|
255
291
|
|
256
|
-
|
257
|
-
|
292
|
+
it "returns the resolution" do
|
293
|
+
expect(transformer.transformed_resolution).to eq("Done")
|
294
|
+
end
|
295
|
+
end
|
258
296
|
|
259
|
-
|
260
|
-
|
261
|
-
end
|
262
|
-
end
|
297
|
+
context "when the status is Rejected" do
|
298
|
+
let(:remapped_field_extracted_row) { { "status" => "Rejected" } }
|
263
299
|
|
264
|
-
|
265
|
-
|
300
|
+
it "returns the resolution" do
|
301
|
+
expect(transformer.transformed_resolution).to eq("Won't Fix")
|
302
|
+
end
|
303
|
+
end
|
266
304
|
|
267
|
-
|
268
|
-
|
269
|
-
end
|
270
|
-
end
|
305
|
+
it_behaves_like "an unmapped value handler", :resolution, :status
|
306
|
+
end
|
271
307
|
|
272
|
-
|
273
|
-
|
308
|
+
describe "#transformed_team" do
|
309
|
+
let(:mappings) do
|
310
|
+
{ "team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" } }
|
311
|
+
end
|
274
312
|
|
275
|
-
|
276
|
-
let(:mappings) { { "team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" } } }
|
313
|
+
let(:remapped_field_extracted_row) { { "team" => "Source Team" } }
|
277
314
|
|
278
|
-
|
315
|
+
it "returns the team" do
|
316
|
+
expect(transformer.transformed_team).to eq("Transformed Team")
|
317
|
+
end
|
279
318
|
|
280
|
-
|
281
|
-
|
282
|
-
end
|
319
|
+
it_behaves_like "an unmapped value handler", :team
|
320
|
+
end
|
283
321
|
|
284
|
-
|
285
|
-
|
322
|
+
describe "#transformed_priority" do
|
323
|
+
let(:mappings) { { "priority_mapping" => { "1 - Critical" => "Critical", "2 - High" => "High" } } }
|
286
324
|
|
287
|
-
|
288
|
-
let(:mappings) { { "priority_mapping" => { "1 - Critical" => "Critical", "2 - High" => "High" } } }
|
325
|
+
let(:remapped_field_extracted_row) { { "priority" => "1 - Critical" } }
|
289
326
|
|
290
|
-
|
327
|
+
it "returns the priority" do
|
328
|
+
expect(transformer.transformed_priority).to eq("Critical")
|
329
|
+
end
|
291
330
|
|
292
|
-
|
293
|
-
|
331
|
+
it_behaves_like "an unmapped value handler", :priority
|
332
|
+
end
|
333
|
+
end
|
334
|
+
# rubocop:enable Metrics/BlockLength
|
294
335
|
end
|
295
|
-
|
296
|
-
it_behaves_like "an unmapped value handler", :priority
|
297
336
|
end
|
298
337
|
end
|
299
|
-
|
338
|
+
|
339
|
+
# rubocop:enable Metrics/ClassLength
|
300
340
|
end
|
301
341
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ticket-replicator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christophe Broult
|
@@ -146,6 +146,7 @@ extra_rdoc_files: []
|
|
146
146
|
files:
|
147
147
|
- ".rspec"
|
148
148
|
- ".rubocop.yml"
|
149
|
+
- ".ruby-version"
|
149
150
|
- CHANGELOG.md
|
150
151
|
- CODE_OF_CONDUCT.md
|
151
152
|
- Guardfile
|