ticket-replicator 0.1.1 → 1.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.
@@ -20,12 +20,13 @@ module Ticket
20
20
  end
21
21
 
22
22
  describe ".fields_to_load" do
23
- it { expect(described_class.fields_to_load).to eq(%i[id status resolution priority team summary]) }
23
+ it { expect(described_class.fields_to_load).to eq(%i[id status resolution priority summary source_ticket_url]) }
24
24
  end
25
25
 
26
26
  describe "#run" do
27
27
  it do
28
28
  expect(loader).to receive(:save_ticket)
29
+ expect(loader).to receive(:update_source_ticket_remote_link)
29
30
  expect(loader).to receive(:transition_ticket_to_the_expected_status)
30
31
 
31
32
  loader.run
@@ -56,6 +57,49 @@ module Ticket
56
57
  end
57
58
  end
58
59
 
60
+ describe "#jira_resolution_value" do
61
+ let(:loader) { described_class.send(:new, jira_project, row) }
62
+ let(:row) { { resolution: actual_resolution } }
63
+ let(:logger) { instance_double(Logger, info: nil) }
64
+ let(:resolutions) { { "Done" => double(id: "10004"), "Won't Fix" => double(id: "10005") } }
65
+
66
+ let(:jira_resolution_value) { loader.send(:jira_resolution_value) }
67
+
68
+ before do
69
+ allow(jira_project).to receive(:resolutions).and_return(resolutions)
70
+ allow(loader).to receive(:log).and_return(logger)
71
+ end
72
+
73
+ context "when the resolution is not set" do
74
+ let(:actual_resolution) { nil }
75
+ it { expect(jira_resolution_value).to be_nil }
76
+ end
77
+
78
+ context "when the resolution is an empty string" do
79
+ let(:actual_resolution) { "" }
80
+ it { expect(jira_resolution_value).to be_nil }
81
+ end
82
+
83
+ context "when the resolution is set to an existing value" do
84
+ let(:actual_resolution) { "Done" }
85
+ it { expect(jira_resolution_value).to eq({ name: "Done" }) }
86
+ end
87
+
88
+ context "when the resolution is set to an unknown value" do
89
+ let(:actual_resolution) { "an unknown resolution #{rand}" }
90
+
91
+ it do
92
+ expect(logger).to receive(:warn) do |&block|
93
+ expect(block.call)
94
+ .to eq("Setting resolution to unset " \
95
+ "since resolution #{actual_resolution.inspect} not found in #{resolutions.inspect}!")
96
+ end
97
+
98
+ expect(jira_resolution_value).to be_nil
99
+ end
100
+ end
101
+ end
102
+
59
103
  describe "#fetch_ticket" do
60
104
  let(:loader) { described_class.send(:new, jira_project, { id: "123" }) }
61
105
 
@@ -92,7 +136,8 @@ module Ticket
92
136
  let(:loader) { described_class.send(:new, jira_project, row) }
93
137
 
94
138
  let(:row) do
95
- { id: "123", priority: "Low", resolution: "", status: "Open", summary: "PREFIX | summary", team: "A Team" }
139
+ { id: "123", priority: "Low", resolution: "Done", status: "Open", summary: "PREFIX | summary",
140
+ team: "A Team" }
96
141
  end
97
142
 
98
143
  let(:ticket) { instance_double(Ticket, jira_ticket: jira_ticket) }
@@ -100,8 +145,15 @@ module Ticket
100
145
  let(:jira_ticket) { double(JIRA::Resource::Issue) }
101
146
 
102
147
  before do
103
- allow(loader).to receive_messages(ticket: ticket,
104
- :ticket_fields_need_to_be_updated? => ticket_fields_need_to_be_updated?)
148
+ allow(loader).to receive_messages(
149
+ ticket: ticket,
150
+ :ticket_fields_need_to_be_updated? => ticket_fields_need_to_be_updated?
151
+ )
152
+
153
+ allow(jira_project)
154
+ .to receive_messages(resolutions: {
155
+ "Done" => nil, "Won't Do" => nil, "Duplicate" => nil, "Cannot Reproduce" => nil
156
+ })
105
157
  end
106
158
 
107
159
  context "when the previously replicated needs to be updated " do
@@ -112,9 +164,8 @@ module Ticket
112
164
  fields: {
113
165
  project: { key: "PROJKEY" },
114
166
  issuetype: { name: "Bug" },
115
- # resolution: { name: "" },
167
+ resolution: { name: "Done" },
116
168
  priority: { name: "Low" },
117
- # team: { name: "A Team" },
118
169
  summary: "PREFIX | summary"
119
170
  }
