ticket-replicator 1.2.1 → 1.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4908df60d4dab3d25cd2e90eeb384b39ab9bb979e8f9b37a8f2c42cc2f55504d
4
- data.tar.gz: dfbc162f181e5430b18b9415065cb6fc88d1ae995b4371eb9dbe4b01cb2d304b
3
+ metadata.gz: f526835796d74716dc38bff10d6be52892b547ad78c089b8a7e77e5fc080ffde
4
+ data.tar.gz: 0f004dbeda462964f94aa14f8ddaf51edca62dc1ac73d3f1fb89fab209e3a5d7
5
5
  SHA512:
6
- metadata.gz: b304fa83d1ad816f78e6471ccd0213c4fa85a0bc8a3439536dd272a222c60fc0aaf3585f05454905850e6a9517caf0335a7f5eea59c10a007896a3bdbdf2cfca
7
- data.tar.gz: 56023fe34b940b1d3e1be046edbae4d6912815a87a69f2d965110eb94a827388d5483443fe4f0fe8e24e067e534f6630e43f063a19e48b0efed3384074b68aff
6
+ metadata.gz: 34c1f9ad5dd0f245d72f483c507515c44055f2489039320667ad5430aeea3a58585b2be0e2c4075eebccc98ee2924495bf07afa1f78c94463d27c14566eba58f
7
+ data.tar.gz: dcd4d81a0ce4361f906e4fe921421035987db61693ed224fbbcf14bd2d3d6e8f3eaa534d1b90c26f2f8d1958af34050ddfd4dffb947021f1c51652c73e8ac705
data/README.md CHANGED
@@ -52,6 +52,8 @@ Optional environment variables:
52
52
  - `JAT_RATE_INTERVAL_IN_SECONDS` - Interval for rate limiting in seconds (e.g., "1").
53
53
  - `JAT_RATE_LIMIT_PER_INTERVAL` - Rate limit per interval for Jira API calls (e.g., "1")
54
54
  - `TICKET_REPLICATOR_DISABLE_RESOLUTION_LOADING` - To deal with tickets not having a resolution field.
55
+ - `TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS` - Limit the amount of requests
56
+ to modified tickets only (default to "true" when unset). Set to "false" to disable this feature.
55
57
 
56
58
  ### Setup Queue Folder And Field Mapping
57
59
 
@@ -42,7 +42,7 @@ Feature: Load tickets into Jira
42
42
  Given the following environment variables have been set:
43
43
  | name | value |
44
44
  | TICKET_REPLICATOR_DISABLE_RESOLUTION_LOADING | true |
45
- Given a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
45
+ And a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
46
46
  """
47
47
  "ID","Status","Resolution","Priority","Summary","Source Ticket URL"
48
48
  "10001","Open","","Highest","SMAN-10001 | Login page randomly fails to load CSS assets (10001)","http://url/to/source/ticket/10001"
@@ -56,6 +56,23 @@ Feature: Load tickets into Jira
56
56
  | Confirmed | | Medium | SMAN-10003 \| Invalid date format in SOAP response (10003) | http://url/to/source/ticket/10003 |
57
57
  | Withdrawn | | Low | SMAN-10007 \| Test data missing edge case scenarios (10007) | http://url/to/source/ticket/10007 |
58
58
 
59
+ Scenario: No duplicates created when the same ticket is listed several times in a given file
60
+ Given a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
61
+ """
62
+ "ID","Status","Resolution","Priority","Summary","Source Ticket URL"
63
+ "10001","Open","","Highest","SMAN-10001 | Login page randomly fails to load CSS assets (10001)","http://url/to/source/ticket/10001"
64
+ "10001","Open","","Highest","SMAN-10001 | Login page randomly fails to load CSS assets (10001)","http://url/to/source/ticket/10001"
65
+ "10003","Open","","Medium","SMAN-10003 | Invalid date format in SOAP response (10003)","http://url/to/source/ticket/10003"
66
+ "10007","Withdrawn","Won't Do","Low","SMAN-10007 | Test data missing edge case scenarios (10007)","http://url/to/source/ticket/10007"
67
+ "10003","Confirmed","Done","Medium","SMAN-10003 | Invalid date format in SOAP response (10003)","http://url/to/source/ticket/10003"
68
+ """
69
+ When I successfully run `ticket-replicator --load`
70
+ Then the Jira project should only have the following tickets:
71
+ | status | resolution | priority | summary | source_ticket_url |
72
+ | Open | | Highest | SMAN-10001 \| Login page randomly fails to load CSS assets (10001) | http://url/to/source/ticket/10001 |
73
+ | Confirmed | Done | Medium | SMAN-10003 \| Invalid date format in SOAP response (10003) | http://url/to/source/ticket/10003 |
74
+ | Withdrawn | Won't Do | Low | SMAN-10007 \| Test data missing edge case scenarios (10007) | http://url/to/source/ticket/10007 |
75
+
59
76
 
60
77
  Scenario: Loading the ticket information twice in Jira does not create additional tickets
61
78
  Given a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
@@ -95,12 +112,37 @@ Feature: Load tickets into Jira
95
112
  """
96
113
  And I successfully run `ticket-replicator --jira-http-debug --load`
97
114
  Then the Jira project should only have the following tickets:
