ticket-replicator 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/Guardfile +158 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +136 -0
  9. data/Rakefile +23 -0
  10. data/bin/ticket-replicator +67 -0
  11. data/config/examples/ticket-replicator.mappings.yml +54 -0
  12. data/cucumber.yml +7 -0
  13. data/features/extract-sap-solution-manager-defect-tickets.feature +45 -0
  14. data/features/load_tickets_in_jira.feature +129 -0
  15. data/features/setup_ticket_replicator.feature +85 -0
  16. data/features/step_definitions/anonymized_sample.xlsx +0 -0
  17. data/features/step_definitions/anonymized_sample.xlsx:Zone.Identifier +3 -0
  18. data/features/step_definitions/execution_context_steps.rb +13 -0
  19. data/features/step_definitions/extract_defect_tickets_from_sap_solution_manager_steps.rb.rb +29 -0
  20. data/features/step_definitions/load_tickets_in_jira_steps.rb +47 -0
  21. data/features/step_definitions/transform_solution_manager_tickets_steps.rb +21 -0
  22. data/features/support/10.setup_cucumber.rb +10 -0
  23. data/features/support/env.rb +15 -0
  24. data/features/support/hooks.rb +13 -0
  25. data/features/support/manage_mock_sap_solution_manager.rb.DISABLED +12 -0
  26. data/features/support/mocks/mock_defect_ticket_server.rb.DISABLED +251 -0
  27. data/features/support/setup_rspec.rb +15 -0
  28. data/features/support/setup_simplecov.rb +5 -0
  29. data/features/transform-solution-manager-tickets-into-jira-loadable-tickets.feature +313 -0
  30. data/features/transform_and_load_extracted_ticket_queue.feature +121 -0
  31. data/lib/tasks/version.rake +55 -0
  32. data/lib/ticket/replicator/defect_export_automation.rb.DISABLED +128 -0
  33. data/lib/ticket/replicator/file_loader.rb +46 -0
  34. data/lib/ticket/replicator/file_replicator.rb +67 -0
  35. data/lib/ticket/replicator/file_transformer/for_csv.rb +22 -0
  36. data/lib/ticket/replicator/file_transformer/for_xlsx.rb +34 -0
  37. data/lib/ticket/replicator/file_transformer.rb +70 -0
  38. data/lib/ticket/replicator/jira_project.rb +65 -0
  39. data/lib/ticket/replicator/replicated_summary.rb +73 -0
  40. data/lib/ticket/replicator/row_loader.rb +109 -0
  41. data/lib/ticket/replicator/row_transformer.rb +126 -0
  42. data/lib/ticket/replicator/s_a_p_solution_manager_client.rb.DISABLED +169 -0
  43. data/lib/ticket/replicator/setup.rb +49 -0
  44. data/lib/ticket/replicator/ticket.rb +70 -0
  45. data/lib/ticket/replicator/ticket_status_transitioner.rb +45 -0
  46. data/lib/ticket/replicator/version.rb +7 -0
  47. data/lib/ticket/replicator.rb +90 -0
  48. data/sig/ticket/replicator.rbs +6 -0
  49. data/spec/spec_helper.rb +19 -0
  50. data/spec/ticket/replicator/file_loader_spec.rb +77 -0
  51. data/spec/ticket/replicator/file_replicator_spec.rb +153 -0
  52. data/spec/ticket/replicator/file_transformer/for_csv_spec.rb +52 -0
  53. data/spec/ticket/replicator/file_transformer/for_xlsx_spec.rb +52 -0
  54. data/spec/ticket/replicator/file_transformer_spec.rb +83 -0
  55. data/spec/ticket/replicator/jira_project_spec.rb +127 -0
  56. data/spec/ticket/replicator/replicated_summary_spec.rb +70 -0
  57. data/spec/ticket/replicator/row_loader_spec.rb +245 -0
  58. data/spec/ticket/replicator/row_transformer_spec.rb +234 -0
  59. data/spec/ticket/replicator/setup_spec.rb +80 -0
  60. data/spec/ticket/replicator/ticket_spec.rb +244 -0
  61. data/spec/ticket/replicator/ticket_status_transitioner_spec.rb +123 -0
  62. data/spec/ticket/replicator_spec.rb +137 -0
  63. data/transformed_file1 +1 -0
  64. metadata +235 -0
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ticket/replicator/jira_project"
4
+
5
+ module Ticket
6
+ class Replicator
7
+ class RowLoader
8
+ def self.run_on(jira_project, row)
9
+ new(jira_project, row).run
10
+ end
11
+
12
+ private_class_method :new
13
+
14
+ attr_reader :jira_project, :row
15
+
16
+ def initialize(jira_project, row)
17
+ @jira_project = jira_project
18
+ @row = row
19
+ end
20
+
21
+ def run
22
+ save_ticket
23
+ transition_ticket_to_the_expected_status
24
+ end
25
+
26
+ def ticket_previously_replicated?
27
+ jira_project.replicated_tickets.key?(id)
28
+ end
29
+
30
+ def save_ticket
31
+ return unless ticket_fields_need_to_be_updated?
32
+
33
+ ticket.jira_ticket.save!({
34
+ fields: {
35
+ project: { key: jira_project.project_key },
36
+ issuetype: { name: jira_project.ticket_type_name },
37
+ # resolution: { name: resolution },
38
+ priority: { name: priority },
39
+ # implementation_team: { name: team },
40
+ summary: summary
41
+ }
42
+ })
43
+
44
+ ticket.jira_ticket.fetch
45
+ end
46
+
47
+ def ticket_fields_need_to_be_updated?
48
+ !ticket_previously_replicated? || ticket_fields_changed?
49
+ end
50
+
51
+ def ticket_fields_changed?
52
+ fields_to_save.any? { |field| ticket.send(field) != send(field) }
53
+ end
54
+
55
+ def fields_to_save
56
+ %i[summary priority]
57
+ end
58
+
59
+ def transition_ticket_to_the_expected_status
60
+ ticket.transition_to(status)
61
+ end
62
+
63
+ def ticket
64
+ @ticket ||=
65
+ if ticket_previously_replicated?
66
+ fetch_ticket
67
+ else
68
+ create_ticket
69
+ end
70
+ end
71
+
72
+ def fetch_ticket
73
+ jira_project.replicated_tickets.fetch(id)
74
+ end
75
+
76
+ def create_ticket
77
+ Replicator::Ticket.new(jira_project.jira_auto_tool, jira_project.jira_client.Issue.build)
78
+ end
79
+
80
+ private
81
+
82
+ def method_missing(name, *args)
83
+ if field_to_load?(name)
84
+ row.fetch(name) { |key| raise "No value found for #{key.inspect} in #{row.inspect}" }
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def respond_to_missing?(name, *args)
91
+ field_to_load?(name) || super
92
+ end
93
+
94
+ def field_to_load?(field)
95
+ fields_to_load.include?(field)
96
+ end
97
+
98
+ def fields_to_load
99
+ self.class.fields_to_load
100
+ end
101
+
102
+ class << self
103
+ def fields_to_load
104
+ @fields_to_load ||= RowTransformer.fields_to_transform.collect { |field| field.to_s.downcase.to_sym }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ticket/replicator/replicated_summary"
4
+
5
+ module Ticket
6
+ class Replicator
7
+ class RowTransformer
8
+ def self.run_on(extracted_row)
9
+ new(extracted_row).run
10
+ end
11
+
12
+ private_class_method :new
13
+
14
+ attr_reader :extracted_row
15
+
16
+ def initialize(extracted_row)
17
+ @extracted_row = extracted_row
18
+ end
19
+
20
+ def run
21
+ fields_to_transform.collect { |field| send("transformed_#{field.to_s.downcase}") }
22
+ end
23
+
24
+ def transformed_id
25
+ remapped_field_extracted_value_for :id
26
+ end
27
+
28
+ def transformed_status
29
+ mapped_value_for :status
30
+ end
31
+
32
+ def transformed_resolution
33
+ mapped_value_for :resolution, remapped_field_extracted_value_for(:status)
34
+ end
35
+
36
+ def transformed_priority
37
+ mapped_value_for :priority
38
+ end
39
+
40
+ def transformed_team
41
+ mapped_value_for :team
42
+ end
43
+
44
+ def transformed_summary
45
+ ReplicatedSummary.build(remapped_field_extracted_value_for(:id),
46
+ remapped_field_extracted_value_for(:summary)).to_s
47
+ end
48
+
49
+ def fields_to_transform
50
+ self.class.fields_to_transform
51
+ end
52
+
53
+ def self.fields_to_transform
54
+ %i[ID Status Resolution Priority Team Summary]
55
+ end
56
+
57
+ def mapped_value_for(field, extracted_value = remapped_field_extracted_value_for(field))
58
+ mapping_for(field).fetch(extracted_value) do |key|
59
+ unless mapping_for(field)["defaults_to"] == "keep_original_value"
60
+ raise "No mapping found for #{field.inspect} = #{key.inspect} in #{mapping_for(field).inspect}"
61
+ end
62
+
63
+ extracted_value
64
+ end
65
+ end
66
+
67
+ def remapped_field_extracted_value_for(field)
68
+ remapped_field_extracted_row.fetch(remapped_field_key(field)) do |key|
69
+ raise "No value found for #{key.inspect} in #{remapped_field_extracted_row.inspect}"
70
+ end
71
+ end
72
+
73
+ def mappings
74
+ @mappings ||= YAML.safe_load_file(mapping_file_path, symbolize_names: false)
75
+ end
76
+
77
+ def remapped_field_extracted_row
78
+ @remapped_field_extracted_row ||= fields_to_transform.to_h do |field|
79
+ mapped_field_key = remapped_field_key(field)
80
+ [mapped_field_key, extracted_row_value_for(mapped_field_key)]
81
+ end
82
+ end
83
+
84
+ def extracted_field_mapping_for(field)
85
+ original_field = extracted_field_mapping.fetch(remapped_field_key(field)) do |key|
86
+ raise "No mapping found for #{key.inspect} in #{extracted_field_mapping.inspect}"
87
+ end
88
+
89
+ remapped_field_key(original_field)
90
+ end
91
+
92
+ def remapped_field_key(field)
93
+ field.to_s.downcase
94
+ end
95
+
96
+ def extracted_field_mapping
97
+ @extracted_field_mapping ||= mapping_for :field
98
+ end
99
+
100
+ def self.mapping_file_path
101
+ File.join("config", "ticket-replicator.mappings.yml")
102
+ end
103
+
104
+ private
105
+
106
+ def extracted_row_value_for(mapped_field_key)
107
+ extracted_field_mapping = extracted_field_mapping_for(mapped_field_key)
108
+
109
+ extracted_row.fetch(extracted_field_mapping) do
110
+ raise "No value found for #{mapped_field_key.inspect} as " \
111
+ "#{extracted_field_mapping.inspect} in #{extracted_row.inspect}"
112
+ end
113
+ end
114
+
115
+ def mapping_for(field)
116
+ mappings.fetch("#{field.to_s.downcase}_mapping") do |key|
117
+ raise "No mapping found for #{key.inspect} in #{mappings.inspect}"
118
+ end
119
+ end
120
+
121
+ def mapping_file_path
122
+ self.class.mapping_file_path
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ class SAPSolutionManagerClient
8
+ HTTP_POST = Net::HTTP::Post::METHOD
9
+ HTTP_GET = Net::HTTP::Get::METHOD
10
+ HTTP_PUT = Net::HTTP::Put::METHOD
11
+ HTTP_DELETE = Net::HTTP::Delete::METHOD
12
+ CONTENT_TYPE_JSON = "application/json"
13
+ HTTP_OK = Net::HTTPSuccess::CODE
14
+ HTTP_CREATED = Net::HTTPCreated::CODE
15
+ HTTP_NO_CONTENT = Net::HTTPNoContent::CODE
16
+ API_AUTH_PATH = "/api/auth"
17
+ API_TICKETS_PATH = "/api/defect_tickets"
18
+ API_EXPORT_PATH = "/api/export/defect_tickets"
19
+ DEFAULT_TIMEOUT = 30
20
+ DEFAULT_FORMAT = "json"
21
+ DEFAULT_BASE_URL = "http://localhost:4567"
22
+
23
+ attr_reader :base_url, :auth_token
24
+
25
+ def initialize(base_url = DEFAULT_BASE_URL)
26
+ @base_url = base_url
27
+ @auth_token = nil
28
+ end
29
+
30
+ def authenticate(username, password)
31
+ uri = URI("#{@base_url}#{API_AUTH_PATH}")
32
+
33
+ request = Net::HTTP::Post.new(uri)
34
+ request.content_type = CONTENT_TYPE_JSON
35
+ request.body = { username: username, password: password }.to_json
36
+
37
+ response = send_request(uri, request)
38
+
39
+ if response.code == HTTP_OK
40
+ data = JSON.parse(response.body)
41
+ @auth_token = data["token"]
42
+ true
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ def get_tickets(params = {})
49
+ query_params = params.map { |k, v| "#{k}=#{v}" }.join("&")
50
+ uri = URI("#{@base_url}/api/defect_tickets")
51
+ uri.query = query_params unless query_params.empty?
52
+
53
+ request = Net::HTTP::Get.new(uri)
54
+ add_auth_header(request)
55
+
56
+ response = send_request(uri, request)
57
+
58
+ if response.code == "200"
59
+ JSON.parse(response.body)
60
+ else
61
+ handle_error_response(response)
62
+ end
63
+ end
64
+
65
+ def get_ticket(id)
66
+ uri = URI("#{@base_url}/api/defect_tickets/#{id}")
67
+
68
+ request = Net::HTTP.const_get(HTTP_GET).new(uri)
69
+ add_auth_header(request)
70
+
71
+ response = send_request(uri, request)
72
+
73
+ if response.code == HTTP_OK
74
+ JSON.parse(response.body)
75
+ else
76
+ handle_error_response(response)
77
+ end
78
+ end
79
+
80
+ def create_ticket(attributes)
81
+ uri = URI("#{@base_url}#{API_TICKETS_PATH}")
82
+
83
+ request = Net::HTTP::Post.new(uri)
84
+ request.content_type = "application/json"
85
+ request.body = attributes.to_json
86
+ add_auth_header(request)
87
+
88
+ response = send_request(uri, request)
89
+
90
+ if response.code == HTTP_CREATED
91
+ JSON.parse(response.body)
92
+ else
93
+ handle_error_response(response)
94
+ end
95
+ end
96
+
97
+ def update_ticket(id, attributes)
98
+ uri = URI("#{@base_url}/api/defect_tickets/#{id}")
99
+
100
+ request = Net::HTTP.const_get(HTTP_PUT).new(uri)
101
+ request.content_type = CONTENT_TYPE_JSON
102
+ request.body = attributes.to_json
103
+ add_auth_header(request)
104
+
105
+ response = send_request(uri, request)
106
+
107
+ if response.code == HTTP_OK
108
+ JSON.parse(response.body)
109
+ else
110
+ handle_error_response(response)
111
+ end
112
+ end
113
+
114
+ def delete_ticket(id)
115
+ uri = URI("#{@base_url}/api/defect_tickets/#{id}")
116
+
117
+ request = Net::HTTP::Delete.new(uri)
118
+ add_auth_header(request)
119
+
120
+ response = send_request(uri, request)
121
+
122
+ response.code == HTTP_NO_CONTENT
123
+ end
124
+
125
+ def export_tickets(format = DEFAULT_FORMAT, params = {})
126
+ query_params = params.map { |k, v| "#{k}=#{v}" }.join("&")
127
+ query_params += "&format=#{format}"
128
+
129
+ uri = URI("#{@base_url}#{API_EXPORT_PATH}")
130
+ uri.query = query_params
131
+
132
+ request = Net::HTTP::Get.new(uri)
133
+ add_auth_header(request)
134
+
135
+ response = send_request(uri, request)
136
+
137
+ if response.code == HTTP_OK
138
+ if format == "json"
139
+ JSON.parse(response.body)
140
+ else
141
+ response.body
142
+ end
143
+ else
144
+ handle_error_response(response)
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def add_auth_header(request)
151
+ request["Authorization"] = "Bearer #{@auth_token}" if @auth_token
152
+ end
153
+
154
+ def send_request(uri, request)
155
+ http = Net::HTTP.new(uri.host, uri.port)
156
+ http.read_timeout = DEFAULT_TIMEOUT
157
+ http.open_timeout = DEFAULT_TIMEOUT
158
+ http.use_ssl = (uri.scheme == "https")
159
+
160
+ http.request(request)
161
+ end
162
+
163
+ def handle_error_response(response)
164
+ error_data = JSON.parse(response.body)
165
+ raise "API Error (#{response.code}): #{error_data["error"]}"
166
+ rescue JSON::ParserError
167
+ raise "API Error (#{response.code}): #{response.body}"
168
+ end
169
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticket
4
+ class Replicator
5
+ class Setup
6
+ SetupError = Class.new(StandardError)
7
+
8
+ attr_reader :replicator
9
+
10
+ def initialize(replicator)
11
+ @replicator = replicator
12
+ end
13
+
14
+ def run
15
+ create_queue
16
+ create_initial_configuration
17
+ end
18
+
19
+ private
20
+
21
+ def create_queue
22
+ log.info "Created folder #{replicator.extracted_folder.inspect}"
23
+ end
24
+
25
+ def create_initial_configuration
26
+ if File.exist?(config_file_path)
27
+ raise SetupError, "Not overwriting existing config file #{config_file_path.inspect}"
28
+ end
29
+
30
+ FileUtils.cp(example_config_file_path, config_file_path)
31
+
32
+ log.info { "Created file #{config_file_path.inspect}" }
33
+ end
34
+
35
+ def config_file_path
36
+ @config_file_path ||=
37
+ begin
38
+ path = RowTransformer.mapping_file_path
39
+ FileUtils.mkdir_p(File.dirname(path))
40
+ path
41
+ end
42
+ end
43
+
44
+ def example_config_file_path
45
+ File.join(replicator.home_dir, "config", "examples", File.basename(config_file_path))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ticket_status_transitioner"
4
+
5
+ module Ticket
6
+ class Replicator
7
+ class Ticket
8
+ attr_reader :jira_auto_tool, :jira_ticket
9
+
10
+ def initialize(jira_auto_tool, jira_ticket)
11
+ @jira_auto_tool = jira_auto_tool
12
+ @jira_ticket = jira_ticket
13
+ end
14
+
15
+ def delete
16
+ jira_ticket.delete
17
+ end
18
+
19
+ def jira_client
20
+ jira_auto_tool.jira_client
21
+ end
22
+
23
+ def transition_to(status)
24
+ TicketStatusTransitioner.new(self).transition_to(status)
25
+ end
26
+
27
+ def <=>(other)
28
+ cmp_values(self) <=> cmp_values(other)
29
+ end
30
+
31
+ def replicated?
32
+ ReplicatedSummary.match?(summary)
33
+ end
34
+
35
+ def source_id
36
+ ReplicatedSummary.parse(summary).source_id
37
+ end
38
+
39
+ private
40
+
41
+ def cmp_values(object)
42
+ object&.source_id
43
+ end
44
+
45
+ def method_missing(name, *args)
46
+ if jira_ticket.respond_to?(name)
47
+ value = jira_ticket.send(name)
48
+
49
+ sanitize(value)
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ def sanitize(value)
56
+ if value.respond_to?(:name)
57
+ value.name
58
+ elsif value.respond_to?(:value)
59
+ value.value
60
+ else
61
+ value.nil? ? "" : value.to_s
62
+ end
63
+ end
64
+
65
+ def respond_to_missing?(name, *args)
66
+ jira_ticket.respond_to?(name) || super
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticket
4
+ class Replicator
5
+ class TicketStatusTransitioner
6
+ attr_reader :ticket
7
+
8
+ def initialize(ticket)
9
+ @ticket = ticket
10
+ end
11
+
12
+ def tool
13
+ @tool ||= ticket.jira_auto_tool
14
+ end
15
+
16
+ def transition_to(status)
17
+ return if status == ticket.status
18
+
19
+ ticket.jira_client.post(ticket.jira_auto_tool.jira_url("/rest/api/2/issue/#{ticket.key}/transitions"),
20
+ transition_payload_for(status),
21
+ http_headers)
22
+ end
23
+
24
+ def transition_payload_for(status)
25
+ 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}."
28
+
29
+ { transition: { id: transition["id"] } }.to_json
30
+ end
31
+
32
+ def available_transitions
33
+ response = tool.jira_client.get(tool.jira_url("/rest/api/2/issue/#{ticket.key}?expand=transitions"))
34
+
35
+ JSON.parse(response.body).fetch("transitions")
36
+ end
37
+
38
+ def http_headers
39
+ {
40
+ "Content-Type" => "application/json"
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticket
4
+ class Replicator
5
+ VERSION = "0.1.1"
6
+ end
7
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool"
4
+
5
+ require_relative "replicator/file_replicator"
6
+ require_relative "replicator/file_loader"
7
+ require_relative "replicator/file_transformer"
8
+ require_relative "replicator/jira_project"
9
+ require_relative "replicator/setup"
10
+ require_relative "replicator/version"
11
+
12
+ module Ticket
13
+ class Replicator
14
+ class Error < StandardError; end
15
+
16
+ def ticket_queue_transform_and_load
17
+ extracted_files.each { |file| FileReplicator.run_on(self, file) }
18
+ end
19
+
20
+ def jira_client
21
+ jira_auto_tool.jira_client
22
+ end
23
+
24
+ def jira_auto_tool
25
+ @jira_auto_tool ||= Jira::Auto::Tool.new
26
+ end
27
+
28
+ def jira_project
29
+ @jira_project = JiraProject.new(jira_auto_tool)
30
+ end
31
+
32
+ def transformed_queue_clear
33
+ FileUtils.rm_rf(transformed_folder)
34
+ end
35
+
36
+ def load
37
+ transformed_files.each { |file| FileReplicator.load(self, file) }
38
+ end
39
+
40
+ def transform
41
+ extracted_files.each { |file| FileReplicator.transform(self, file) }
42
+ end
43
+
44
+ def extracted_files
45
+ queue_files(extracted_folder)
46
+ end
47
+
48
+ def extracted_folder
49
+ queue_folder("10.extracted")
50
+ end
51
+
52
+ def transformed_files
53
+ queue_files(transformed_folder)
54
+ end
55
+
56
+ def transformed_folder
57
+ queue_folder("20.transformed")
58
+ end
59
+
60
+ def archived_folder
61
+ queue_folder("30.archived")
62
+ end
63
+
64
+ def queue_folder(*path_components)
65
+ path = File.join(queue_dir, *path_components)
66
+
67
+ FileUtils.mkdir_p(path)
68
+
69
+ path
70
+ end
71
+
72
+ def setup
73
+ Setup.new(self)
74
+ end
75
+
76
+ def home_dir
77
+ File.expand_path(File.join("..", ".."), __dir__)
78
+ end
79
+
80
+ private
81
+
82
+ def queue_dir
83
+ "queue"
84
+ end
85
+
86
+ def queue_files(queue_folder)
87
+ Dir.glob(File.join(queue_folder, "**", "*"))
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,6 @@
1
+ module Ticket
2
+ module Replicator
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ticket/replicator"
4
+
5
+ RSpec.configure do |config|
6
+ # Enable flags like --only-failures and --next-failure
7
+ config.example_status_persistence_file_path = ".rspec_status"
8
+
9
+ # Disable RSpec exposing methods globally on `Module` and `main`
10
+ config.disable_monkey_patching!
11
+
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = :expect
14
+ end
15
+
16
+ config.mock_with :rspec do |mocks|
17
+ mocks.verify_partial_doubles = true
18
+ end
19
+ end