120
171
  })
@@ -137,6 +188,30 @@ module Ticket
137
188
  end
138
189
  end
139
190
 
191
+ describe "#build_ticket_link_attributes" do
192
+ it "builds the link attributes with a default application" do
193
+ expect(described_class.send(:build_ticket_link_attributes,
194
+ "https://url/to/source/ticket/123", "Source Ticket 123"))
195
+ .to eq({ "object" => { "url" => "https://url/to/source/ticket/123", "title" => "Source Ticket 123" },
196
+ "application" => { "name" => "Ticket Source" } })
197
+ end
198
+
199
+ it "builds the link attributes with a custom application" do
200
+ expect(described_class.send(:build_ticket_link_attributes,
201
+ "https://url/to/source/ticket/123", "Source Ticket 123",
202
+ "A custom application"))
203
+ .to eq({ "object" => { "url" => "https://url/to/source/ticket/123", "title" => "Source Ticket 123" },
204
+ "application" => { "name" => "A custom application" } })
205
+ end
206
+
207
+ it "builds the link attributes with no application" do
208
+ expect(described_class.send(:build_ticket_link_attributes,
209
+ "https://url/to/source/ticket/123", "Source Ticket 123",
210
+ nil))
211
+ .to eq({ "object" => { "url" => "https://url/to/source/ticket/123", "title" => "Source Ticket 123" } })
212
+ end
213
+ end
214
+
140
215
  describe "#ticket_fields_need_to_be_updated?" do
141
216
  before do