98
- | example purpose | status | resolution | priority | summary | source_ticket_url |
99
- | one update due to status change | Closed | | Highest | SMAN-10001 \| Login page randomly fails to load CSS assets (10001) | http://url/to/source/ticket/10001 |
100
- | one update due to priority, resolution and summary updates | In Process | | High | SMAN-10002 \| *Recurring* database deadlock during order processing (10002) | http://url/to/source/ticket/10002 |
101
- | three updates due to new ticket created (fields, source ticket link and status) | Open | | Highest | SMAN-10008 \| Memory leak in caching implementation (10008) | http://url/to/source/ticket/10008 |
102
- | no update since no status or field change | Confirmed | Done | High | SMAN-10016 \| Slow response time on product search API (10016) | http://url/to/source/ticket/10016 |
103
- And only 5 Jira update requests were emitted
115
+ | example purpose | status | resolution | priority | summary | source_ticket_url |
116
+ | one update due to status change | Closed | | Highest | SMAN-10001 \| Login page randomly fails to load CSS assets (10001) | http://url/to/source/ticket/10001 |
117
+ | two updates due to field updated (priority, resolution and summary) and updating the remote link | In Process | | High | SMAN-10002 \| *Recurring* database deadlock during order processing (10002) | http://url/to/source/ticket/10002 |
118
+ | three updates due to new ticket created (fields, source ticket link and status) | Open | | Highest | SMAN-10008 \| Memory leak in caching implementation (10008) | http://url/to/source/ticket/10008 |
119
+ | no update since no status or field change | Confirmed | Done | High | SMAN-10016 \| Slow response time on product search API (10016) | http://url/to/source/ticket/10016 |
120
+ And only 6 Jira update requests were emitted
121
+
122
+ Scenario: Updating fields and statuses of closed and withdrawn tickets is possible
123
+ Given a file named "queue/20.transformed/10.initial_replication.csv" with:
124
+ """
125
+ "ID","Status","Resolution","Priority","Summary","Source Ticket URL"
126
+ "10001","Closed","","Highest","SMAN-10001 | Possible to update summary and resolution of closed ticket (100001)","http://url/to/source/ticket/10001"
127
+ "10002","Closed","","Low","SMAN-10002 | Reopening ticket while updating priority (10002)","http://url/to/source/ticket/10002"
128
+ "10003","Withdrawn","Won't Do","Medium","SMAN-10003 | Possible to update a resolved ticket (10003)","http://url/to/source/ticket/10003"
129
+ "10004","Withdrawn","Done","Low","SMAN-10004 | Re-opening resolved ticket (10004)","http://url/to/source/ticket/10004"
130
+ """
131
+ And a file named "queue/20.transformed/20.update_replication.csv" with:
132
+ """
133
+ "ID","Status","Resolution","Priority","Summary","Source Ticket URL"
134
+ "10001","Closed","Done","Highest","SMAN-10001 | Successfully updated summary and resolution of the closed ticket (100001)","http://url/to/source/ticket/10001"
135
+ "10002","Open","","Highest","SMAN-10002 | Successfully reopened ticket while its updating priority and summary (10002)","http://url/to/source/ticket/10002"
136
+ "10003","Withdrawn","Done","High","SMAN-10003 | Successfully updated resolved ticket (10003)","http://url/to/source/ticket/10003"
137
+ "10004","Open","","Low","SMAN-10004 | Successfully re-opened and updated the ticket (10004)","http://url/to/source/ticket/10004"
138
+ """
139
+ When I successfully run `ticket-replicator --load`
140
+ Then the Jira project should only have the following tickets:
141
+ | status | resolution | priority | summary | source_ticket_url |
142
+ | Closed | Done | Highest | SMAN-10001 \| Successfully updated summary and resolution of the closed ticket (100001) | http://url/to/source/ticket/10001 |
143
+ | Open | | Highest | SMAN-10002 \| Successfully reopened ticket while its updating priority and summary (10002) | http://url/to/source/ticket/10002 |
144
+ | Withdrawn | Done | High | SMAN-10003 \| Successfully updated resolved ticket (10003) | http://url/to/source/ticket/10003 |
145
+ | Open | | Low | SMAN-10004 \| Successfully re-opened and updated the ticket (10004) | http://url/to/source/ticket/10004 |
104
146
 
105
147
  Scenario: Attempting to set an unexpected status generates information about the current row being loaded
106
148
  Given a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
@@ -120,7 +162,7 @@ Feature: Load tickets into Jira
120
162
  Then it should fail with:
121
163
  """
122
164
  ERROR Object : Ticket::Replicator::FileLoader::LoadError: queue/20.transformed/sap_solution_manager_defects.csv:5: error while loading row:
123
- No transition found for "____ INEXISTING STATUS ____" in ["No Error -> No Error", "Tester Action -> Tester Action", "Close Bug -> Closed", "Confirmed -> Confirmed", "Defect Correction in Process -> Defect Correction in Process", "Deferred -> Deferred", "Forwarded -> Forwarded", "Information Required -> Information Required", "New -> New", "Open -> Open", "Solution Proposal -> Solution Proposal", "Wait for Defect Correction -> Wait for Defect Correction", "Wait on External -> Wait on External", "Withdrawn -> Withdrawn", "In Process -> In Process"].:
165
+ No transition found for "____ INEXISTING STATUS ____" in ["No Error => No Error", "Tester Action => Tester Action", "Close Bug => Closed", "Confirmed => Confirmed", "Defect Correction in Process => Defect Correction in Process", "Deferred => Deferred", "Forwarded => Forwarded", "Information Required => Information Required", "New => New", "Open => Open", "Solution Proposal => Solution Proposal", "Wait for Defect Correction => Wait for Defect Correction", "Wait on External => Wait on External", "Withdrawn => Withdrawn", "In Process => In Process"].:
124
166
  #<CSV::Row id:"10004" status:"____ INEXISTING STATUS ____" resolution:"Done" priority:"Low" summary:"SMAN-10004 | App crashes when offline on Android 12 (10004)" source_ticket_url:"http://url/to/source/ticket/10004">
125
167
  """
