qat-reporter-xray 6.0.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.
@@ -0,0 +1,229 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'active_support/core_ext/hash/keys'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'qat/logger'
6
+
7
+ module QAT
8
+ module Reporter
9
+ class Xray
10
+ # QAT::Reporter::Xray::Publisher integrator module
11
+ module Publisher
12
+ # QAT::Reporter::Xray::Publisher::Base integrator class
13
+ class Base
14
+ include QAT::Logger
15
+
16
+ attr_reader :base_url, :default_headers, :login_credentials, :default_cloud_api_url, :cloud_xray_api_credentials
17
+
18
+ # Initializes Xray Publisher url and login information
19
+ def initialize
20
+ @base_url = QAT::Reporter::Xray::Config.jira_url
21
+ @login_credentials = QAT::Reporter::Xray::Config.login_credentials
22
+ @default_cloud_api_url = QAT::Reporter::Xray::Config.xray_default_api_url
23
+ @cloud_xray_api_credentials = QAT::Reporter::Xray::Config.cloud_xray_api_credentials
24
+ end
25
+
26
+ # Creates a Jira issue
27
+ def create_issue(data)
28
+ Client.new(base_url).post('/rest/api/2/issue', data.to_json, default_headers)
29
+ end
30
+
31
+
32
+ # Get the default headers for Xray ('password' in Xray API is password)
33
+ def default_headers
34
+ headers = if QAT::Reporter::Xray::Config.jira_type == 'cloud'
35
+ auth_headers_jira_cloud
36
+ else
37
+ auth_headers
38
+ end
39
+ {
40
+ 'Content-Type': 'application/json'
41
+ }.merge(headers)
42
+ end
43
+
44
+
45
+ private
46
+ # Authentication header for xray, Basic authentication done with: username, password
47
+ def auth_headers
48
+ username = login_credentials[0]
49
+ password = login_credentials[1]
50
+ {
51
+ Authorization: "Basic #{::Base64::encode64("#{username}:#{password}").delete("\n")}"
52
+ }
53
+ end
54
+
55
+ # Authentication header for jira, Basic authentication done with: username, apiToken
56
+ def auth_headers_jira_cloud
57
+ username = login_credentials[0]
58
+ api_token = login_credentials[2]
59
+ {
60
+ Authorization: "Basic #{::Base64::encode64("#{username}:#{api_token}").delete("\n")}"
61
+ }
62
+ end
63
+
64
+ #Gets a zip file from a response and extracts features to 'Feature' folder
65
+ def extract_feature_files(rsp)
66
+ Zip::InputStream.open(StringIO.new(rsp)) do |io|
67
+ while entry = io.get_next_entry
68
+ entry_path = File.join('features', entry.name)
69
+ log.info 'Feature ' + entry.name + ' was found, extracting...'
70
+ entry.extract(File.join(Dir.pwd, entry_path)) unless File.exist?(entry_path)
71
+ end
72
+ end
73
+ #See https://github.com/rubyzip/rubyzip#notice-about-zipinputstream
74
+ rescue Zip::GPFBit3Error
75
+ Tempfile.open do |file|
76
+ File.write(file.path, rsp)
77
+
78
+ Zip::File.open(file.path) do |zip_file|
79
+ # Handle entries one by one
80
+ zip_file.each do |entry|
81
+ # Extract to file/directory/symlink
82
+ log.info 'Feature ' + entry.name + ' was found, extracting...'
83
+ entry_path = File.join('features', entry.name)
84
+ entry.extract(entry_path)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # REST Base Client implementation
91
+ class Client
92
+ include QAT::Logger
93
+
94
+ # Service Unavailable Error class
95
+ class ServiceUnavailableError < StandardError
96
+ end
97
+ # Connection Error class
98
+ class ConnectionError < StandardError
99
+ end
100
+
101
+ # No Connection Error class
102
+ class NoConnectionFound < StandardError
103
+ end
104
+
105
+ attr_reader :base_uri
106
+
107
+ # Returns a new REST Base Client
108
+ # @return [RestClient::Response]
109
+ def initialize(base_uri)
110
+ #sets the ip:port/base_route
111
+ @base_uri = case base_uri
112
+ when Hash
113
+ URI::HTTP.build(base_uri).to_s
114
+ when URI::HTTP
115
+ base_uri.to_s
116
+ when String
117
+ base_uri
118
+ else
119
+ raise ArgumentError.new "Invalid URI class: #{base_uri.class}"
120
+ end
121
+ end
122
+
123
+ [:put, :post, :get, :delete, :patch].each do |operation|
124
+ define_method operation do |url, *args|
125
+ final_url = base_uri + url
126
+
127
+ log_request operation, final_url, args
128
+ begin
129
+ response = RestClient.method(operation).call(final_url, *args)
130
+ log_response response
131
+ validate response
132
+ rescue RestClient::ExceptionWithResponse => e
133
+ log.error e.response
134
+ raise NoConnectionFound.new ('Jira was not found!!!')
135
+ rescue => exception
136
+ log.error "#{exception.class} #{exception.message.to_s}"
137
+ raise NoConnectionFound.new ('Jira was not found!!!')
138
+ end
139
+ end
140
+ end
141
+
142
+ protected
143
+
144
+ # Validates the response and raises a HTTP Error
145
+ #@param response [RestClient::Response] response
146
+ def validate(response)
147
+ error_klass = case response.code
148
+ when 400 then
149
+ Error::BadRequest
150
+ when 401 then
151
+ Error::Unauthorized
152
+ when 403 then
153
+ Error::Forbidden
154
+ when 404 then
155
+ Error::NotFound
156
+ when 405 then
157
+ Error::MethodNotAllowed
158
+ when 409 then
159
+ Error::Conflict
160
+ when 422 then
161
+ Error::Unprocessable
162
+ when 500 then
163
+ Error::InternalServerError
164
+ when 502 then
165
+ Error::BadGateway
166
+ when 503 then
167
+ Error::ServiceUnavailable
168
+ end
169
+
170
+ raise error_klass.new response if error_klass
171
+ response
172
+ end
173
+
174
+ # Logs the request information
175
+ #@param operation [String] HTTP operation called
176
+ #@param url [String] target URL
177
+ #@param args [String] request arguments
178
+ #@see RestClient#get
179
+ def log_request(operation, url, args)
180
+ log.info { "#{operation.upcase}: #{url}" }
181
+
182
+ args.each do |options|
183
+ log_http_options options
184
+ end
185
+ end
186
+
187
+ # Logs the received response
188
+ #@param response [RestClient::Response] response
189
+ def log_response(response)
190
+ log.info "Response HTTP #{response.code} (#{response.body})"
191
+
192
+ log_http_options({ headers: response.headers.to_h,
193
+ body: response.body }.select { |_, value| !value.nil? })
194
+ end
195
+
196
+ # Logs the request's HTTP options
197
+ #@param options [Hash|String] http options to log
198
+ def log_http_options(options)
199
+ if log.debug?
200
+ temp = if options.is_a?(String)
201
+ { payload: JSON.parse(options) }
202
+ else
203
+ options.map do |k, v|
204
+ if k == :body
205
+ begin
206
+ [k, JSON.pretty_generate(JSON.parse(v))]
207
+ #if body is not JSON by some unknown reason, we still want to print
208
+ rescue JSON::ParserError
209
+ [k, v]
210
+ end
211
+ else
212
+ [k, v]
213
+ end
214
+ end.to_h
215
+ end
216
+
217
+ temp.each do |key, value|
218
+ log.debug "#{key.to_s.humanize}:"
219
+ log.debug value
220
+ end
221
+ end
222
+ end
223
+
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,74 @@
1
+ require_relative 'base'
2
+ require 'zip'
3
+
4
+ module QAT
5
+ module Reporter
6
+ class Xray
7
+ module Publisher
8
+ # QAT::Reporter::Xray::Publisher::Cloud integrator class
9
+ class Cloud < Base
10
+
11
+ # Posts the execution json results in Xray
12
+ def send_execution_results(results)
13
+ headers = { 'Content-Type': 'application/json' }.merge(auth_token)
14
+ Client.new(default_cloud_api_url).post('/api/v1/import/execution', results.to_json, headers)
15
+ end
16
+
17
+ # Get the Authorization Token based on client_id & client_secret (ONLY FOR CLOUD XRAY)
18
+ def auth_token
19
+ return @auth_token if @auth_token
20
+
21
+ client_id = cloud_xray_api_credentials[0]
22
+ client_secret = cloud_xray_api_credentials[1]
23
+ auth_header_cloud = {
24
+ client_id: client_id,
25
+ client_secret: client_secret
26
+ }
27
+
28
+ response = Client.new(default_cloud_api_url).post('/api/v1/authenticate', auth_header_cloud).body
29
+ bearer = JSON.parse(response)
30
+ @auth_token = {
31
+ Authorization: "Bearer #{bearer}"
32
+ }
33
+ end
34
+
35
+ # Import Cucumber features files as a zip file via API
36
+ # @param project_key [String] JIRA's project key
37
+ # @param file_path [String] Cucumber features files' zip file
38
+ # @see https://confluence.xpand-it.com/display/XRAYCLOUD/Importing+Cucumber+Tests+-+REST
39
+ def import_cucumber_tests(project_key, file_path, project_id = nil)
40
+ headers = auth_token.merge({
41
+ multipart: true,
42
+ params: {
43
+ projectKey: project_key,
44
+ projectId: project_id,
45
+ source: project_key
46
+ }
47
+ })
48
+ payload = { file: File.new(file_path, 'rb') }
49
+
50
+ Client.new(default_cloud_api_url).post('/api/v1/import/feature', payload, headers)
51
+ end
52
+
53
+ # Export Xray test scenarios to a zip file via API
54
+ # @param keys [String] test scenarios
55
+ # @param filter [String] project filter
56
+ # @see https://confluence.xpand-it.com/display/XRAYCLOUD/Exporting+Cucumber+Tests+-+REST
57
+ def export_test_scenarios(keys, filter)
58
+ params = {
59
+ keys: keys,
60
+ }
61
+ params[:filter] = filter unless filter == 'nil'
62
+
63
+ headers = auth_token.merge(params: params)
64
+
65
+ log.info "Exporting features from: #{default_cloud_api_url}/api/v1/export/cucumber"
66
+ rsp = RestClient.get("#{default_cloud_api_url}/api/v1/export/cucumber", headers)
67
+
68
+ extract_feature_files(rsp)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,56 @@
1
+ require 'base64'
2
+ require 'zip'
3
+ require_relative 'base'
4
+
5
+ module QAT
6
+ module Reporter
7
+ class Xray
8
+ module Publisher
9
+ # QAT::Reporter::Xray::Publisher::Hosted integrator class
10
+ class Hosted < Base
11
+
12
+ # Posts the execution json results in Xray
13
+ # @param results [String] Execution results
14
+ def send_execution_results(results)
15
+ Client.new(base_url).post('/rest/raven/1.0/import/execution', results.to_json, default_headers)
16
+ end
17
+
18
+ # Import Cucumber features files as a zip file via API
19
+ # @param project_key [String] JIRA's project key
20
+ # @param file_path [String] Cucumber features files' zip file
21
+ # @see https://confluence.xpand-it.com/display/public/XRAY/Importing+Cucumber+Tests+-+REST
22
+ def import_cucumber_tests(project_key, file_path)
23
+ headers = default_headers.merge({
24
+ multipart: true,
25
+ params: {
26
+ projectKey: project_key
27
+ }
28
+ })
29
+ payload = { file: File.new(file_path, 'rb') }
30
+
31
+ Client.new(base_url).post('/rest/raven/1.0/import/feature', payload, headers)
32
+ end
33
+
34
+ # Export Xray test scenarios to a zip file via API
35
+ # @param keys [String] test scenarios
36
+ # @param filter [String] project filter
37
+ # @see https://confluence.xpand-it.com/display/public/XRAY/Exporting+Cucumber+Tests+-+REST
38
+ def export_test_scenarios(keys, filter)
39
+ params = {
40
+ keys: keys,
41
+ fz: true
42
+ }
43
+
44
+ params[:filter] = filter unless filter == 'nil'
45
+
46
+ headers = default_headers.merge(params: params)
47
+
48
+ rsp = RestClient.get("#{base_url}/rest/raven/1.0/export/test", headers)
49
+
50
+ extract_feature_files(rsp)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'publisher/base'
2
+ require_relative 'publisher/cloud'
3
+ require_relative 'publisher/hosted'
@@ -0,0 +1,112 @@
1
+ module QAT
2
+ module Reporter
3
+ class Xray
4
+ # namespace for test ids utility methods and objects
5
+ module Tests
6
+ # helper methods for test id manipulation
7
+ module Helpers
8
+ # Tags all untagged scenarios present in the test id report
9
+ #@param report [Tests::Report] test id report
10
+ def tag_untagged(report)
11
+ max_test_id = report.max_id
12
+ untagged = report.untagged
13
+
14
+ if untagged.any?
15
+ files = map_untagged(untagged)
16
+
17
+ announce_changes(files)
18
+
19
+ update_test_ids(files, max_test_id)
20
+ else
21
+ puts "There are no scenarios without test id. Last test id given was '@test##{max_test_id}'."
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Returns all files containing untagged scenarios and their respective scenario line
28
+ #@param untagged [Array] list of untagged scenarios
29
+ #@return [Array]
30
+ def map_untagged(untagged)
31
+ files = {}
32
+
33
+ untagged.values.each do |file_colon|
34
+ file, colon = file_colon.split(':')
35
+ files[file] ||= []
36
+ files[file] << colon.to_i
37
+ end
38
+
39
+ files
40
+ end
41
+
42
+ # Announces to STDOUT the files that will be changed (test ids added)
43
+ #@param files [Array] list of files to change
44
+ #@see TestIds::Helpers#map_untaged
45
+ def announce_changes(files)
46
+ puts "Giving test ids to scenarios:"
47
+ puts files.to_json({
48
+ indent: ' ',
49
+ space: ' ',
50
+ object_nl: "\n"
51
+ })
52
+ end
53
+
54
+ # Rewrites the untagged files adding the missing test ids
55
+ #@param files [Array] list of files to change
56
+ #@param max_test_id [Integer] current max test id
57
+ def update_test_ids(files, max_test_id)
58
+ #iterate in file to give test id
59
+ begin
60
+ file_lines = []
61
+ files.each { |file, lines| max_test_id = rewrite_file(file, lines, max_test_id) }
62
+ rescue
63
+ path = File.join(Dir.pwd, 'public', 'test_ids_failed.feature')
64
+ puts "Tag attribution failed! Check '#{path}' for more information!"
65
+ File.write(path, file_lines.join)
66
+ end
67
+ end
68
+
69
+ # Rewrites the target file in the identified lines.
70
+ # Returns the max test id after the file update.
71
+ #@param file [String] file to rewrite
72
+ #@param lines [Array] lines to edit (add test id)
73
+ #@param max_test_id [Integer] current max test id
74
+ #@return [Integer]
75
+ def rewrite_file(file, lines, max_test_id)
76
+ norm_lines = lines.map { |line| line - 1 }
77
+ file_path = File.join(Dir.pwd, file)
78
+ file_lines = File.readlines(file_path)
79
+
80
+ norm_lines.size.times do
81
+ line = norm_lines.shift
82
+ puts "Editing file #{file} @ line #{line}."
83
+ max_test_id = add_tags(file_lines, line, max_test_id)
84
+ end
85
+
86
+ File.write(file_path, file_lines.join)
87
+
88
+ max_test_id
89
+ end
90
+
91
+ # Adds the test id tag to the identified line to edit
92
+ # Returns the max test id after the file update.
93
+ #@param file_lines [Array] Set of file lines
94
+ #@param line [Integer] index of line to edit
95
+ #@param max_test_id [Integer] current max test id
96
+ #@return [Integer]
97
+ def add_tags(file_lines, line, max_test_id)
98
+ if file_lines[line - 1].match(/^\s*@\w+/)
99
+ file_lines[line - 1] = " #{file_lines[line - 1].strip} @id:#{max_test_id += 1}\n"
100
+ else
101
+ file_lines[line] = " @id:#{max_test_id += 1}\n#{file_lines[line]}"
102
+ end
103
+
104
+ max_test_id
105
+ end
106
+
107
+ extend self
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'helpers'
2
+
3
+ module QAT
4
+ module Reporter
5
+ class Xray
6
+ # namespace for test ids utility methods and objects
7
+ module Tests
8
+ # the test id report wrapper
9
+ class Report
10
+ include Helpers
11
+
12
+ attr_reader :path, :content
13
+
14
+ def initialize(path)
15
+ @path = path
16
+ @content = JSON.parse(File.read(path))
17
+ end
18
+
19
+ # Returns the report max test id
20
+ def max_id
21
+ @max_id ||= @content['max']
22
+ end
23
+
24
+ # Returns the report untagged tests information
25
+ def untagged
26
+ @untagged ||= @content['untagged']
27
+ end
28
+
29
+ # Returns the report test id mapping to scenarios information
30
+ def mapping
31
+ @mapping ||= @content['mapping']
32
+ end
33
+
34
+ # Returns the report duplicate test id information
35
+ def duplicate
36
+ @duplicate ||= @content['duplicate']
37
+ end
38
+
39
+ # Tags all untagged scenario with a test id
40
+ def tag_untagged!
41
+ tag_untagged(self)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env rake
2
+ #encoding: utf-8
3
+ require 'cucumber'
4
+ require 'cucumber/rake/task'
5
+ require 'rake/testtask'
6
+ require 'json'
7
+ require 'fileutils'
8
+ require 'awesome_print'
9
+ require 'fileutils'
10
+ require 'active_support/core_ext/string/inflections'
11
+ require_relative '../../../formatter/xray/test_ids'
12
+ require_relative 'tests/report'
13
+ require_relative 'tests/helpers'
14
+
15
+ namespace :qat do
16
+ namespace :reporter do
17
+ namespace :xray do
18
+ namespace :tests do
19
+ # Run a rake task by name
20
+ def run_task!(task_name)
21
+ begin
22
+ Rake::Task["qat:reporter:xray:tests:#{task_name}"].invoke
23
+ rescue SystemExit => exception
24
+ exitstatus = exception.status
25
+ @kernel.exit(exitstatus) unless exitstatus == 0
26
+ end
27
+ end
28
+
29
+ desc 'Generates the test id report in JSON'
30
+ task :report_test_ids do
31
+ FileUtils.mkdir('public') unless File.exists?(File.join(Dir.pwd, 'public'))
32
+ ENV['CUCUMBER_OPTS'] = nil
33
+ Cucumber::Rake::Task.new('test_ids', 'Generates test ids as tags for tests without test id') do |task|
34
+ task.bundler = false
35
+ task.fork = false
36
+ task.cucumber_opts = ['--no-profile',
37
+ '--dry-run',
38
+ '--format', 'QAT::Formatter::Xray::TestIds',
39
+ '--out', 'public/xray_test_ids.json']
40
+ end.runner.run
41
+ end
42
+
43
+ desc 'Validates the existing test ids and checks for duplicates'
44
+ task :validate_test_ids do
45
+ run_task!('report_test_ids')
46
+ #read json file
47
+ file_path = File.realpath(File.join(Dir.pwd, 'public', 'xray_test_ids.json'))
48
+ report = QAT::Reporter::Xray::Tests::Report.new(file_path)
49
+
50
+ exit(1) if report.duplicate.any?
51
+ end
52
+
53
+ desc 'Generates test ids as tags for tests without test id'
54
+ task :generate_test_ids do
55
+ run_task!('report_test_ids')
56
+ #read json file
57
+ file_path = File.realpath(File.join(Dir.pwd, 'public', 'xray_test_ids.json'))
58
+ report = QAT::Reporter::Xray::Tests::Report.new(file_path)
59
+
60
+ report.tag_untagged!
61
+ end
62
+
63
+ desc 'Generate features zip file to import in Xray'
64
+ task :zip_features do
65
+ require 'zip'
66
+
67
+ file_mask = File.join('features', '**', '*.feature')
68
+ feature_files = Dir.glob(file_mask)
69
+ puts feature_files
70
+
71
+ zipfile_name = 'features.zip'
72
+
73
+ FileUtils.rm_f(zipfile_name) if File.exists?(zipfile_name)
74
+
75
+ Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
76
+ feature_files.each do |file|
77
+ zipfile.add(file, file)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Validates the import task arguments
83
+ def validate_import_args(args)
84
+ [:xray_username, :jira_url, :jira_type, :project_key].each do |key|
85
+ raise ArgumentError.new "No #{key.to_s.humanize} was provided" unless args[key]
86
+ end
87
+ end
88
+
89
+ desc 'Import Cucumber tests to Xray'
90
+ task :import_features, [:xray_username, :jira_url, :jira_type, :project_key, :file_path] do |_, args|
91
+ run_task!('generate_test_ids')
92
+ run_task!('zip_features')
93
+
94
+ require 'qat/reporter/xray'
95
+
96
+ file_path = args.delete(:file_path) || 'features.zip'
97
+
98
+ validate_import_args(args)
99
+ project_key = args[:project_key] or raise ArgumentError.new 'No project key was provided'
100
+ login_credentials = [args[:xray_username], ENV['JIRA_PASSWORD']] or raise ArgumentError.new 'No login credentials were provided'
101
+ jira_type = args[:jira_type] or raise ArgumentError.new 'No jira type key was provided'
102
+ jira_url = args[:jira_url] or raise ArgumentError.new 'No jira url key was provided'
103
+
104
+ QAT::Reporter::Xray.configure do |c|
105
+ c.project_key = project_key
106
+ c.login_credentials = login_credentials
107
+ c.cloud_xray_api_credentials = login_credentials if jira_type.eql? 'cloud'
108
+ c.jira_type = jira_type
109
+ c.jira_url = jira_url
110
+ end
111
+
112
+ QAT::Reporter::Xray::Config.publisher.import_cucumber_tests(project_key, file_path)
113
+ end
114
+
115
+ desc 'Export Xray test scenarios '
116
+ task :export_xray_test_scenarios, [:xray_username, :jira_url, :jira_type, :project_key, :keys, :filter] do |_, args|
117
+
118
+ require 'qat/reporter/xray'
119
+
120
+ project_key = args[:project_key] or raise ArgumentError.new 'No project key was provided'
121
+ login_credentials = [args[:xray_username], ENV['JIRA_PASSWORD']] or raise ArgumentError.new 'No login credentials were provided'
122
+ jira_type = args[:jira_type] or raise ArgumentError.new 'No jira type was provided'
123
+ jira_url = args[:jira_url] or raise ArgumentError.new 'No jira url was provided'
124
+ xray_export_test_filter = args[:filter] or raise ArgumentError.new 'No filter key was provided'
125
+ xray_export_test_keys = args[:keys] or raise ArgumentError.new 'No test issue key was provided'
126
+
127
+ QAT::Reporter::Xray.configure do |c|
128
+ c.project_key = project_key
129
+ c.login_credentials = login_credentials
130
+ c.cloud_xray_api_credentials = login_credentials if jira_type.eql? 'cloud'
131
+ c.jira_type = jira_type
132
+ c.jira_url = jira_url
133
+ c.xray_export_test_filter = xray_export_test_filter
134
+ c.xray_export_test_keys = xray_export_test_keys
135
+ end
136
+
137
+ QAT::Reporter::Xray::Config.publisher.export_test_scenarios(xray_export_test_keys, xray_export_test_filter)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ #encoding: utf-8
3
+ #@version 1.0
4
+ require_relative '../../cucumber/core_ext/result'
5
+ require_relative 'tasks/tests'