142
217
  allow(loader).to receive_messages(ticket_previously_replicated?: ticket_previously_replicated?,
@@ -225,21 +300,139 @@ module Ticket
225
300
  end
226
301
  end
227
302
 
228
- describe "#transition_ticket_to_the_expected_status" do
303
+ # rubocop:disable Metrics/BlockLength
304
+ context "when caring for the source ticket link" do
305
+ def build_link(url, title, application_name = "Ticket Source")
306
+ RowLoader.build_ticket_link_attributes(url, title, application_name)
307
+ end
308
+
229
309
  let(:loader) { described_class.send(:new, jira_project, row) }
230
- let(:row) { { id: "123", status: "Testing" } }
310
+ let(:row) { { id: "123", source_ticket_url: "https://url/to/source/ticket/123" } }
311
+ let(:ticket) { instance_double(Ticket, jira_ticket: jira_ticket) }
312
+ let(:jira_ticket) { double(JIRA::Resource::Issue, remotelink: double(build: remotelink)) }
313
+ let(:remotelink) { instance_double(JIRA::Resource::Remotelink) }
231
314
 
232
- let(:ticket) { double(Ticket) }
315
+ before { allow(loader).to receive_messages(ticket: ticket) }
233
316
 
234
- it do
235
- expect(loader).to receive(:ticket).at_least(:once).and_return(ticket)
236
- expect(ticket).to receive(:transition_to).with("Testing")
317
+ let(:expected_link) { build_link("https://url/to/source/ticket/123", "Source Ticket 123") }
318
+
319
+ describe "#source_ticket_link_title" do
320
+ it { expect(loader.source_ticket_link_title).to eq("Source Ticket 123") }
321
+ end
322
+
323
+ describe "#update_source_ticket_remote_link" do
324
+ before do
325
+ allow(loader).to receive_messages(source_ticket_link_needs_update?: needs_update)
326
+ end
327
+
328
+ context "when the link does not exist" do
329
+ let(:needs_update) { true }
330
+
331
+ before { allow(ticket).to receive_messages(source_ticket_link: nil) }
332
+
333
+ it "setups the link" do
334
+ expect(remotelink).to receive(:save!).with(expected_link)
335
+
336
+ loader.update_source_ticket_remote_link
337
+ end
338
+ end
339
+
340
+ context "when the link needs to be updated" do
341
+ let(:needs_update) { true }
342
+ let(:source_ticket_link) { instance_double(JIRA::Resource::Remotelink) }
343
+
344
+ before { allow(ticket).to receive_messages(source_ticket_link: source_ticket_link) }
345
+
346
+ it "deletes the previous link and creates a new one" do
347
+ expect(source_ticket_link).to receive(:delete)
348
+ expect(remotelink).to receive(:save!).with(expected_link)
349
+
350
+ loader.update_source_ticket_remote_link
351
+ end
352
+ end
353
+
354
+ context "when the link does not need to be updated" do
355
+ let(:needs_update) { false }
356
+
357
+ it "no link creation happens" do
358
+ expect(remotelink).not_to receive(:save!)
359
+
360
+ loader.update_source_ticket_remote_link
361
+ end
362
+ end
363
+ end
364
+
365
+ describe "#source_ticket_link_needs_update?" do
366
+ before do
367
+ allow(ticket).to receive_messages(source_ticket_link: remote_link)
368
+ end
369
+
370
+ context "when link has yet to be created" do
371
+ let(:remote_link) { nil }
372
+
373
+ it { expect(loader.source_ticket_link_needs_update?).to be_truthy }
374
+ end
375
+
376
+ context "when link already exists" do
377
+ let(:remote_link) do
378
+ instance_double(JIRA::Resource::Remotelink,
379
+ attrs: source_ticket_link,
380
+ inspect: "<JIRA::Resource::Remotelink attrs: #{source_ticket_link}>")
381
+ end
382
+
383
+ context "when the link is not up to date" do
384
+ let(:source_ticket_link) { build_link(existing_url, existing_title) }
385
+
386
+ context "when url has changed" do
387
+ let(:existing_url) { "https://__OLD_URL__/to/source/ticket/1234" }
388
+ let(:existing_title) { "Source Ticket 123" }
389
+
390
+ it { expect(loader.source_ticket_link_needs_update?).to be_truthy }
391
+ end
392
+
393
+ context "when title has changed" do
394
+ let(:existing_url) { "https://url/to/source/ticket/123" }
395
+ let(:existing_title) { "__OLD TITLE__Source Ticket 1234" }
396
+
397
+ it { expect(loader.source_ticket_link_needs_update?).to be_truthy }
398
+ end
399
+ end
400
+
401
+ context "when the link is up to date" do
402
+ let(:existing_url) { "https://url/to/source/ticket/123" }
403
+ let(:existing_title) { "Source Ticket 123" }
404
+ let(:source_ticket_link) do
405
+ { "object" => {
406
+ "url" => "https://url/to/source/ticket/123",
407
+ "title" => "Source Ticket 123",
408
+ "icon" => { "title" => "link", "url16x16" => "https://url/to/source/ticket/123/icon" }
409
+ },
410
+ "application" => { "name" => "Ticket Source" },
411
+ "extra_attributes" =>
412
+ { "id" => 10_443, "self" => "https://url/to/source/ticket/123/remotelink/10443" } }
413
+ end
414
+
415
+ it("only checks the needed attributes") { expect(loader.source_ticket_link_needs_update?).to be_falsey }
416
+ end
417
+ end
418
+ end
419
+ # rubocop:enable Metrics/BlockLength
420
+
421
+ describe "#transition_ticket_to_the_expected_status" do
422
+ let(:loader) { described_class.send(:new, jira_project, row) }
423
+ let(:row) { { id: "123", status: "Testing" } }
424
+
425
+ let(:ticket) { double(Ticket) }
237
426
 
238
- loader.transition_ticket_to_the_expected_status
427
+ it do
428
+ expect(loader).to receive(:ticket).at_least(:once).and_return(ticket)
429
+ expect(ticket).to receive(:transition_to).with("Testing")
430
+
431
+ loader.transition_ticket_to_the_expected_status
432
+ end
239
433
  end
240
434
  end
241
435
  end
436
+ # rubocop:enable Metrics/ClassLength
242
437
  end
243
-
244
- # rubocop:enable Metrics/ClassLength
245
438
  end
@@ -33,11 +33,11 @@ RSpec.describe Ticket::Replicator::RowTransformer do
33
33
  allow(transformer).to receive(:transformed_status).and_return("Open")
34
34
  allow(transformer).to receive(:transformed_resolution).and_return("")
35
35
  allow(transformer).to receive(:transformed_priority).and_return("Low")
36
- allow(transformer).to receive(:transformed_team).and_return("Transformed Team")
37
36
  allow(transformer).to receive(:transformed_summary).and_return("transformed summary (123)")
37
+ allow(transformer).to receive(:transformed_source_ticket_url).and_return("url/to/source/ticket/123")
38
38
 
39
39
  expect(transformer.run)
40
- .to eq(["123", "Open", "", "Low", "Transformed Team", "transformed summary (123)"])
40
+ .to eq(["123", "Open", "", "Low", "transformed summary (123)", "url/to/source/ticket/123"])
41
41
  end
42
42
  end
43
43
 
@@ -84,6 +84,46 @@ RSpec.describe Ticket::Replicator::RowTransformer do
84
84
  end
85
85
  end
86
86
 
87
+ describe "#ticket_replicator_source_ticket_url" do
88
+ let(:extracted_row) { {} }
89
+
90
+ context "when the source ticket url is set in the environment" do
91
+ before do
92
+ allow(ENV)
93
+ .to receive(:fetch).with("TICKET_REPLICATOR_SOURCE_TICKET_URL").and_return("url/to/source/ticket")
94
+ end
95
+
96
+ it { expect(transformer.ticket_replicator_source_ticket_url).to eq("url/to/source/ticket") }
97
+ end
98
+
99
+ context "when the source ticket url is not set in the environment" do
100
+ it do
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
106
+
107
+ describe "#source_ticket_url_builder" do
108
+ let(:extracted_row) { {} }
109
+
110
+ before do
111
+ allow(transformer).to receive_messages(ticket_replicator_source_ticket_url: ticket_replicator_source_ticket_url)
112
+ end
113
+
114
+ context "when the URL has no syntax error" do
115
+ let(:ticket_replicator_source_ticket_url) { "http://url/to/source/ticket/<%= source_ticket_id %>" }
116
+
117
+ it { expect(transformer.source_ticket_url_builder("123")).to eq("http://url/to/source/ticket/123") }
118
+ end
119
+
120
+ context "when the URL has a syntax error" do
121
+ let(:ticket_replicator_source_ticket_url) { "http://url/to/source/ticket/<%= missing_variable %>" }
122
+
123
+ it { expect { transformer.source_ticket_url_builder("123") }.to raise_error(StandardError) }
124
+ end
125
+ end
126
+
87
127
  describe "using mappings" do
88
128
  before { allow(transformer).to receive_messages(mappings: mappings) }
89
129
 
@@ -92,7 +132,7 @@ RSpec.describe Ticket::Replicator::RowTransformer do
92
132
  {
93
133
  "field_mapping" => {
94
134
  "id" => "Defect", "priority" => "Defect Priority", "resolution" => "Defect (2)",
95
- "status" => "Defect status", "summary" => "Defect (2)", "team" => "Defect Team (2)"
135
+ "status" => "Defect status", "summary" => "Defect (2)"
96
136
  }
97
137
  }
98
138
  end
@@ -107,7 +147,7 @@ RSpec.describe Ticket::Replicator::RowTransformer do
107
147
  it do
108
148
  expect(transformer.remapped_field_extracted_row)
109
149
  .to eq({ "id" => "123", "priority" => "High", "resolution" => "a summary",
110
- "status" => "Open", "summary" => "a summary", "team" => "system team" })
150
+ "status" => "Open", "summary" => "a summary" })
111
151
  end