126
168
 
@@ -9,7 +9,7 @@ Feature: Transform and load extracted ticket queue
9
9
  | TICKET_REPLICATOR_JIRA_PROJECT_KEY |
10
10
  | TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME |
11
11
  And the following environment variables have been set:
12
- | name | value |
12
+ | name | value |
13
13
  | TICKET_REPLICATOR_SOURCE_TICKET_URL | http://url/to/source/ticket/<%= source_ticket_id %> |
14
14
 
15
15
  And a file named "config/ticket-replicator.mappings.yml" with:
@@ -34,7 +34,7 @@ module Ticket
34
34
  #{row.inspect}
35
35
  EOERROR
36
36
 
37
- raise LoadError, message
37
+ raise LoadError, message, e.backtrace
38
38
  end
39
39
  end
40
40
 
@@ -40,17 +40,23 @@ module Ticket
40
40
  end
41
41
  end
42
42
 
43
+ private
44
+
43
45
  def extracted_rows
44
46
  raise NotImplementedError, "You must implement this method in your subclass"
45
47
  end
46
48
 
49
+ def mappings
50
+ @mappings ||= Mappings.new
51
+ end
52
+
47
53
  FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER = 2
48
54
  private_constant :FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
49
55
 
50
56
  def transformed_rows
51
57
  row_index = FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
52
58
  extracted_rows.collect do |row|
53
- RowTransformer.run_on(row)
59
+ RowTransformer.run_on(row, mappings)
54
60
  rescue StandardError => e
55
61
  raise TransformError, <<~EOERRORMSG
56
62
  #{extracted_path}:#{row_index}: error while transforming row:
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticket
4
+ class Replicator
5
+ class Mappings
6
+ def self.file_path
7
+ File.join("config", "ticket-replicator.mappings.yml")
8
+ end
9
+
10
+ def to_h
11
+ @to_h = YAML.safe_load_file(self.class.file_path, symbolize_names: false)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -38,9 +38,15 @@ module Ticket
38
38
  def save_ticket
39
39
  return unless ticket_fields_need_to_be_updated?
40
40
 
41
+ reopen_ticket_if_status_in_done_category
42
+
41
43
  ticket.jira_ticket.save!(attributes_for_save)
42
44
 
43
45
  ticket.jira_ticket.fetch
46
+
47
+ @ticket_was_just_updated = true
48
+
49
+ jira_project.replicated_tickets[id] = ticket
44
50
  end
45
51
 
46
52
  private
@@ -76,6 +82,12 @@ module Ticket
76
82
  nil
77
83
  end
78
84
 
85
+ def reopen_ticket_if_status_in_done_category
86
+ return unless ticket_previously_replicated? && ticket.status_in_done_category?
87
+
88
+ ticket.transition_to_not_done
89
+ end
90
+
79
91
  def ticket_fields_need_to_be_updated?
80
92
  !ticket_previously_replicated? || ticket_fields_changed?
81
93
  end
@@ -98,6 +110,8 @@ module Ticket
98
110
 
99
111
  # rubocop:disable Metrics/MethodLength
100
112
  def source_ticket_link_needs_update?
113
+ return ticket_was_just_updated? if only_update_links_for_modified_ticket?
114
+
101
115
  existing_ticket_link_attributes = ticket.source_ticket_link&.attrs
102
116
 
103
117
  object = existing_ticket_link_attributes&.[]("object")
@@ -118,6 +132,10 @@ module Ticket
118
132
  filtered_existing_ticket_link_attributes != ticket_link_attributes
119
133
  end
120
134
 
135
+ def only_update_links_for_modified_ticket?
136
+ ENV.fetch("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS", "true") != "false"
137
+ end
138
+
121
139
  # rubocop:enable Metrics/MethodLength
122
140
 
123
141
  def source_ticket_link_title
@@ -137,10 +155,15 @@ module Ticket
137
155
  if ticket_previously_replicated?
138
156
  fetch_ticket
139
157
  else
158
+ @ticket_was_just_updated = true
140
159
  create_ticket
141
160
  end
142
161
  end
143
162
 
163
+ def ticket_was_just_updated?
164
+ @ticket_was_just_updated
165
+ end
166
+
144
167
  def fetch_ticket
145
168
  jira_project.replicated_tickets.fetch(id)
146
169
  end
@@ -2,21 +2,23 @@
2
2
 
3
3
  require "ticket/replicator/replicated_summary"
4
4
  require "ticket/replicator/file_transformer"
5
+ require "ticket/replicator/mappings"
5
6
  require "erb"
6
7
 
7
8
  module Ticket
8
9
  class Replicator
9
10
  class RowTransformer
10
- def self.run_on(extracted_row)
11
- new(extracted_row).run
11
+ def self.run_on(extracted_row, mappings)
12
+ new(extracted_row, mappings).run
12
13
  end
13
14
 
14
15
  private_class_method :new
15
16
 
16
- attr_reader :extracted_row
17
+ attr_reader :extracted_row, :mappings
17
18
 
18
- def initialize(extracted_row)
19
+ def initialize(extracted_row, mappings)
19
20
  @extracted_row = extracted_row
21
+ @mappings = mappings.to_h
20
22
  end
21
23
 
22
24
  def run
