qat-reporter-xray 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'