112
152
  end
113
153
 
@@ -126,6 +166,21 @@ RSpec.describe Ticket::Replicator::RowTransformer do
126
166
  end
127
167
  end
128
168
 
169
+ describe "#transformed_source_ticket_url" do
170
+ let(:remapped_field_extracted_row) { { "id" => "123" } }
171
+ let(:mappings) { {} }
172
+
173
+ before do
174
+ allow(transformer)
175
+ .to receive_messages(transformed_id: "123",
176
+ ticket_replicator_source_ticket_url: "url/to/source/ticket/<%= source_ticket_id %>")
177
+ end
178
+
179
+ it "returns the source ticket url" do
180
+ expect(transformer.transformed_source_ticket_url).to eq("url/to/source/ticket/123")
181
+ end
182
+ end
183
+
129
184
  describe "#transformed_summary" do
130
185
  let(:remapped_field_extracted_row) { { "id" => "426", "summary" => "a summary" } }
131
186
  let(:mappings) { {} }
@@ -155,7 +210,19 @@ RSpec.describe Ticket::Replicator::RowTransformer do
155
210
  it { expect(transformed_value).to eq(an_original_value) }
156
211
  end
157
212
 
158
- context "when the status is unexpected" do
213
+ context "when defaulting to unset value" do
214
+ let(:mappings) { { "#{field_name}_mapping" => { "defaults_to" => "unset_value" } } }
215
+
216
+ it { expect(transformed_value).to eq(nil) }
217
+ end
218
+
219
+ context "when defaulting to blank value" do
220
+ let(:mappings) { { "#{field_name}_mapping" => { "defaults_to" => "blank_value" } } }
221
+
222
+ it { expect(transformed_value).to eq("") }
223
+ end
224
+
225
+ context "when the value is unexpected" do
159
226
  let(:mappings) do
160
227
  { "#{field_name}_mapping" => { "extracted value" => "transformed value" } }
161
228
  end
@@ -136,7 +136,6 @@ module Ticket
136
136
  let(:jira_auto_tool) { instance_double(Jira::Auto::Tool) }