@@ -115,10 +117,6 @@ module Ticket
115
117
  end
116
118
  end
117
119
 
118
- def mappings
119
- @mappings ||= YAML.safe_load_file(mapping_file_path, symbolize_names: false)
120
- end
121
-
122
120
  def remapped_field_extracted_row
123
121
  @remapped_field_extracted_row ||= fields_to_remap.to_h do |field|
124
122
  mapped_field_key = remapped_field_key(field)
@@ -142,10 +140,6 @@ module Ticket
142
140
  @extracted_field_mapping ||= mapping_for :field
143
141
  end
144
142
 
145
- def self.mapping_file_path
146
- File.join("config", "ticket-replicator.mappings.yml")
147
- end
148
-
149
143
  private
150
144
 
151
145
  def extracted_row_value_for(mapped_field_key)
@@ -35,7 +35,7 @@ module Ticket
35
35
  def config_file_path
36
36
  @config_file_path ||=
37
37
  begin
38
- path = RowTransformer.mapping_file_path
38
+ path = Mappings.file_path
39
39
  FileUtils.mkdir_p(File.dirname(path))
40
40
  path
41
41
  end
@@ -21,7 +21,7 @@ module Ticket
21
21
  end
22
22
 
23
23
  def transition_to(status)
24
- TicketStatusTransitioner.new(self).transition_to(status)
24
+ status_transitioner.transition_to(status)
25
25
  end
26
26
 
27
27
  def <=>(other)
@@ -49,8 +49,20 @@ module Ticket
49
49
  source_ticket_link_attributes(source_ticket_link)&.[]("object")&.[]("url")
50
50
  end
51
51
 
52
+ def status_in_done_category?
53
+ jira_ticket.status.attrs.fetch("statusCategory").fetch("key") == "done"
54
+ end
55
+
56
+ def transition_to_not_done
57
+ status_transitioner.transition_to_not_done
58
+ end
59
+
52
60
  private
53
61
 
62
+ def status_transitioner
63
+ TicketStatusTransitioner.new(self)
64
+ end
65
+
54
66
  def source_ticket_link_attributes(link)
55
67
  link&.attrs
56
68
  end
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Ticket
4
4
  class Replicator
5
+ # Assumption: The ticket workflow allows for transitioning to any other status.
5
6
  class TicketStatusTransitioner
7
+ StatusTransitionerError = Class.new(StandardError)
8
+
6
9
  attr_reader :ticket
7
10
 
8
11
  def initialize(ticket)
@@ -21,14 +24,27 @@ module Ticket
21
24
  http_headers)
22
25
  end
23
26
 
27
+ def transition_to_not_done
28
+ not_done_status =
29
+ available_transitions.find do |transition|
30
+ transition.fetch("to").fetch("statusCategory").fetch("key") != "done"
31
+ end or
32
+ raise StatusTransitionerError, build_error_message("No non-done transition found")
33
+
34
+ transition_to(not_done_status.fetch("to").fetch("name"))
35
+ end
36
+
24
37
  def transition_payload_for(status)
25
38
  transition = available_transitions.find { |transition| transition.fetch("to").fetch("name") == status } or
26
- raise "No transition found for #{status.inspect} in " \
27
- "#{available_transitions.collect { |t| "#{t["name"]} -> #{t["to"]["name"]}" }.inspect}."
39
+ raise StatusTransitionerError, build_error_message("No transition found for #{status.inspect}")
28
40
 
29
41
  { transition: { id: transition["id"] } }.to_json
30
42
  end
31
43
 
44
+ def build_error_message(message)
45
+ message + " in #{available_transitions.collect { |t| "#{t["name"]} => #{t["to"]["name"]}" }.inspect}."
46
+ end
47
+
32
48
  def available_transitions
33
49
  response = tool.jira_client.get(tool.jira_url("/rest/api/2/issue/#{ticket.key}?expand=transitions"))
34
50
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ticket
4
4
  class Replicator
5
- VERSION = "1.2.1"
5
+ VERSION = "1.2.3"
6
6
  end
7
7
  end
@@ -43,34 +43,34 @@ module Ticket
43
43
 
44
44
  describe "#transformed_headers" do
45
45
  it "returns the headers" do
46
- expect(transformer.transformed_headers)
46
+ expect(transformer.send(:transformed_headers))
47
47
  .to eq(%w[ID Status Resolution Priority Summary] + ["Source Ticket URL"])
48
48
  end
49
49
  end
50
50
 
51
51
  describe "#transformed_rows" do
52
52
  before do
53
- allow(transformer).to receive_messages(extracted_rows: %i[a_row another_row])
53
+ allow(transformer).to receive_messages(extracted_rows: %i[a_row another_row], mappings: :mappings)
54
54
  end
55
55
 
56
56
  it "returns the rows" do
57
- allow(RowTransformer).to receive(:run_on).with(:a_row).and_return(:a_tranformed_row)
58
- allow(RowTransformer).to receive(:run_on).with(:another_row).and_return(:another_transformed_row)
57
+ allow(RowTransformer).to receive(:run_on).with(:a_row, :mappings).and_return(:a_tranformed_row)
58
+ allow(RowTransformer).to receive(:run_on).with(:another_row, :mappings).and_return(:another_transformed_row)
59
59
 
60
- expect(transformer.transformed_rows).to eq(%i[a_tranformed_row another_transformed_row])
60
+ expect(transformer.send(:transformed_rows)).to eq(%i[a_tranformed_row another_transformed_row])
61
61
  end
62
62
 
