itriagetestrail 1.0.36

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # TestRail API binding for Ruby (API v2, available since TestRail 3.0)
5
+ #
6
+ # Learn more:
7
+ #
8
+ # http://docs.gurock.com/testrail-api2/start
9
+ # http://docs.gurock.com/testrail-api2/accessing
10
+ #
11
+ # Copyright Gurock Software GmbH. See license.md for details.
12
+ #
13
+
14
+ require 'net/http'
15
+ require 'net/https'
16
+ require 'uri'
17
+ require 'json'
18
+
19
+ module TestRail
20
+ class APIClient
21
+ @url = ''
22
+ @user = ''
23
+ @password = ''
24
+ @response_code = nil
25
+
26
+ attr_accessor :user
27
+ attr_accessor :password
28
+ attr_reader :response_code
29
+
30
+ def initialize(base_url)
31
+ valid_regex = %r{/\/$/}
32
+ base_url += '/' unless valid_regex.match(base_url)
33
+ @url = base_url + 'index.php?/api/v2/'
34
+ end
35
+
36
+ #
37
+ # Send Get
38
+ #
39
+ # Issues a GET request (read) against the API and returns the result
40
+ # (as Ruby hash).
41
+ #
42
+ # Arguments:
43
+ #
44
+ # uri The API method to call including parameters
45
+ # (e.g. get_case/1)
46
+ #
47
+ def send_get(uri)
48
+ res = read_cache(uri)
49
+ res || _send_request('GET', uri, nil, true)
50
+ end
51
+
52
+ #
53
+ # Send POST
54
+ #
55
+ # Issues a POST request (write) against the API and returns the result
56
+ # (as Ruby hash).
57
+ #
58
+ # Arguments:
59
+ #
60
+ # uri The API method to call including parameters
61
+ # (e.g. add_case/1)
62
+ # data The data to submit as part of the request (as
63
+ # Ruby hash, strings must be UTF-8 encoded)
64
+ #
65
+ def send_post(uri, data)
66
+ _send_request('POST', uri, data)
67
+ end
68
+
69
+ private
70
+
71
+ def make_connection(url)
72
+ conn = Net::HTTP.new(url.host, url.port)
73
+ if url.scheme == 'https'
74
+ conn.use_ssl = true
75
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
76
+ end
77
+ conn
78
+ end
79
+
80
+ def _parse_result(response)
81
+ if response.body && !response.body.empty?
82
+ JSON.parse(response.body)
83
+ else
84
+ {}
85
+ end
86
+ end
87
+
88
+ def _error_check(response, result)
89
+ return if response.code == '200'
90
+ error = if !result.nil? && result.key?('error')
91
+ '"' + result['error'] + '"'
92
+ else
93
+ 'No additional error message received'
94
+ end
95
+ raise APIError, format('TestRail API returned HTTP %<response_code>s\n%<error>s',
96
+ response_code: response.code, error: error)
97
+ end
98
+
99
+ def _send_request(method, uri, data, write_cache = false)
100
+ url = URI.parse(@url + uri)
101
+ if method == 'POST'
102
+ request = Net::HTTP::Post.new(url.path + '?' + url.query)
103
+ request.body = JSON.dump(data)
104
+ else
105
+ request = Net::HTTP::Get.new(url.path + '?' + url.query)
106
+ end
107
+ request.basic_auth(@user, @password)
108
+ request.add_field('Content-Type', 'application/json')
109
+
110
+ conn = make_connection(url)
111
+
112
+ retry_count = 0
113
+ while retry_count < 10
114
+ response = conn.request(request)
115
+ @response_code = response.code
116
+ if @response_code == '429'
117
+ write_http_status_to_file('testrail_429s', url)
118
+ sleep(response.header['retry-after'].to_i)
119
+ elsif @response_code == '500'
120
+ puts response.to_s
121
+ # this might require different handling for 500 'Deadlock found when
122
+ # trying to get lock; try restarting transaction'
123
+ sleep(2)
124
+ else
125
+ write_http_status_to_file('testrail_200s', url)
126
+ break
127
+ end
128
+ retry_count += 1
129
+ end
130
+
131
+ result = _parse_result(response)
132
+
133
+ _error_check(response, result)
134
+
135
+ if write_cache
136
+ cache_to_file(uri, result)
137
+ end
138
+
139
+ result
140
+ end
141
+
142
+ # This method is only used publicly
143
+ def write_http_status_to_file(filename, endpoint)
144
+ Dir.mkdir('./tmp') unless File.exist?('./tmp')
145
+ file = File.open("./tmp/#{filename}", 'a')
146
+ file.write("#{Time.now},#{endpoint}\n")
147
+ file.close
148
+ end
149
+
150
+ def cache_to_file(url, content)
151
+ payload = {'response' => content}
152
+ Dir.mkdir('./tmp') unless File.exist?('./tmp')
153
+ filename = sanitize_filename(url)
154
+ file = File.open("./tmp/#{filename}", 'w')
155
+ file.write(payload.to_json)
156
+ file.close
157
+ end
158
+
159
+ def read_cache(url)
160
+ filename = sanitize_filename(url)
161
+ return unless File.exist?("./tmp/#{filename}")
162
+ file = File.open("./tmp/#{filename}", 'r')
163
+ content = JSON.parse(file.read)
164
+ file.close
165
+ content[:response]
166
+ end
167
+
168
+ def sanitize_filename(filename)
169
+ # Split the name when finding a period which is preceded by some
170
+ # character, and is followed by some character other than a period,
171
+ # if there is no following period that is followed by something
172
+ # other than a period (yeah, confusing, I know)
173
+ fn = filename.split /(?<=.)\.(?=[^.])(?!.*\.[^.])/m
174
+
175
+ # We now have one or two parts (depending on whether we could find
176
+ # a suitable period). For each of these parts, replace any unwanted
177
+ # sequence of characters with an underscore
178
+ fn.map! { |s| s.gsub /[^a-z0-9\-]+/i, '_' }
179
+
180
+ # Finally, join the parts with a period and return the result
181
+ return fn.join '.'
182
+ end
183
+ end
184
+
185
+ class APIError < StandardError
186
+ end
187
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tzinfo'
4
+
5
+ module Itriagetestrail
6
+ module Milestones
7
+ def normalize_origin
8
+ case @testrail_config[:origin]
9
+ when 'prd', 'production', 'origin/production'
10
+ 'Production'
11
+ when 'stg', 'staging', 'origin/staging'
12
+ 'Staging'
13
+ when 'dev', 'development', 'origin/development'
14
+ 'Development'
15
+ when 'local', ''
16
+ 'Local'
17
+ when 'master', 'origin/master'
18
+ 'Master'
19
+ else
20
+ 'Dev Branch'
21
+ end
22
+ end
23
+
24
+ # Establish the milestone name based on origin passed in,
25
+ # usually origin represents a branch or environment
26
+ def normalize_milestone
27
+ if @testrail_config[:milestone].nil? || @testrail_config[:milestone].empty?
28
+ normalize_origin
29
+ else
30
+ @testrail_config[:milestone]
31
+ end
32
+ end
33
+
34
+ # returns timestamp for begining of first day of current quarter
35
+ def milestone_period_start
36
+ time_zone = TZInfo::Timezone.get('America/Denver')
37
+ current_year = time_zone.now.year
38
+ month = time_zone.now.mon
39
+
40
+ # determine which quarter we are in
41
+ if month <= 3
42
+ time_zone.utc_to_local(Time.utc(current_year, 1, 1))
43
+ elsif month <= 6
44
+ time_zone.utc_to_local(Time.utc(current_year, 4, 1))
45
+ elsif month <= 9
46
+ time_zone.utc_to_local(Time.utc(current_year, 7, 1))
47
+ else
48
+ time_zone.utc_to_local(Time.utc(current_year, 10, 1))
49
+ end
50
+ end
51
+
52
+ # determine the due date (end of quarter) for a new milestone being added
53
+ def milestone_due_date
54
+ time_zone = TZInfo::Timezone.get('America/Denver')
55
+ current_year = time_zone.now.year
56
+ month = time_zone.now.mon
57
+
58
+ # determine which quarter we are in
59
+ if month <= 3
60
+ Time.utc(current_year, 3, 31).strftime('%s')
61
+ elsif month <= 6
62
+ Time.utc(current_year, 6, 30).strftime('%s')
63
+ elsif month <= 9
64
+ Time.utc(current_year, 9, 30).strftime('%s')
65
+ else
66
+ Time.new(current_year, 12, 31).strftime('%s')
67
+ end
68
+ end
69
+
70
+ # returns the id for a requested milestone by name, or creates one if the milestone does not exist
71
+ def fetch_milestone(requested_milestone_name)
72
+ milestones = @client.send_get("get_milestones/#{@project_id}")
73
+ res = -1
74
+ milestones.each do |milestone|
75
+ res = milestone['id'] if milestone['name'] == requested_milestone_name
76
+ end
77
+
78
+ if res == -1
79
+ # We need to add the milestone to TestRail
80
+
81
+ body = {
82
+ name: requested_milestone_name,
83
+ due_on: milestone_due_date
84
+ }
85
+
86
+ res = @client.send_post("add_milestone/#{@project_id}", body)['id']
87
+ end
88
+ res
89
+ end
90
+
91
+ # return a standardized name for a milestone to be archived
92
+ def milestone_archive_name(milestone_name, date)
93
+ year = date.year
94
+ month = date.mon
95
+
96
+ if month <= 3
97
+ "#{milestone_name} #{year}-Q1"
98
+ elsif month <= 6
99
+ "#{milestone_name} #{year}-Q2"
100
+ elsif month <= 9
101
+ "#{milestone_name} #{year}-Q3"
102
+ else
103
+ "#{milestone_name} #{year}-Q4"
104
+ end
105
+ end
106
+
107
+ # return all the runs associated with an existing milestone
108
+ def milestone_runs(milestone_name)
109
+ # use the matching milestone id for project
110
+ milestone_id = fetch_milestone(milestone_name)
111
+
112
+ # fetch all test runs associated with the milestone id for project
113
+ @client.send_get("get_runs/#{@project_id}&milestone_id=#{milestone_id}&is_completed=1") || []
114
+ end
115
+
116
+ # testrail call to rename a milestone (for archiving)
117
+ def rename_milestone(id, new_name)
118
+ # TODO: rename milestone with previous_milestone
119
+ body = { name: new_name }
120
+ @client.send_post("update_milestone/#{id}", body)['id']
121
+ end
122
+
123
+ # this archives a milestone at the turn of a quarter and creates a new one in its place
124
+ def reset_milestone(milestone_name)
125
+ runs = milestone_runs(milestone_name)
126
+ return if runs.empty?
127
+ last_run_time = Time.at(runs.last['completed_on'])
128
+
129
+ # if last run time is smaller than period start, do below
130
+ return unless last_run_time < milestone_period_start
131
+ rename_milestone(@milestone_id, milestone_archive_name(milestone_name, last_run_time))
132
+ @milestone_id = fetch_milestone(@milestone_name)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'itriagetestrail/version'
4
+ require 'itriagetestrail/testrail_binding'
5
+ require 'itriagetestrail/pool'
6
+ require 'tzinfo'
7
+
8
+ module Itriagetestrail
9
+ module Projects
10
+ # populate projects instance variable with all projects objects in the testrail site
11
+ def projects
12
+ @projects ||= @client.send_get('get_projects')
13
+ end
14
+
15
+ def project_by_name(name)
16
+ res = -1
17
+ projects.each do |project|
18
+ res = project if project['name'] == name
19
+ end
20
+ res
21
+ end
22
+
23
+ # return the project object for a given project id
24
+ def project_by_id(id)
25
+ res = -1
26
+ projects.each do |project|
27
+ res = project if project['id'] == id.to_i
28
+ end
29
+ res
30
+ end
31
+
32
+ # set the project_id by a requested project from config/environment variable
33
+ def set_project
34
+ requested_id = @testrail_config[:projectId]
35
+ case requested_id
36
+ when nil, ''
37
+ # a project id was not provided, fetch it from TestRail by project name
38
+ res = project_by_name(@testrail_config[:projectName])
39
+ if res == -1
40
+ @execute = false
41
+ return
42
+ end
43
+ @project_id = res['id']
44
+ @suite_mode = res['suite_mode']
45
+ else
46
+ # use the requested project id
47
+ @project_id = requested_id
48
+ @suite_mode = project_by_id(@project_id)['suite_mode']
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Itriagetestrail
4
+ module Sections
5
+ # TestRail Sections
6
+ def testrail_sections
7
+ case @suite_mode
8
+ when 2, 3
9
+ @suite_id = testrail_suite_id(@suite_name)
10
+ @sections = @client.send_get("get_sections/#{@project_id}&suite_id=#{@suite_id}")
11
+ else
12
+ @sections = @client.send_get("get_sections/#{@project_id}")
13
+ end
14
+ end
15
+
16
+ def testrail_section_id(section_title)
17
+ res = -1
18
+ @sections.each do |section|
19
+ res = section['id'] if section['name'] == section_title
20
+ end
21
+ res
22
+ end
23
+
24
+ def add_testrail_section(section_title)
25
+ body = if @suite_name
26
+ {
27
+ name: section_title,
28
+ suite_id: testrail_suite_id(@suite_name)
29
+ }
30
+ else
31
+ {
32
+ name: section_title
33
+ }
34
+ end
35
+
36
+ res = @client.send_post("add_section/#{@project_id}", body)
37
+
38
+ testrail_section = res['id']
39
+
40
+ # re-establish sections
41
+ testrail_sections
42
+
43
+ testrail_section
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Itriagetestrail
4
+ module Suites
5
+ # TestRail Suites
6
+ def testrail_suites
7
+ case @suite_mode
8
+ when 2, 3
9
+ @suites = @client.send_get("get_suites/#{@project_id}")
10
+ end
11
+ end
12
+
13
+ def testrail_suite_id(suite_name)
14
+ res = -1
15
+ @suites.each do |suite|
16
+ res = suite['id'] if suite['name'] == suite_name
17
+ end
18
+ res
19
+ end
20
+
21
+ def add_testrail_suite(suite_name)
22
+ body = { name: suite_name }
23
+ res = @client.send_post("add_suite/#{@project_id}", body)
24
+ testrail_suite = res['id']
25
+
26
+ # re-establish suites
27
+ testrail_suites
28
+
29
+ testrail_suite
30
+ end
31
+ end
32
+ end