137
137
  let(:jira_client) { instance_double(JIRA::Client, Field: field_navigator) }
138
138
 
139
- # let(:jira_ticket) { OpenStruct.new(jira_ticket_properties_hash["fields"]) }
140
139
  let(:jira_ticket) { JIRA::Resource::Issue.build(jira_client, jira_ticket_properties_hash) }
141
140
 
142
141
  let(:ticket) { described_class.new(jira_auto_tool, jira_ticket) }
@@ -149,6 +148,56 @@ module Ticket
149
148
  it { expect(ticket.summary).to eq("SMAN-16384 | Login page randomly fails to load CSS assets (10001)") }
150
149
  end
151
150
 
151
+ def build_ticket_link(url, title, application = Ticket::SOURCE_TICKET_REMOTE_LINK_APPLICATION)
152
+ double(JIRA::Resource::Remotelink, attrs: RowLoader.build_ticket_link_attributes(url, title, application))
153
+ end
154
+
155
+ describe "#source_ticket_link" do
156
+ let(:ticket_links) { [double(JIRA::Resource::Remotelink, attrs: {}), source_ticket_jira_link].compact }
157
+
158
+ before { allow(jira_ticket).to receive_messages(remotelink: double(all: ticket_links)) }
159
+
160
+ let(:source_ticket_jira_link) { build_ticket_link("https://ur/to/source/ticket/16384", "Source Ticket 16384") }
161
+
162
+ it { expect(ticket.source_ticket_link).to eq(source_ticket_jira_link) }
163
+
164
+ context "when no source ticket is linked" do
165
+ let(:source_ticket_jira_link) { nil }
166
+ it { expect(ticket.source_ticket_link).to be_nil }
167
+ end
168
+
169
+ context "when the existing remote link is not a ticket source link (no application specified)" do
170
+ let(:source_ticket_jira_link) do
171
+ build_ticket_link("https://ur/to/source/ticket/16384", "Source Ticket 16384", nil)
172
+ end
173
+
174
+ it { expect(ticket.source_ticket_link).to be_nil }
175
+ end
176
+
177
+ context "when the existing remote link is not a ticket source link (another application specified)" do
178
+ let(:source_ticket_jira_link) do
179
+ build_ticket_link("https://ur/to/source/ticket/16384", "Source Ticket 16384", "Another application")
180
+ end
181
+
182
+ it { expect(ticket.source_ticket_link).to be_nil }
183
+ end
184
+ end
185
+
186
+ describe "#source_ticket_url" do
187
+ before do
188
+ allow(ticket).to receive_messages(source_ticket_link: source_ticket_jira_link)
189
+ end
190
+
191
+ let(:source_ticket_jira_link) { build_ticket_link("https://ur/to/source/ticket/16384", "Source Ticket 16384") }
192
+
193
+ it { expect(ticket.source_ticket_url).to eq("https://ur/to/source/ticket/16384") }
194
+
195
+ context "when no source ticket is linked" do
196
+ let(:source_ticket_jira_link) { nil }
197
+ it { expect(ticket.source_ticket_url).to be_nil }
198
+ end
199
+ end
200
+
152
201
  describe "replicated?" do
153
202
  let(:ticket) { described_class.new(jira_auto_tool, jira_ticket) }
154
203
  let(:jira_ticket) { double(JIRA::Resource::Issue, summary: actual_summary) }
@@ -238,7 +287,28 @@ module Ticket
238
287
  ticket.transition_to("Testing")
239
288
  end
240
289
  end
290
+
291
+ describe "#sanitize" do
292
+ let(:expectations) do
293
+ [
294
+ ["value as name attribute", double(name: "value as name attribute")],
295
+ ["value as value attribute", double(value: "value as value attribute")],
296
+ ["value from name entry in hash", { "name" => "value from name entry in hash" }],
297
+ ["value from value entry in hash", { "value" => "value from value entry in hash" }],
298
+ ["value as a string", "value as a string"],
299
+ ["125", 125],
300
+ ["object as a string", double(to_s: "object as a string")],
301
+ ["", nil]
302
+ ]
303
+ end
304
+
305
+ it do
306
+ expectations.each do |expected_sanitized_value, value|
307
+ expect(ticket.send(:sanitize, value)).to eq(expected_sanitized_value)
308
+ end
309
+ end
310
+ end
241
311
  end
242
312
  end
243
- # rubocop:enable Metrics/ClassLength
244
313
  end
314
+ # rubocop:enable Metrics/ClassLength
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: 0.1.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christophe Broult