63
63
  context "when a row transformation error occurs" do
64
64
  before do
65
- allow(RowTransformer).to receive(:run_on).with(:a_row).and_return(:a_tranformed_row)
65
+ allow(RowTransformer).to receive(:run_on).with(:a_row, :mappings).and_return(:a_tranformed_row)
66
66
 
67
67
  allow(RowTransformer)
68
- .to receive(:run_on).with(:another_row)
68
+ .to receive(:run_on).with(:another_row, :mappings)
69
69
  .and_raise(TransformError, "transformation error")
70
70
  end
71
71
 
72
72
  it "raises an error message including the file name and line number of the row that was processed" do
73
- expect { transformer.transformed_rows }
73
+ expect { transformer.send(:transformed_rows) }
74
74
  .to raise_error(TransformError,
75
75
  /source\.csv:3: error while transforming row:\ntransformation error:\n:another_row\n/)
76
76
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticket
4
+ class Replicator
5
+ class Mappings
6
+ RSpec.describe Mappings do
7
+ let(:mappings) { described_class.new }
8
+
9
+ describe ".file_path" do
10
+ it { expect(described_class.file_path).to eq("config/ticket-replicator.mappings.yml") }
11
+ end
12
+
13
+ describe "#to_h" do
14
+ let(:mappings_yaml) do
15
+ <<~'YAML'
16
+ field_mapping:
17
+ id: Defect
18
+ summary: Defect (2)
19
+ id_extraction_regex: "\\((?<id>\\d+)\\)$"
20
+ status_mapping:
21
+ defaults_to: keep_original_value
22
+ Open: Open
23
+ Closed: Closed
24
+ resolution_mapping:
25
+ Closed: Done
26
+ Rejected: Won't Fix
27
+ team_mapping:
28
+ Source Team: Transformed Team
29
+ Front End: Web Team
30
+ priority_mapping:
31
+ 1 - Critical: Critical
32
+ 2 - High: High
33
+ 3 - Medium: Medium
34
+ 4 - Low: Low
35
+ YAML
36
+ end
37
+
38
+ let(:file_io) { instance_double(IO) }
39
+
40
+ it "loads the mappings from the mappings.yml file" do
41
+ expect(IO).to receive(:open).with("config/ticket-replicator.mappings.yml",
42
+ "r:bom|utf-8").and_yield(mappings_yaml)
43
+
44
+ expect(mappings.to_h)
45
+ .to eq(
46
+ "field_mapping" => { "id" => "Defect", "summary" => "Defect (2)" },
47
+ "id_extraction_regex" => "\\((?<id>\\d+)\\)$",
48
+ "status_mapping" => { "defaults_to" => "keep_original_value", "Open" => "Open",
49
+ "Closed" => "Closed" },
50
+ "resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" },
51
+ "team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" },
52
+ "priority_mapping" =>
53
+ { "1 - Critical" => "Critical", "2 - High" => "High", "3 - Medium" => "Medium",
54
+ "4 - Low" => "Low" }
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -149,6 +149,8 @@ module Ticket
149
149
  ticket: ticket,
150
150
  :ticket_fields_need_to_be_updated? => ticket_fields_need_to_be_updated?
151
151
  )
152
+
153
+ allow(jira_project).to receive_messages(replicated_tickets: { "456" => :another_ticket })
152
154
  end
153
155
 
154
156
  context "when the previously replicated needs to be updated " do
@@ -156,10 +158,14 @@ module Ticket
156
158
 
157
159
  it do
158
160
  allow(loader).to receive_messages(attributes_for_save: { fields: { a: "b" } })
159
- expect(jira_ticket).to receive(:save!).with({ fields: { a: "b" } })
160
161
 
162
+ expect(loader).to receive(:reopen_ticket_if_status_in_done_category)
163
+ expect(jira_ticket).to receive(:save!).with({ fields: { a: "b" } })
161
164
  expect(jira_ticket).to receive(:fetch)
162
165
 
166
+ # TODO: fix this check that should work
167
+ # expect(loader).to be_ticket_previously_replicated
168
+
163
169
  loader.save_ticket
164
170
  end
165
171
  end
@@ -176,6 +182,52 @@ module Ticket
176
182
  end
177
183
  end
178
184
 
185
+ describe "#reopen_ticket_if_status_in_done_category" do
186
+ let(:loader) { described_class.send(:new, jira_project, double("row", id: "123", status: "Closed")) }
187
+ let(:ticket) { instance_double(Ticket) }
188
+
189
+ before do
190
+ allow(loader).to receive_messages(ticket: ticket,
191
+ :ticket_previously_replicated? => actual_ticket_previously_replicated)
192
+ end
193
+
194
+ context "when the ticket was not previously replicated" do
195
+ let(:actual_ticket_previously_replicated) { false }
196
+
197
+ it do
198
+ expect(ticket).not_to receive(:transition_to_not_done)
199
+
200
+ loader.send(:reopen_ticket_if_status_in_done_category)
201
+ end
202
+ end
203
+
204
+ context "when the ticket was previously replicated" do
205
+ let(:actual_ticket_previously_replicated) { true }
206
+
207
+ before { allow(ticket).to receive_messages(status_in_done_category?: actual_status_in_done_category) }
208
+
209
+ context "when the ticket is not closed" do
210
+ let(:actual_status_in_done_category) { false }
211
+
212
+ it do
213
+ expect(ticket).not_to receive(:transition_to_not_done)
214
+
215
+ loader.send(:reopen_ticket_if_status_in_done_category)
216
+ end
217
+ end
218
+
219
+ context "when the ticket is closed" do
220
+ let(:actual_status_in_done_category) { true }
221
+
222
+ it do
223
+ expect(ticket).to receive(:transition_to_not_done)
224
+
225
+ loader.send(:reopen_ticket_if_status_in_done_category)
226
+ end
227
+ end
228
+ end
229
+ end
230
+
179
231
  describe "#attributes_for_save" do
180
232
  let(:loader) { described_class.send(:new, jira_project, row) }
181
233
 
@@ -405,6 +457,24 @@ module Ticket
405
457
  it { expect(loader.send(:source_ticket_link_title)).to eq("Source Ticket 123") }
406
458
  end
407
459
 
460
+ describe "#only_update_links_for_modified_ticket?" do
461
+ it "defaults to true when unset" do
462
+ expect(ENV)
463
+ .to receive(:fetch).with("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS",
464
+ "true").and_return("true")
465
+
466
+ expect(loader.send(:only_update_links_for_modified_ticket?)).to be_truthy
467
+ end
468
+
469
+ it "when TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS set to false" do
470
+ expect(ENV)
471
+ .to receive(:fetch).with("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS", "true")
472
+ .and_return("false")
473
+
474
+ expect(loader.send(:only_update_links_for_modified_ticket?)).to be_falsey
475
+ end
476
+ end
477
+
408
478
  describe "#update_source_ticket_remote_link" do
409
479
  before do
410
480
  allow(loader).to receive_messages(source_ticket_link_needs_update?: needs_update)
@@ -449,57 +519,87 @@ module Ticket
449
519
 
450
520
  describe "#source_ticket_link_needs_update?" do
451
521
  before do
452
- allow(ticket).to receive_messages(source_ticket_link: remote_link)
522
+ allow(loader).to receive_messages(only_update_links_for_modified_ticket?: only_update_links_for_new_ticket)
453
523
  end
454
524
 
455
- context "when link has yet to be created" do
456
- let(:remote_link) { nil }
525
+ context "when only for modified tickets" do
526
+ let(:only_update_links_for_new_ticket) { true }
527
+
528
+ before { allow(loader).to receive_messages(ticket_was_just_updated?: actual_was_updated) }
529
+
530
+ context "when the ticket has already been replicated (i.e., exists)" do
531
+ let(:actual_was_updated) { false }
532
+
533
+ it "does not request the remote link information" do
534
+ expect(ticket).not_to receive(:source_ticket_link)
457
535
 
458
- it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
536
+ expect(loader.send(:source_ticket_link_needs_update?)).to be_falsey
537
+ end
538
+ end
539
+
540
+ context "when the ticket is new" do
541
+ let(:actual_was_updated) { true }
542
+
543
+ it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
544
+ end
459
545
  end
460
546
 
461
- context "when link already exists" do
462
- let(:remote_link) do
463
- instance_double(JIRA::Resource::Remotelink,
464
- attrs: source_ticket_link,
465
- inspect: "<JIRA::Resource::Remotelink attrs: #{source_ticket_link}>")
547
+ context "when not only for new tickets" do
548
+ let(:only_update_links_for_new_ticket) { false }
549
+
550
+ before do
551
+ allow(ticket).to receive_messages(source_ticket_link: remote_link)
466
552
  end
467
553
 
468
- context "when the link is not up to date" do
469
- let(:source_ticket_link) { build_link(existing_url, existing_title) }
554
+ context "when link has yet to be created" do
555
+ let(:remote_link) { nil }
470
556
 
471
- context "when url has changed" do
472
- let(:existing_url) { "https://__OLD_URL__/to/source/ticket/1234" }
473
- let(:existing_title) { "Source Ticket 123" }
557
+ it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
558
+ end
474
559
 
475
- it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
560
+ context "when link already exists" do
561
+ let(:remote_link) do
562
+ instance_double(JIRA::Resource::Remotelink,
563
+ attrs: source_ticket_link,
564
+ inspect: "<JIRA::Resource::Remotelink attrs: #{source_ticket_link}>")
476
565
  end
477
566
 
478
- context "when title has changed" do
479
- let(:existing_url) { "https://url/to/source/ticket/123" }
480
- let(:existing_title) { "__OLD TITLE__Source Ticket 1234" }
567
+ context "when the link is not up to date" do
568
+ let(:source_ticket_link) { build_link(existing_url, existing_title) }
481
569
 
482
- it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
483
- end
484
- end
570
+ context "when url has changed" do
571
+ let(:existing_url) { "https://__OLD_URL__/to/source/ticket/1234" }
572
+ let(:existing_title) { "Source Ticket 123" }
573
+
574
+ it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
575
+ end
485
576
 
486
- context "when the link is up to date" do
487
- let(:existing_url) { "https://url/to/source/ticket/123" }
488
- let(:existing_title) { "Source Ticket 123" }
489
- let(:source_ticket_link) do
490
- { "object" => {
491
- "url" => "https://url/to/source/ticket/123",
492
- "title" => "Source Ticket 123",
493
- "icon" => { "title" => "link", "url16x16" => "https://url/to/source/ticket/123/icon" }
494
- },
495
- "application" => { "name" => "Ticket Source" },
496
- "extra_attributes" =>
497
- { "id" => 10_443, "self" => "https://url/to/source/ticket/123/remotelink/10443" } }
577
+ context "when title has changed" do
578
+ let(:existing_url) { "https://url/to/source/ticket/123" }
579
+ let(:existing_title) { "__OLD TITLE__Source Ticket 1234" }
580
+
581
+ it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
582
+ end
498
583
  end
499
584
 
500
- it("only checks the needed attributes") {
501
- expect(loader.send(:source_ticket_link_needs_update?)).to be_falsey
502
- }
585
+ context "when the link is up to date" do
586
+ let(:existing_url) { "https://url/to/source/ticket/123" }
587
+ let(:existing_title) { "Source Ticket 123" }
588
+ let(:source_ticket_link) do
589
+ { "object" => {
590
+ "url" => "https://url/to/source/ticket/123",
591
+ "title" => "Source Ticket 123",
592
+ "icon" => { "title" => "link", "url16x16" => "https://url/to/source/ticket/123/icon" }
593
+ },
594
+ "application" => { "name" => "Ticket Source" },
595
+ "extra_attributes" =>
596
+ { "id" => 10_443, "self" => "https://url/to/source/ticket/123/remotelink/10443" } }
597
+ end
598
+
599
+ it("only checks the needed attributes") {
600
+ expect(loader.send(:source_ticket_link_needs_update?)).to be_falsey
601
+ }
602
+ end
503
603
  end
504
604
  end
505
605
  end
@@ -519,6 +619,44 @@ module Ticket
519
619
  end
520
620
  end
521
621
  end
622
+
623
+ describe "#ticket_was_just_updated?" do
624
+ let(:loader) { described_class.send(:new, jira_project, { id: 789 }) }
625
+
626
+ context "when the ticket was just created" do
627
+ it do
628
+ allow(loader).to receive_messages(ticket_previously_replicated?: false)
629
+ allow(loader).to receive_messages(create_ticket: nil)
630
+
631
+ loader.send(:ticket)
632
+
633
+ expect(loader.send(:ticket_was_just_updated?)).to be_truthy
634
+ end
635
+ end
636
+
637
+ context "when the ticket had modified fields" do
638
+ let(:jira_ticket) { double(JIRA::Resource::Issue, save!: nil, fetch: nil) }
639
+ let(:ticket) { instance_double(Ticket, jira_ticket: jira_ticket) }
640
+
641
+ it do
642
+ allow(loader)
643
+ .to receive_messages(ticket_fields_need_to_be_updated?: true,
644
+ reopen_ticket_if_status_in_done_category: nil,
645
+ ticket: ticket,
646
+ attributes_for_save: {})
647
+
648
+ allow(jira_project).to receive_messages(replicated_tickets: { "789" => "a_ticket" })
649
+
650
+ loader.send(:save_ticket)
651
+
652
+ expect(loader.send(:ticket_was_just_updated?)).to be_truthy
653
+ end
654
+ end
655
+
656
+ context "when the ticket was not new or had modified fields" do
657
+ it { expect(loader.send(:ticket_was_just_updated?)).to be_falsey }
658
+ end
659
+ end
522
660
  end
523
661
  # rubocop:enable Metrics/ClassLength
524
662
  end
@@ -7,17 +7,18 @@ module Ticket
7
7
  # rubocop:disable Metrics/ClassLength
8
8
  class RowTransformer
9
9
  RSpec.describe RowTransformer do
10
- let(:transformer) { described_class.send(:new, extracted_row) }
10
+ let(:transformer) { described_class.send(:new, extracted_row, mappings) }
11
+ let(:mappings) { instance_double(Mappings, to_h: {}) }
11
12
 
12
13
  describe ".run_on" do
13
14
  let(:extracted_row) { :a_row_to_transform }
14
15
 
15
16
  it "transforms the given file" do
16
- expect(described_class).to receive(:new).with(:a_row_to_transform).and_return(transformer)
17
+ expect(described_class).to receive(:new).with(:a_row_to_transform, mappings).and_return(transformer)
17
18
 
18
19
  expect(transformer).to receive(:run)
19
20
 
20
- described_class.run_on(:a_row_to_transform)
21
+ described_class.run_on(:a_row_to_transform, mappings)
21
22
  end
22
23
  end
23
24
 
@@ -46,50 +47,13 @@ module Ticket
46
47
  end
47
48
 
48
49
  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
72
-
73
50
  let(:extracted_row) { {} }
51
+ let(:mappings) { instance_double(Mappings, to_h: { "field_mapping" => {} }) }
52
+
53
+ it "uses the mapping object" do
54
+ RowTransformer.send(:new, extracted_row, mappings)
74
55
 
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
- )
56
+ expect(transformer.mappings).to eq({ "field_mapping" => {} })
93
57
  end
94
58
  end
95
59
 
@@ -217,6 +217,28 @@ module Ticket
217
217
  it { expect(ticket.status).to eq("To Do") }
218
218
  end
219
219
 
220
+ describe "#status_in_done_category?" do
221
+ let(:jira_ticket) do
222
+ double(JIRA::Resource::Issue, status: actual_status)
223
+ end
224
+
225
+ let(:actual_status) do
226
+ double(JIRA::Resource::Status, attrs: { "name" => "A status", "statusCategory" => actual_status_category })
227
+ end
228
+
229
+ context "when status is in done category" do
230
+ let(:actual_status_category) { { "key" => "done", "name" => "Done" } }
231
+
232
+ it { expect(ticket.status_in_done_category?).to be_truthy }
233
+ end
234
+
235
+ context "when status is not in done category" do
236
+ let(:actual_status_category) { { "key" => "new", "name" => "To Do" } }
237
+
238
+ it { expect(ticket.status_in_done_category?).to be_falsy }
239
+ end
240
+ end
241
+
220
242
  describe "#resolution" do
221
243
  it { expect(ticket.resolution).to eq("") }
222
244
  end
@@ -280,7 +302,8 @@ module Ticket
280
302
 
281
303
  describe "#transition_to" do
282
304
  let(:transitioner) { instance_double(TicketStatusTransitioner) }
283
- it do
305
+
306
+ it "uses a status transitioner" do
284
307
  expect(TicketStatusTransitioner).to receive(:new).with(ticket).and_return(transitioner)
285
308
  expect(transitioner).to receive(:transition_to).with("Testing")
286
309
 
@@ -288,6 +311,17 @@ module Ticket
288
311
  end
289
312
  end
290
313
 
314
+ describe "#transition_to_not_done" do
315
+ let(:transitioner) { instance_double(TicketStatusTransitioner) }
316
+
317
+ it "uses a status transitioner" do
318
+ expect(TicketStatusTransitioner).to receive(:new).with(ticket).and_return(transitioner)
319
+ expect(transitioner).to receive(:transition_to_not_done)
320
+
321
+ ticket.transition_to_not_done
322
+ end
323
+ end
324
+
291
325
  describe "#sanitize" do
292
326
  let(:expectations) do
293
327
  [
@@ -311,4 +345,5 @@ module Ticket
311
345
  end
312
346
  end
313
347
  end
348
+
314
349
  # rubocop:enable Metrics/ClassLength
@@ -6,7 +6,7 @@ require "ticket/replicator/ticket_status_transitioner"
6
6
  module Ticket
7
7
  class Replicator
8
8
  RSpec.describe TicketStatusTransitioner do
9
- let(:transitioner) { described_class.send(:new, ticket) }
9
+ let(:transitioner) { described_class.new(ticket) }
10
10
 
11
11
  let(:ticket) do
12
12
  double(Ticket, key: "PROJKEY-16", status: "Todo", jira_auto_tool: jira_auto_tool, jira_client: jira_client)
@@ -15,7 +15,7 @@ module Ticket
15
15
  let(:jira_auto_tool) { instance_double(Jira::Auto::Tool, jira_client: jira_client) }
16
16
  let(:jira_client) { instance_double(JIRA::Client) }
17
17
 
18
- describe "#transistion_to" do
18
+ describe "#transition_to" do
19
19
  let(:expected_payload) { { transition: { id: 131 } }.to_json }
20
20
  let(:expected_headers) { { "Content-Type" => "application/json" } }
21
21
 
@@ -48,6 +48,53 @@ module Ticket
48
48
  end
49
49
  end
50
50
 
51
+ describe "#transition_to_not_done" do
52
+ let(:available_transitions) do
53
+ [
54
+ { "name" => "Withdraw",
55
+ "to" => { "name" => "Withdrawn",
56
+ "statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } },
57
+ { "name" => "Set As Defect Correction in Process",
58
+ "to" => {
59
+ "name" => "Defect Correction in Process",
60
+ "statusCategory" => { "key" => "indeterminate", "colorName" => "yellow", "name" => "In Progress" }
61
+ } },
62
+ { "name" => "Confirm",
63
+ "to" => { "name" => "Confirmed",
64
+ "statusCategory" => { "key" => "new", "colorName" => "blue-gray", "name" => "To Do" } } }
65
+ ]
66
+ end
67
+
68
+ before do
69
+ expect(transitioner).to receive(:available_transitions).at_least(:once).and_return(available_transitions)
70
+ end
71
+
72
+ it "finds a non-done transition and transitions to it" do
73
+ expect(transitioner).to receive(:transition_to).with("Defect Correction in Process")
74
+
75
+ transitioner.transition_to_not_done
76
+ end
77
+
78
+ context "when no non-done transition is found" do
79
+ let(:available_transitions) do
80
+ [
81
+ { "name" => "Withdraw",
82
+ "to" => { "name" => "Withdrawn",
83
+ "statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } },
84
+ { "name" => "Set To Done",
85
+ "to" => { "name" => "Done",
86
+ "statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } }
87
+ ]
88
+ end
89
+
90
+ it "transitions to the first done transition" do
91
+ expect { transitioner.transition_to_not_done }
92
+ .to raise_error(TicketStatusTransitioner::StatusTransitionerError,
93
+ /No non-done transition found in .+/)
94
+ end
95
+ end
96
+ end
97
+
51
98
  describe "#transition_payload_for" do
52
99
  let(:available_transitions) do
53
100
  [
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.2.1
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christophe Broult
@@ -183,6 +183,7 @@ files:
183
183
  - lib/ticket/replicator/file_transformer/for_csv.rb
184
184
  - lib/ticket/replicator/file_transformer/for_xlsx.rb
185
185
  - lib/ticket/replicator/jira_project.rb
186
+ - lib/ticket/replicator/mappings.rb
186
187
  - lib/ticket/replicator/replicated_summary.rb
187
188
  - lib/ticket/replicator/row_loader.rb
188
189
  - lib/ticket/replicator/row_transformer.rb
@@ -199,6 +200,7 @@ files:
199
200
  - spec/ticket/replicator/file_transformer/for_xlsx_spec.rb
200
201
  - spec/ticket/replicator/file_transformer_spec.rb
201
202
  - spec/ticket/replicator/jira_project_spec.rb
203
+ - spec/ticket/replicator/mappings_spec.rb
202
204
  - spec/ticket/replicator/replicated_summary_spec.rb
203
205
  - spec/ticket/replicator/row_loader_spec.rb
204
206
  - spec/ticket/replicator/row_transformer_spec.rb