File Coverage

blib/lib/TestRail/API.pm
Criterion Covered Total %
statement 692 746 92.7
branch 239 320 74.6
condition 155 233 66.5
subroutine 110 112 98.2
pod 91 91 100.0
total 1287 1502 85.6


line stmt bran cond sub pod time code
1             # ABSTRACT: Provides an interface to TestRail's REST api via HTTP
2             # PODNAME: TestRail::API
3              
4             package TestRail::API;
5             $TestRail::API::VERSION = '0.052';
6              
7 21     21   514949 use 5.010;
  21         153  
8              
9 21     21   114 use strict;
  21         42  
  21         420  
10 21     21   91 use warnings;
  21         55  
  21         737  
11              
12 21     21   131 use Carp qw{cluck confess};
  21         34  
  21         1518  
13 21     21   157 use Scalar::Util qw{reftype looks_like_number};
  21         37  
  21         1410  
14 21     21   5340 use Clone 'clone';
  21         32256  
  21         1237  
15 21     21   3802 use Try::Tiny;
  21         15961  
  21         1387  
16              
17             use Types::Standard
18 21     21   10730 qw( slurpy ClassName Object Str Int Bool HashRef ArrayRef Maybe Optional);
  21         1411780  
  21         290  
19 21     21   45538 use Type::Params qw( compile );
  21         212522  
  21         243  
20              
21 21     21   11708 use JSON::MaybeXS 1.001000 ();
  21         69502  
  21         519  
22 21     21   6069 use HTTP::Request;
  21         216795  
  21         1603  
23 21     21   9396 use LWP::UserAgent;
  21         337212  
  21         777  
24 21     21   9723 use HTTP::CookieJar::LWP;
  21         535726  
  21         979  
25 21     21   11206 use Data::Validate::URI qw{is_uri};
  21         911661  
  21         1546  
26 21     21   181 use List::Util 1.33;
  21         545  
  21         1076  
27 21     21   8559 use Encode ();
  21         150053  
  21         221478  
28              
29             sub new {
30 106     106 1 11085 state $check = compile(
31             ClassName,
32             Str,
33             Str,
34             Str,
35             Optional [ Maybe [Str] ],
36             Optional [ Maybe [Bool] ],
37             Optional [ Maybe [Bool] ],
38             Optional [ Maybe [Int] ],
39             Optional [ Maybe [HashRef] ]
40             );
41             my (
42 106         168530 $class, $apiurl, $user,
43             $pass, $encoding, $debug,
44             $do_post_redirect, $max_tries, $userfetch_opts
45             ) = $check->(@_);
46              
47 106 100       20234 die("Invalid URI passed to constructor") if !is_uri($apiurl);
48 105   50     28549 $debug //= 0;
49              
50             my $self = {
51             user => $user,
52             pass => $pass,
53             apiurl => $apiurl,
54             debug => $debug,
55             encoding => $encoding || 'UTF-8',
56             testtree => [],
57             flattree => [],
58             user_cache => [],
59             configurations => {},
60             tr_fields => undef,
61 105   50     3189 tr_project_id => $userfetch_opts->{'project_id'},
      100        
62             default_request => undef,
63             global_limit => 250, #Discovered by experimentation
64             browser => LWP::UserAgent->new(
65             keep_alive => 10,
66             cookie_jar => HTTP::CookieJar::LWP->new(),
67             ),
68             do_post_redirect => $do_post_redirect,
69             max_tries => $max_tries // 1,
70             retry_delay => 5,
71             };
72              
73             #Allow POST redirects
74 105 100       89274 if ( $self->{do_post_redirect} ) {
75 56         129 push @{ $self->{'browser'}->requests_redirectable }, 'POST';
  56         378  
76             }
77              
78             #Check chara encoding
79             $self->{'encoding-nonaliased'} =
80 105         2000 Encode::resolve_alias( $self->{'encoding'} );
81             die( "Invalid encoding alias '"
82             . $self->{'encoding'}
83             . "' passed, see Encoding::Supported for a list of allowed encodings"
84 105 50       9631 ) unless $self->{'encoding-nonaliased'};
85              
86             die( "Invalid encoding '"
87             . $self->{'encoding-nonaliased'}
88             . "' passed, see Encoding::Supported for a list of allowed encodings"
89             )
90 105 50       639 unless grep { $_ eq $self->{'encoding-nonaliased'} }
  13020         116412  
91             ( Encode->encodings(":all") );
92              
93             #Create default request to pass on to LWP::UserAgent
94 105         1820 $self->{'default_request'} = HTTP::Request->new();
95 105         7368 $self->{'default_request'}->authorization_basic( $user, $pass );
96              
97 105         32725 bless( $self, $class );
98 105 100       451 return $self if $self->debug; #For easy class testing without mocks
99              
100             # Manually do the get_users call to check HTTP status...
101             # Allow users to skip the check if you have a zillion users etc,
102             # as apparently that is fairly taxing on TR itself.
103 1 50       5 if ( !$userfetch_opts->{skip_usercache} ) {
104 1         5 my $res = $self->getUsers( $userfetch_opts->{project_id} );
105 1 50       14 confess "Error: network unreachable" if !defined($res);
106 1 50 50     13 if ( ( reftype($res) || 'undef' ) ne 'ARRAY' ) {
107 1 50       10 confess "Unexpected return from _doRequest: $res"
108             if !looks_like_number($res);
109 1 50       585 confess
110             "Could not communicate with TestRail Server! Check that your URI is correct, and your TestRail installation is functioning correctly."
111             if $res == -500;
112 0 0       0 confess
113             "Could not list testRail users! Check that your TestRail installation has it's API enabled, and your credentials are correct"
114             if $res == -403;
115 0 0       0 confess "Bad user credentials!" if $res == -401;
116 0 0       0 confess
117             "HTTP error $res encountered while communicating with TestRail server. Resolve issue and try again."
118             if $res < 0;
119 0         0 confess "Unknown error occurred: $res";
120             }
121             confess
122 0 0       0 "No users detected on TestRail Install! Check that your API is functioning correctly."
123             if !scalar(@$res);
124             }
125              
126 0         0 return $self;
127             }
128              
129             sub apiurl {
130 1838     1838 1 4245 state $check = compile(Object);
131 1838         16448 my ($self) = $check->(@_);
132 1838         22474 return $self->{'apiurl'};
133             }
134              
135             sub debug {
136 1832     1832 1 3133 state $check = compile(Object);
137 1832         18858 my ($self) = $check->(@_);
138 1832         17259 return $self->{'debug'};
139             }
140              
141             #Convenient JSON-HTTP fetcher
142             sub _doRequest {
143 1726     1726   6401 state $check = compile(
144             Object, Str,
145             Optional [ Maybe [Str] ],
146             Optional [ Maybe [HashRef] ]
147             );
148 1726         73613 my ( $self, $path, $method, $data ) = $check->(@_);
149              
150 1726         34620 $self->{num_tries}++;
151              
152 1726         28025 my $req = clone $self->{'default_request'};
153 1726   100     8788 $method //= 'GET';
154              
155 1726         7902 $req->method($method);
156 1726         22043 $req->url( $self->apiurl . '/' . $path );
157              
158 1726 100       272687 warn "$method " . $self->apiurl . "/$path" if $self->debug;
159              
160 1726         10851 my $coder = JSON::MaybeXS->new;
161              
162             #Data sent is JSON, and encoded per user preference
163             my $content =
164             $data
165 1726 100       32384 ? Encode::encode( $self->{'encoding-nonaliased'}, $coder->encode($data) )
166             : '';
167              
168 1726         18093 $req->content($content);
169             $req->header(
170 1726         38199 "Content-Type" => "application/json; charset=" . $self->{'encoding'} );
171              
172 1726         94645 my $response = eval { $self->{'browser'}->request($req) };
  1726         8035  
173              
174             #Uncomment to generate mocks
175             #use Data::Dumper;
176             #open(my $fh, '>>', 'mock.out');
177             #print $fh "{\n\n";
178             #print $fh Dumper($path,'200','OK',$response->headers,$response->content);
179             #print $fh '$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));';
180             #print $fh "\n\n}\n\n";
181             #close $fh;
182              
183 1726 50       3111226 if ($@) {
184              
185             #LWP threw an ex, probably a timeout
186 0 0       0 if ( $self->{num_tries} >= $self->{max_tries} ) {
187 0         0 $self->{num_tries} = 0;
188 0         0 confess "Failed to satisfy request after $self->{num_tries} tries!";
189             }
190             cluck
191 0         0 "WARNING: TestRail API request failed due to timeout, or other LWP fatal condition, re-trying request...\n";
192 0 0       0 sleep $self->{retry_delay} if $self->{retry_delay};
193 0         0 goto &_doRequest;
194             }
195              
196 1726 50       4200 return $response if !defined($response); #worst case
197              
198 1726 100       4052 if ( $response->code == 403 ) {
199 1         11 confess "ERROR 403: Access Denied: " . $response->content;
200             }
201 1725 100       16608 if ( $response->code == 401 ) {
202 1         10 confess "ERROR 401: Authentication failed: " . $response->content;
203             }
204              
205 1724 100       15191 if ( $response->code != 200 ) {
206              
207             #LWP threw an ex, probably a timeout
208 121 100       1082 if ( $self->{num_tries} >= $self->{max_tries} ) {
209 119         167 $self->{num_tries} = 0;
210 119         219 cluck "ERROR: Arguments Bad? (got code "
211             . $response->code . "): "
212             . $response->content;
213 119         35281 return -int( $response->code );
214             }
215 2         7 cluck "WARNING: TestRail API request failed (got code "
216             . $response->code
217             . "), re-trying request...\n";
218 2 100       5001180 sleep $self->{retry_delay} if $self->{retry_delay};
219 2         66 goto &_doRequest;
220              
221             }
222 1603         14739 $self->{num_tries} = 0;
223              
224             try {
225 1603     1603   63509 return $coder->decode( $response->content );
226             }
227             catch {
228 9 50 33 9   262 if ( $response->code == 200 && !$response->content ) {
229 9         327 return 1; #This function probably just returns no data
230             }
231             else {
232 0         0 cluck "ERROR: Malformed JSON returned by API.";
233 0         0 cluck $@;
234 0 0       0 if ( !$self->debug )
235             { #Otherwise we've already printed this, but we need to know if we encounter this
236 0         0 cluck "RAW CONTENT:";
237 0         0 cluck $response->content;
238             }
239 0         0 return 0;
240             }
241             }
242 1603         13337 }
243              
244             sub getUsers {
245 78     78 1 4993 state $check = compile( Object, Optional [ Maybe [Str] ] );
246 78         20848 my ( $self, $project_id ) = $check->(@_);
247              
248             # Return shallow clone of user_cache if set.
249 6         18 return [ @{ $self->{'user_cache'} } ]
250             if ref $self->{'user_cache'} eq 'ARRAY'
251 78 100 50     2711 && scalar( @{ $self->{'user_cache'} } );
  78         370  
252 72 100       304 my $maybe_project = $project_id ? "/$project_id" : '';
253 72         299 my $res = $self->_doRequest("index.php?/api/v2/get_users$maybe_project");
254 70 100 100     3350 return -500 if !$res || ( reftype($res) || 'undef' ) ne 'ARRAY';
      66        
255 60         216 $self->{'user_cache'} = $res;
256 60         1739 return clone($res);
257             }
258              
259             sub getUserByID {
260 4     4 1 5501 state $check = compile( Object, Int );
261 4         3290 my ( $self, $user ) = $check->(@_);
262              
263 3         57 my $users = $self->getUsers();
264 3 100       20 return $users if ref $users ne 'ARRAY';
265 1         4 foreach my $usr (@$users) {
266 1 50       8 return $usr if $usr->{'id'} == $user;
267             }
268 0         0 return 0;
269             }
270              
271             sub getUserByName {
272 4     4 1 4828 state $check = compile( Object, Str );
273 4         3075 my ( $self, $user ) = $check->(@_);
274              
275 3         68 my $users = $self->getUsers();
276 3 100       22 return $users if ref $users ne 'ARRAY';
277 1         4 foreach my $usr (@$users) {
278 1 50       7 return $usr if $usr->{'name'} eq $user;
279             }
280 0         0 return 0;
281             }
282              
283             sub getUserByEmail {
284 4     4 1 4877 state $check = compile( Object, Str );
285 4         3492 my ( $self, $email ) = $check->(@_);
286              
287 3         51 my $users = $self->getUsers();
288 3 100       18 return $users if ref $users ne 'ARRAY';
289 1         4 foreach my $usr (@$users) {
290 1 50       6 return $usr if $usr->{'email'} eq $email;
291             }
292 0         0 return 0;
293             }
294              
295             sub userNamesToIds {
296 7     7 1 9455 state $check = compile( Object, slurpy ArrayRef [Str] );
297 7         22734 my ( $self, $names ) = $check->(@_);
298              
299 7 100       747 confess("At least one user name must be provided") if !scalar(@$names);
300 12         24 my @ret = grep { defined $_ } map {
301 12         18 my $user = $_;
302 12         20 my @list = grep { $user->{'name'} eq $_ } @$names;
  18         39  
303 12 100       40 scalar(@list) ? $user->{'id'} : undef
304 6         11 } @{ $self->getUsers() };
  6         22  
305 6 100       218 confess("One or more user names provided does not exist in TestRail.")
306             unless scalar(@$names) == scalar(@ret);
307 5         18 return @ret;
308             }
309              
310             sub createProject {
311 4     4 1 6057 state $check = compile(
312             Object, Str,
313             Optional [ Maybe [Str] ],
314             Optional [ Maybe [Bool] ]
315             );
316 4         11263 my ( $self, $name, $desc, $announce ) = $check->(@_);
317              
318 3   100     357 $desc //= 'res ipsa loquiter';
319 3   50     28 $announce //= 0;
320              
321 3         17 my $input = {
322             name => $name,
323             announcement => $desc,
324             show_announcement => $announce
325             };
326              
327 3         17 return $self->_doRequest( 'index.php?/api/v2/add_project', 'POST', $input );
328             }
329              
330             sub deleteProject {
331 4     4 1 4561 state $check = compile( Object, Int );
332 4         3272 my ( $self, $proj ) = $check->(@_);
333              
334 3         61 return $self->_doRequest( 'index.php?/api/v2/delete_project/' . $proj,
335             'POST' );
336             }
337              
338             sub getProjects {
339 106     106 1 3677 state $check = compile( Object, Optional [ Maybe [HashRef] ] );
340 106         38101 my ( $self, $filters ) = $check->(@_);
341              
342 106         3407 my $result = $self->_doRequest( 'index.php?/api/v2/get_projects'
343             . _convert_filters_to_string($filters) );
344 106 100 100     7323 return -500 if !$result || ( reftype($result) || 'undef' ) ne 'HASH';
      66        
345 100         361 my $projects = $result->{'projects'};
346 100 50 50     1066 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      33        
347              
348             #Save state for future use, if needed
349 100         299 $self->{'testtree'} = $projects;
350              
351 100         182 foreach my $pj ( @{$projects} ) {
  100         253  
352 300         612 $pj->{'type'} = 'project';
353             }
354              
355 100         479 return $projects;
356             }
357              
358             sub getProjectByName {
359 121     121 1 5088 state $check = compile( Object, Str );
360 121         15727 my ( $self, $project ) = $check->(@_);
361              
362             #See if we already have the project list...
363 120         1871 my $projects = $self->{'testtree'};
364 120 50 50     1328 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      33        
365 120 100       710 $projects = $self->getProjects() unless scalar(@$projects);
366              
367             #Search project list for project
368 120 100 100     943 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
369 118         376 for my $candidate (@$projects) {
370 232 100       891 return $candidate if ( $candidate->{'name'} eq $project );
371             }
372              
373 1         3 return 0;
374             }
375              
376             sub getProjectByID {
377 8     8 1 4619 state $check = compile( Object, Int );
378 8         5239 my ( $self, $project ) = $check->(@_);
379              
380             #See if we already have the project list...
381 7         119 my $projects = $self->{'testtree'};
382 7 100       53 $projects = $self->getProjects() unless scalar(@$projects);
383              
384             #Search project list for project
385 7 100 100     83 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
386 5         26 for my $candidate (@$projects) {
387 7 100       42 return $candidate if ( $candidate->{'id'} eq $project );
388             }
389              
390 0         0 return 0;
391             }
392              
393             sub createTestSuite {
394 5     5 1 7228 state $check = compile( Object, Int, Str, Optional [ Maybe [Str] ] );
395 5         8129 my ( $self, $project_id, $name, $details ) = $check->(@_);
396              
397 3   100     247 $details //= 'res ipsa loquiter';
398 3         15 my $input = {
399             name => $name,
400             description => $details
401             };
402              
403 3         18 return $self->_doRequest( 'index.php?/api/v2/add_suite/' . $project_id,
404             'POST', $input );
405             }
406              
407             sub deleteTestSuite {
408 4     4 1 4594 state $check = compile( Object, Int );
409 4         3239 my ( $self, $suite_id ) = $check->(@_);
410              
411 3         64 return $self->_doRequest( 'index.php?/api/v2/delete_suite/' . $suite_id,
412             'POST' );
413             }
414              
415             sub getTestSuites {
416 15     15 1 5377 state $check = compile( Object, Int );
417 15         9020 my ( $self, $proj ) = $check->(@_);
418              
419 14         236 return $self->_doRequest( 'index.php?/api/v2/get_suites/' . $proj );
420             }
421              
422             sub getTestSuiteByName {
423 13     13 1 6791 state $check = compile( Object, Int, Str );
424 13         14578 my ( $self, $project_id, $testsuite_name ) = $check->(@_);
425              
426             #TODO cache
427 11         274 my $suites = $self->getTestSuites($project_id);
428 11 100 100     513 return -500
      66        
429             if !$suites
430             || ( reftype($suites) || 'undef' ) ne
431             'ARRAY'; #No suites for project, or no project
432 9         36 foreach my $suite (@$suites) {
433 9 50       73 return $suite if $suite->{'name'} eq $testsuite_name;
434             }
435 0         0 return 0; #Couldn't find it
436             }
437              
438             sub getTestSuiteByID {
439 4     4 1 4684 state $check = compile( Object, Int );
440 4         3209 my ( $self, $testsuite_id ) = $check->(@_);
441              
442 3         59 return $self->_doRequest( 'index.php?/api/v2/get_suite/' . $testsuite_id );
443             }
444              
445             sub createSection {
446 6     6 1 9189 state $check = compile( Object, Int, Int, Str, Optional [ Maybe [Int] ] );
447 6         8832 my ( $self, $project_id, $suite_id, $name, $parent_id ) = $check->(@_);
448              
449 3         249 my $input = {
450             name => $name,
451             suite_id => $suite_id
452             };
453 3 50       15 $input->{'parent_id'} = $parent_id if $parent_id;
454              
455 3         17 return $self->_doRequest( 'index.php?/api/v2/add_section/' . $project_id,
456             'POST', $input );
457             }
458              
459             sub deleteSection {
460 4     4 1 4603 state $check = compile( Object, Int );
461 4         3283 my ( $self, $section_id ) = $check->(@_);
462              
463 3         63 return $self->_doRequest( 'index.php?/api/v2/delete_section/' . $section_id,
464             'POST' );
465             }
466              
467             sub getSections {
468 34     34 1 7116 state $check = compile( Object, Int, Int );
469 34         11479 my ( $self, $project_id, $suite_id ) = $check->(@_);
470              
471             #Cache sections to reduce requests in tight loops
472 30 100       689 return $self->{'sections'}->{$suite_id} if $self->{'sections'}->{$suite_id};
473 17         120 my $response = $self->_doRequest(
474             "index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
475 17 100 100     1083 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
476 11         43 my $sections = $response->{'sections'};
477 11 50 50     149 return -500 if !$sections || ( reftype($sections) || 'undef' ) ne 'ARRAY';
      33        
478              
479 11         54 $self->{'sections'}->{$suite_id} = $sections;
480              
481 11         98 return $self->{'sections'}->{$suite_id};
482             }
483              
484             sub getSectionByID {
485 4     4 1 4583 state $check = compile( Object, Int );
486 4         3244 my ( $self, $section_id ) = $check->(@_);
487              
488 3         58 return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
489             }
490              
491             sub getSectionByName {
492 7     7 1 8266 state $check = compile( Object, Int, Int, Str );
493 7         6815 my ( $self, $project_id, $suite_id, $section_name ) = $check->(@_);
494              
495 4         117 my $sections = $self->getSections( $project_id, $suite_id );
496 4 100 100     83 return -500 if !$sections || ( reftype($sections) || 'undef' ) ne 'ARRAY';
      66        
497 2         9 foreach my $sec (@$sections) {
498 6 100       28 return $sec if $sec->{'name'} eq $section_name;
499             }
500 0         0 return 0;
501             }
502              
503             sub getChildSections {
504 11     11 1 5739 state $check = compile( Object, Int, HashRef );
505 11         6181 my ( $self, $project_id, $section ) = $check->(@_);
506              
507 11         275 my $sections_orig = $self->getSections( $project_id, $section->{suite_id} );
508 11 100 100     138 return []
      66        
509             if !$sections_orig || ( reftype($sections_orig) || 'undef' ) ne 'ARRAY';
510             my @sections =
511 10 100       32 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $section->{'id'} : 0 }
  50         127  
512             @$sections_orig;
513 10         44 foreach my $sec (@sections) {
514             push( @sections,
515 9 100       19 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $sec->{'id'} : 0 }
  72         118  
516             @$sections_orig );
517             }
518 10         41 return \@sections;
519             }
520              
521             sub sectionNamesToIds {
522 14     14 1 7162 my ( $self, $project_id, $suite_id, @names ) = @_;
523 14 50       96 my $sections = $self->getSections( $project_id, $suite_id )
524             or confess("Could not find sections in provided project/suite.");
525 12         76 return _X_in_my_Y( $self, $sections, 'id', @names );
526             }
527              
528             sub getCaseTypes {
529 16     16 1 3525 state $check = compile(Object);
530 16         3671 my ($self) = $check->(@_);
531 16 100       377 return clone( $self->{'type_cache'} ) if defined( $self->{'type_cache'} );
532              
533 7         31 my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
534 7 100 100     241 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
535 3         19 $self->{'type_cache'} = $types;
536              
537 3         473 return clone $types;
538             }
539              
540             sub getCaseTypeByName {
541 13     13 1 6441 state $check = compile( Object, Str );
542 13         4902 my ( $self, $name ) = $check->(@_);
543              
544 12         184 my $types = $self->getCaseTypes();
545 12 100 100     95 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
546 10         23 foreach my $type (@$types) {
547 16 100       80 return $type if $type->{'name'} eq $name;
548             }
549 0         0 confess("No such case type '$name'!");
550             }
551              
552             sub typeNamesToIds {
553 1     1 1 5 my ( $self, @names ) = @_;
554 1         3 return _X_in_my_Y( $self, $self->getCaseTypes(), 'id', @names );
555             }
556              
557             sub createCase {
558 5     5 1 6774 state $check = compile(
559             Object, Int, Str,
560             Optional [ Maybe [Int] ],
561             Optional [ Maybe [HashRef] ],
562             Optional [ Maybe [HashRef] ]
563             );
564 5         13561 my ( $self, $section_id, $title, $type_id, $opts, $extras ) = $check->(@_);
565              
566 3         346 my $stuff = {
567             title => $title,
568             type_id => $type_id
569             };
570              
571             #Handle sort of optional but baked in options
572 3 50 33     17 if ( defined($extras) && reftype($extras) eq 'HASH' ) {
573             $stuff->{'priority_id'} = $extras->{'priority_id'}
574 0 0       0 if defined( $extras->{'priority_id'} );
575             $stuff->{'estimate'} = $extras->{'estimate'}
576 0 0       0 if defined( $extras->{'estimate'} );
577             $stuff->{'milestone_id'} = $extras->{'milestone_id'}
578 0 0       0 if defined( $extras->{'milestone_id'} );
579 0         0 $stuff->{'refs'} = join( ',', @{ $extras->{'refs'} } )
580 0 0       0 if defined( $extras->{'refs'} );
581             }
582              
583             #Handle custom fields
584 3 50 33     21 if ( defined($opts) && reftype($opts) eq 'HASH' ) {
585 0         0 foreach my $key ( keys(%$opts) ) {
586 0         0 $stuff->{"custom_$key"} = $opts->{$key};
587             }
588             }
589              
590 3         16 return $self->_doRequest( "index.php?/api/v2/add_case/$section_id",
591             'POST', $stuff );
592             }
593              
594             sub updateCase {
595 1     1 1 625 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
596 1         2275 my ( $self, $case_id, $options ) = $check->(@_);
597              
598 1         96 return $self->_doRequest( "index.php?/api/v2/update_case/$case_id",
599             'POST', $options );
600             }
601              
602             sub deleteCase {
603 4     4 1 4823 state $check = compile( Object, Int );
604 4         3151 my ( $self, $case_id ) = $check->(@_);
605              
606 3         68 return $self->_doRequest( "index.php?/api/v2/delete_case/$case_id",
607             'POST' );
608             }
609              
610             sub getCases {
611 24     24 1 8918 state $check = compile( Object, Int, Int, Optional [ Maybe [HashRef] ] );
612 24         22953 my ( $self, $project_id, $suite_id, $filters ) = $check->(@_);
613              
614 22         1136 my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
615 22         76 $url .= _convert_filters_to_string($filters);
616              
617 22         92 my $response = $self->_doRequest($url);
618 22 100 100     1055 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
619 15         45 my $cases = $response->{'cases'};
620 15 50 50     164 return -500 if !$cases || ( reftype($cases) || 'undef' ) ne 'ARRAY';
      33        
621 15         85 return $cases;
622             }
623              
624             sub getCaseByName {
625 7     7 1 10124 state $check =
626             compile( Object, Int, Int, Str, Optional [ Maybe [HashRef] ] );
627 7         8857 my ( $self, $project_id, $suite_id, $name, $filters ) = $check->(@_);
628              
629 4         280 my $cases = $self->getCases( $project_id, $suite_id, $filters );
630 4 100 100     51 return -500 if !$cases || ( reftype($cases) || 'undef' ) ne 'ARRAY';
      66        
631 1         4 foreach my $case (@$cases) {
632 1 50       7 return $case if $case->{'title'} eq $name;
633             }
634 0         0 return 0;
635             }
636              
637             sub getCaseByID {
638 4     4 1 4637 state $check = compile( Object, Int );
639 4         3271 my ( $self, $case_id ) = $check->(@_);
640              
641 3         58 return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
642             }
643              
644             sub getCaseFields {
645 3     3 1 973 state $check = compile(Object);
646 3         1122 my ($self) = $check->(@_);
647 3 100       38 return $self->{case_fields} if $self->{case_fields};
648              
649             $self->{case_fields} =
650 2         12 $self->_doRequest("index.php?/api/v2/get_case_fields");
651 2         19 return $self->{case_fields};
652             }
653              
654             sub addCaseField {
655 1     1 1 8 state $check = compile( Object, slurpy HashRef );
656 1         1585 my ( $self, $options ) = $check->(@_);
657 1         23 $self->{case_fields} = undef;
658 1         6 return $self->_doRequest( "index.php?/api/v2/add_case_field", 'POST',
659             $options );
660             }
661              
662             sub getPriorities {
663 4     4 1 3336 state $check = compile(Object);
664 4         1652 my ($self) = $check->(@_);
665             return clone( $self->{'priority_cache'} )
666 4 100       74 if defined( $self->{'priority_cache'} );
667              
668 2         22 my $priorities = $self->_doRequest("index.php?/api/v2/get_priorities");
669 2 100 100     75 return -500
      66        
670             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
671 1         4 $self->{'priority_cache'} = $priorities;
672              
673 1         22 return clone $priorities;
674             }
675              
676             sub getPriorityByName {
677 2     2 1 2526 state $check = compile( Object, Str );
678 2         2164 my ( $self, $name ) = $check->(@_);
679              
680 1         15 my $priorities = $self->getPriorities();
681 1 50 50     11 return -500
      33        
682             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
683 1         3 foreach my $priority (@$priorities) {
684 1 50       9 return $priority if $priority->{'name'} eq $name;
685             }
686 0         0 confess("No such priority '$name'!");
687             }
688              
689             sub priorityNamesToIds {
690 1     1 1 4 my ( $self, @names ) = @_;
691 1         4 return _X_in_my_Y( $self, $self->getPriorities(), 'id', @names );
692             }
693              
694             #If you pass an array of case ids, it implies include_all is false
695             sub createRun {
696 277     277 1 20604 state $check = compile(
697             Object, Int, Int, Str,
698             Optional [ Maybe [Str] ],
699             Optional [ Maybe [Int] ],
700             Optional [ Maybe [Int] ],
701             Optional [ Maybe [ ArrayRef [Int] ] ]
702             );
703 277         49891 my ( $self, $project_id, $suite_id, $name, $desc, $milestone_id,
704             $assignedto_id, $case_ids )
705             = $check->(@_);
706              
707 274 100       10081 my $stuff = {
708             suite_id => $suite_id,
709             name => $name,
710             description => $desc,
711             milestone_id => $milestone_id,
712             assignedto_id => $assignedto_id,
713             include_all => defined($case_ids) ? 0 : 1,
714             case_ids => $case_ids
715             };
716              
717 274         879 return $self->_doRequest( "index.php?/api/v2/add_run/$project_id",
718             'POST', $stuff );
719             }
720              
721             sub deleteRun {
722 4     4 1 4604 state $check = compile( Object, Int );
723 4         3206 my ( $self, $run_id ) = $check->(@_);
724              
725 3         63 return $self->_doRequest( "index.php?/api/v2/delete_run/$run_id", 'POST' );
726             }
727              
728             sub getRuns {
729 112     112 1 6111 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
730 112         37614 my ( $self, $project_id, $filters ) = $check->(@_);
731              
732             my $initial_runs =
733 111         4966 $self->getRunsPaginated( $project_id, $self->{'global_limit'},
734             0, $filters );
735 111 100 100     656 return $initial_runs
736             unless ( reftype($initial_runs) || 'undef' ) eq 'ARRAY';
737 107         260 my $runs = [];
738 107         924 push( @$runs, @$initial_runs );
739 107         247 my $offset = 1;
740 107         584 while ( scalar(@$initial_runs) == $self->{'global_limit'} ) {
741             $initial_runs = $self->getRunsPaginated(
742             $project_id,
743             $self->{'global_limit'},
744 22         128 ( $self->{'global_limit'} * $offset ), $filters
745             );
746 22 50 50     307 return $initial_runs
747             unless ( reftype($initial_runs) || 'undef' ) eq 'ARRAY';
748 22         81 push( @$runs, @$initial_runs );
749 22         74 $offset++;
750             }
751 107         439 return $runs;
752             }
753              
754             sub getRunsPaginated {
755 135     135 1 4077 state $check = compile(
756             Object,
757             Int,
758             Optional [ Maybe [Int] ],
759             Optional [ Maybe [Int] ],
760             Optional [ Maybe [HashRef] ]
761             );
762 135         63185 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
763              
764             confess( "Limit greater than " . $self->{'global_limit'} )
765 134 50       8134 if $limit > $self->{'global_limit'};
766 134         519 my $apiurl = "index.php?/api/v2/get_runs/$project_id";
767 134 100       717 $apiurl .= "&offset=$offset" if defined($offset);
768 134 100       613 $apiurl .= "&limit=$limit"
769             if $limit; #You have problems if you want 0 results
770 134         1037 $apiurl .= _convert_filters_to_string($filters);
771 134         1082 my $response = $self->_doRequest($apiurl);
772 134 100 100     88411 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
773 129         460 my $runs = $response->{'runs'};
774 129 50 50     1038 return -500 if !$runs || ( reftype($runs) || 'undef' ) ne 'ARRAY';
      33        
775 129         1057 return $runs;
776             }
777              
778             sub getRunByName {
779 60     60 1 6526 state $check = compile( Object, Int, Str );
780 60         18035 my ( $self, $project_id, $name ) = $check->(@_);
781              
782 58         1621 my $runs = $self->getRuns($project_id);
783 58 100 100     567 return -500 if !$runs || ( reftype($runs) || 'undef' ) ne 'ARRAY';
      66        
784 56         159 foreach my $run (@$runs) {
785 178 100       2069 return $run if $run->{'name'} eq $name;
786             }
787 19         278 return 0;
788             }
789              
790             sub getRunByID {
791 6     6 1 5311 state $check = compile( Object, Int );
792 6         3700 my ( $self, $run_id ) = $check->(@_);
793              
794 5         95 return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
795             }
796              
797             sub closeRun {
798 4     4 1 7297 state $check = compile( Object, Int );
799 4         4225 my ( $self, $run_id ) = $check->(@_);
800              
801 4         96 return $self->_doRequest( "index.php?/api/v2/close_run/$run_id", 'POST' );
802             }
803              
804             sub getRunSummary {
805 14     14 1 2128 state $check = compile( Object, slurpy ArrayRef [HashRef] );
806 14         46185 my ( $self, $runs ) = $check->(@_);
807 14 100       1647 confess("At least one run must be passed!") unless scalar(@$runs);
808              
809             #Translate custom statuses
810 13         112 my $statuses = $self->getPossibleTestStatuses();
811 13         34 my %shash;
812              
813             #XXX so, they do these tricks with the status names, see...so map the counts to their relevant status ids.
814             @shash{
815             map {
816             ( $_->{'id'} < 6 )
817             ? $_->{'name'} . "_count"
818             : "custom_status"
819 117 100       458 . ( $_->{'id'} - 5 )
820             . "_count"
821             } @$statuses
822 13         51 } = map { $_->{'id'} } @$statuses;
  117         241  
823              
824 13         56 my @sname;
825              
826             #Create listing of keys/values
827             @$runs = map {
828 13         38 my $run = $_;
  282         391  
829 282         1667 @{ $run->{statuses} }{ grep { $_ =~ m/_count$/ } keys(%$run) } =
  7833         11253  
830 282         1692 grep { $_ =~ m/_count$/ } keys(%$run);
  7833         12524  
831 282         783 foreach my $status ( keys( %{ $run->{'statuses'} } ) ) {
  282         1098  
832 3348 100       4891 next if !exists( $shash{$status} );
833             @sname = grep {
834 2511         2970 exists( $shash{$status} )
835 22599 50       48838 && $_->{'id'} == $shash{$status}
836             } @$statuses;
837             $run->{'statuses_clean'}->{ $sname[0]->{'label'} } =
838 2511         4861 $run->{$status};
839             }
840 282         646 $run;
841             } @$runs;
842              
843             return map {
844 13         49 {
845             'id' => $_->{'id'},
846             'name' => $_->{'name'},
847             'run_status' => $_->{'statuses_clean'},
848 282         1128 'config_ids' => $_->{'config_ids'}
849             }
850             } @$runs;
851              
852             }
853              
854             sub getRunResults {
855 1     1 1 2312 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
856 1         2995 my ( $self, $run_id, $filters ) = $check->(@_);
857              
858             my $initial_results =
859 1         107 $self->getRunResultsPaginated( $run_id, $self->{'global_limit'},
860             undef, $filters );
861 1 50 50     9 return $initial_results
862             unless ( reftype($initial_results) || 'undef' ) eq 'ARRAY';
863 1         3 my $results = [];
864 1         3 push( @$results, @$initial_results );
865 1         3 my $offset = 1;
866 1         4 while ( scalar(@$initial_results) == $self->{'global_limit'} ) {
867             $initial_results = $self->getRunResultsPaginated(
868             $run_id,
869             $self->{'global_limit'},
870 0         0 ( $self->{'global_limit'} * $offset ), $filters
871             );
872 0 0 0     0 return $initial_results
873             unless ( reftype($initial_results) || 'undef' ) eq 'ARRAY';
874 0         0 push( @$results, @$initial_results );
875 0         0 $offset++;
876             }
877 1         3 return $results;
878             }
879              
880             sub getRunResultsPaginated {
881 1     1 1 5 state $check = compile(
882             Object,
883             Int,
884             Optional [ Maybe [Int] ],
885             Optional [ Maybe [Int] ],
886             Optional [ Maybe [HashRef] ]
887             );
888 1         4125 my ( $self, $run_id, $limit, $offset, $filters ) = $check->(@_);
889              
890             confess( "Limit greater than " . $self->{'global_limit'} )
891 1 50       180 if $limit > $self->{'global_limit'};
892 1         4 my $apiurl = "index.php?/api/v2/get_results_for_run/$run_id";
893 1 50       4 $apiurl .= "&offset=$offset" if defined($offset);
894 1 50       4 $apiurl .= "&limit=$limit"
895             if $limit; #You have problems if you want 0 results
896 1         7 $apiurl .= _convert_filters_to_string($filters);
897 1         8 my $response = $self->_doRequest($apiurl);
898 1         61 return $response->{'results'};
899             }
900              
901             sub getChildRuns {
902 2432     2432 1 6484 state $check = compile( Object, HashRef );
903 2432         14167 my ( $self, $plan ) = $check->(@_);
904              
905             return 0
906             unless defined( $plan->{'entries'} )
907 2431 100 50     21850 && ( reftype( $plan->{'entries'} ) || 'undef' ) eq 'ARRAY';
      66        
908 2429         3194 my $entries = $plan->{'entries'};
909 2429         3221 my $plans = [];
910 2429         3125 foreach my $entry (@$entries) {
911 2458         3950 push( @$plans, @{ $entry->{'runs'} } )
912             if defined( $entry->{'runs'} )
913 2458 50 50     7645 && ( ( reftype( $entry->{'runs'} ) || 'undef' ) eq 'ARRAY' );
      33        
914             }
915 2429         3809 return $plans;
916             }
917              
918             sub getChildRunByName {
919 42     42 1 6245 state $check = compile(
920             Object, HashRef, Str,
921             Optional [ Maybe [ ArrayRef [Str] ] ],
922             Optional [ Maybe [Int] ]
923             );
924 42         48577 my ( $self, $plan, $name, $configurations, $testsuite_id ) = $check->(@_);
925              
926 40         2890 my $runs = $self->getChildRuns($plan);
927 40 100       96 @$runs = grep { $_->{suite_id} == $testsuite_id } @$runs if $testsuite_id;
  2         8  
928 40 100       132 return 0 if !$runs;
929              
930 39         74 my @pconfigs = ();
931              
932             #Figure out desired config IDs
933 39 100       88 if ( defined $configurations ) {
934 38         117 my $avail_configs = $self->getConfigurations( $plan->{'project_id'} );
935 38         64 my ($cname);
936 36         156 @pconfigs = map { $_->{'id'} } grep {
937 38         86 $cname = $_->{'name'};
  158         217  
938 158         200 grep { $_ eq $cname } @$configurations
  144         266  
939             } @$avail_configs; #Get a list of IDs from the names passed
940             }
941 39 50 66     281 confess("One or more configurations passed does not exist in your project!")
942             if defined($configurations)
943             && ( scalar(@pconfigs) != scalar(@$configurations) );
944              
945 39         89 my $found;
946 39         108 foreach my $run (@$runs) {
947 38 100       123 next if $run->{name} ne $name;
948 35 100       58 next if scalar(@pconfigs) != scalar( @{ $run->{'config_ids'} } );
  35         90  
949              
950             #Compare run config IDs against desired, invalidate run if all conditions not satisfied
951 34         57 $found = 0;
952 34         49 foreach my $cid ( @{ $run->{'config_ids'} } ) {
  34         69  
953 33 100       56 $found++ if grep { $_ == $cid } @pconfigs;
  33         125  
954             }
955              
956 34 100       53 return $run if $found == scalar( @{ $run->{'config_ids'} } );
  34         178  
957             }
958 7         34 return 0;
959             }
960              
961             sub createPlan {
962 264     264 1 21969 state $check = compile(
963             Object, Int, Str,
964             Optional [ Maybe [Str] ],
965             Optional [ Maybe [Int] ],
966             Optional [ Maybe [ ArrayRef [HashRef] ] ]
967             );
968 264         43550 my ( $self, $project_id, $name, $desc, $milestone_id, $entries ) =
969             $check->(@_);
970              
971 262         7911 my $stuff = {
972             name => $name,
973             description => $desc,
974             milestone_id => $milestone_id,
975             entries => $entries
976             };
977              
978 262         837 return $self->_doRequest( "index.php?/api/v2/add_plan/$project_id",
979             'POST', $stuff );
980             }
981              
982             sub deletePlan {
983 4     4 1 5844 state $check = compile( Object, Int );
984 4         3712 my ( $self, $plan_id ) = $check->(@_);
985              
986 3         61 return $self->_doRequest( "index.php?/api/v2/delete_plan/$plan_id",
987             'POST' );
988             }
989              
990             sub getPlans {
991 105     105 1 5717 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
992 105         30603 my ( $self, $project_id, $filters ) = $check->(@_);
993              
994             my $initial_plans =
995 104         3651 $self->getPlansPaginated( $project_id, $self->{'global_limit'},
996             0, $filters );
997 104 100 100     843 return $initial_plans
998             unless ( reftype($initial_plans) || 'undef' ) eq 'ARRAY';
999 100         229 my $plans = [];
1000 100         823 push( @$plans, @$initial_plans );
1001 100         194 my $offset = 1;
1002 100         433 while ( scalar(@$initial_plans) == $self->{'global_limit'} ) {
1003             $initial_plans = $self->getPlansPaginated(
1004             $project_id,
1005             $self->{'global_limit'},
1006 19         117 ( $self->{'global_limit'} * $offset ), $filters
1007             );
1008 19 50 50     244 return $initial_plans
1009             unless ( reftype($initial_plans) || 'undef' ) eq 'ARRAY';
1010 19         62 push( @$plans, @$initial_plans );
1011 19         75 $offset++;
1012             }
1013 100         476 return $plans;
1014             }
1015              
1016             sub getPlansPaginated {
1017 125     125 1 4037 state $check = compile(
1018             Object,
1019             Int,
1020             Optional [ Maybe [Int] ],
1021             Optional [ Maybe [Int] ],
1022             Optional [ Maybe [HashRef] ]
1023             );
1024 125         51280 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
1025              
1026             confess( "Limit greater than " . $self->{'global_limit'} )
1027 124 50       6885 if $limit > $self->{'global_limit'};
1028 124         449 my $apiurl = "index.php?/api/v2/get_plans/$project_id";
1029 124 100       739 $apiurl .= "&offset=$offset" if defined($offset);
1030 124 100       698 $apiurl .= "&limit=$limit"
1031             if $limit; #You have problems if you want 0 results
1032 124         488 $apiurl .= _convert_filters_to_string($filters);
1033 124         437 my $response = $self->_doRequest($apiurl);
1034 124 100 100     60961 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
1035 119         360 my $plans = $response->{'plans'};
1036 119 50 50     1178 return -500 if !$plans || ( reftype($plans) || 'undef' ) ne 'ARRAY';
      33        
1037 119         974 return $plans;
1038             }
1039              
1040             sub getPlanByName {
1041 51     51 1 6959 state $check = compile( Object, Int, Str );
1042 51         14561 my ( $self, $project_id, $name ) = $check->(@_);
1043              
1044 49         1177 my $plans = $self->getPlans($project_id);
1045 49 100 100     420 return -500 if !$plans || ( reftype($plans) || 'undef' ) ne 'ARRAY';
      66        
1046 47         135 foreach my $plan (@$plans) {
1047 856 100       1616 if ( $plan->{'name'} eq $name ) {
1048 39         151 return $self->getPlanByID( $plan->{'id'} );
1049             }
1050             }
1051 8         1834 return 0;
1052             }
1053              
1054             sub getPlanByID {
1055 113     113 1 5737 state $check = compile( Object, Int );
1056 113         12256 my ( $self, $plan_id ) = $check->(@_);
1057              
1058 112         1943 return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
1059             }
1060              
1061             sub getPlanSummary {
1062 6     6 1 3425 state $check = compile( Object, Int );
1063 6         8724 my ( $self, $plan_id ) = $check->(@_);
1064              
1065 5         132 my $runs = $self->getPlanByID($plan_id);
1066 5         683 $runs = $self->getChildRuns($runs);
1067 5         18 @$runs = $self->getRunSummary( @{$runs} );
  5         59  
1068 5         17 my $total_sum = 0;
1069 5         20 my $ret = { plan => $plan_id };
1070              
1071             #Compile totals
1072 5         24 foreach my $summary (@$runs) {
1073 11         18 my @elems = keys( %{ $summary->{'run_status'} } );
  11         53  
1074 11         19 foreach my $key (@elems) {
1075 99 100       177 $ret->{'totals'}->{$key} = 0 if !defined $ret->{'totals'}->{$key};
1076 99         118 $ret->{'totals'}->{$key} += $summary->{'run_status'}->{$key};
1077 99         126 $total_sum += $summary->{'run_status'}->{$key};
1078             }
1079             }
1080              
1081             #Compile percentages
1082 5         10 foreach my $key ( keys( %{ $ret->{'totals'} } ) ) {
  5         29  
1083 45 50       94 next if grep { $_ eq $key } qw{plan configs percentages};
  135         220  
1084             $ret->{"percentages"}->{$key} =
1085 45         263 sprintf( "%.2f%%", ( $ret->{'totals'}->{$key} / $total_sum ) * 100 );
1086             }
1087              
1088 5         49 return $ret;
1089             }
1090              
1091             #If you pass an array of case ids, it implies include_all is false
1092             sub createRunInPlan {
1093 14     14 1 8335 state $check = compile(
1094             Object, Int, Int, Str,
1095             Optional [ Maybe [Int] ],
1096             Optional [ Maybe [ ArrayRef [Int] ] ],
1097             Optional [ Maybe [ ArrayRef [Int] ] ]
1098             );
1099 14         26079 my ( $self, $plan_id, $suite_id, $name, $assignedto_id, $config_ids,
1100             $case_ids )
1101             = $check->(@_);
1102              
1103 11 100       1421 my $runs = [
1104             {
1105             config_ids => $config_ids,
1106             include_all => defined($case_ids) ? 0 : 1,
1107             case_ids => $case_ids
1108             }
1109             ];
1110              
1111 11 100       112 my $stuff = {
1112             suite_id => $suite_id,
1113             name => $name,
1114             assignedto_id => $assignedto_id,
1115             include_all => defined($case_ids) ? 0 : 1,
1116             case_ids => $case_ids,
1117             config_ids => $config_ids,
1118             runs => $runs
1119             };
1120 11         74 return $self->_doRequest( "index.php?/api/v2/add_plan_entry/$plan_id",
1121             'POST', $stuff );
1122             }
1123              
1124             sub closePlan {
1125 6     6 1 3713 state $check = compile( Object, Int );
1126 6         5692 my ( $self, $plan_id ) = $check->(@_);
1127              
1128 6         126 return $self->_doRequest( "index.php?/api/v2/close_plan/$plan_id", 'POST' );
1129             }
1130              
1131             sub createMilestone {
1132 5     5 1 8354 state $check = compile(
1133             Object, Int, Str,
1134             Optional [ Maybe [Str] ],
1135             Optional [ Maybe [Int] ]
1136             );
1137 5         12802 my ( $self, $project_id, $name, $desc, $due_on ) = $check->(@_);
1138              
1139 3         385 my $stuff = {
1140             name => $name,
1141             description => $desc,
1142             due_on => $due_on # unix timestamp
1143             };
1144              
1145 3         20 return $self->_doRequest( "index.php?/api/v2/add_milestone/$project_id",
1146             'POST', $stuff );
1147             }
1148              
1149             sub deleteMilestone {
1150 4     4 1 4737 state $check = compile( Object, Int );
1151 4         3156 my ( $self, $milestone_id ) = $check->(@_);
1152              
1153 3         64 return $self->_doRequest(
1154             "index.php?/api/v2/delete_milestone/$milestone_id", 'POST' );
1155             }
1156              
1157             sub getMilestones {
1158 7     7 1 5374 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
1159 7         7112 my ( $self, $project_id, $filters ) = $check->(@_);
1160              
1161 6         263 my $response =
1162             $self->_doRequest( "index.php?/api/v2/get_milestones/$project_id"
1163             . _convert_filters_to_string($filters) );
1164 6 100 100     196 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
1165 2         4 my $milestones = $response->{'milestones'};
1166 2 50 50     16 return -500
      33        
1167             if !$milestones || ( reftype($milestones) || 'undef' ) ne 'ARRAY';
1168 2         11 return $milestones;
1169             }
1170              
1171             sub getMilestoneByName {
1172 5     5 1 6396 state $check = compile( Object, Int, Str );
1173 5         4006 my ( $self, $project_id, $name ) = $check->(@_);
1174              
1175 3         83 my $milestones = $self->getMilestones($project_id);
1176 3 100 100     37 return -500
      66        
1177             if !$milestones || ( reftype($milestones) || 'undef' ) ne 'ARRAY';
1178 1         5 foreach my $milestone (@$milestones) {
1179 1 50       8 return $milestone if $milestone->{'name'} eq $name;
1180             }
1181 0         0 return 0;
1182             }
1183              
1184             sub getMilestoneByID {
1185 27     27 1 4664 state $check = compile( Object, Int );
1186 27         7717 my ( $self, $milestone_id ) = $check->(@_);
1187              
1188 26         412 return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
1189             }
1190              
1191             sub getTests {
1192 92     92 1 7741 state $check = compile(
1193             Object, Int,
1194             Optional [ Maybe [ ArrayRef [Int] ] ],
1195             Optional [ Maybe [ ArrayRef [Int] ] ]
1196             );
1197 92         56407 my ( $self, $run_id, $status_ids, $assignedto_ids ) = $check->(@_);
1198              
1199 91         3787 my $query_string = '';
1200 91 50 100     381 $query_string = '&status_id=' . join( ',', @$status_ids )
1201             if defined($status_ids) && scalar(@$status_ids);
1202 91         814 my $response =
1203             $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
1204              
1205 91 100 100     8278 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
1206 87         291 my $results = $response->{'tests'};
1207 87 50 50     758 return -500 if !$results || ( reftype($results) || 'undef' ) ne 'ARRAY';
      33        
1208              
1209             @$results = grep {
1210 87 50 100     291 my $aid = $_->{'assignedto_id'};
  22         28  
1211 22 100       31 grep { defined($aid) && $aid == $_ } @$assignedto_ids
  22         73  
1212             } @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
1213              
1214             #Cache stuff for getTestByName
1215 87   100     342 $self->{tests_cache} //= {};
1216 87         424 $self->{tests_cache}->{$run_id} = $results;
1217              
1218 87         8100 return clone($results);
1219             }
1220              
1221             sub getTestByName {
1222 51     51 1 7851 state $check = compile( Object, Int, Str );
1223 51         10873 my ( $self, $run_id, $name ) = $check->(@_);
1224              
1225 49   100     2156 $self->{tests_cache} //= {};
1226 49         136 my $tests = $self->{tests_cache}->{$run_id};
1227              
1228 49 100       334 $tests = $self->getTests($run_id) if !$tests;
1229 49 100 100     619 return -500 if !$tests || ( reftype($tests) || 'undef' ) ne 'ARRAY';
      66        
1230 47         168 foreach my $test (@$tests) {
1231 137 100       615 return $test if $test->{'title'} eq $name;
1232             }
1233 1         17 return 0;
1234             }
1235              
1236             sub getTestByID {
1237 4     4 1 4751 state $check = compile( Object, Int );
1238 4         3193 my ( $self, $test_id ) = $check->(@_);
1239              
1240 3         64 return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
1241             }
1242              
1243             sub getTestResultFields {
1244 20     20 1 3605 state $check = compile(Object);
1245 20         4565 my ($self) = $check->(@_);
1246              
1247 20 100       279 return $self->{'tr_fields'} if defined( $self->{'tr_fields'} ); #cache
1248 17         159 $self->{'tr_fields'} =
1249             $self->_doRequest('index.php?/api/v2/get_result_fields');
1250 17         2065 return $self->{'tr_fields'};
1251             }
1252              
1253             sub getTestResultFieldByName {
1254 18     18 1 6330 state $check = compile( Object, Str, Optional [ Maybe [Int] ] );
1255 18         15065 my ( $self, $system_name, $project_id ) = $check->(@_);
1256              
1257             my @candidates =
1258 17         983 grep { $_->{'name'} eq $system_name } @{ $self->getTestResultFields() };
  17         120  
  17         85  
1259 17 100       118 return 0 if !scalar(@candidates); #No such name
1260 15 50       116 return -1 if ref( $candidates[0] ) ne 'HASH';
1261             return -2
1262             if ref( $candidates[0]->{'configs'} ) ne 'ARRAY'
1263 15 50 33     136 && !scalar( @{ $candidates[0]->{'configs'} } ); #bogofilter
  0         0  
1264              
1265             #Give it to the user
1266 15         43 my $ret = $candidates[0]; #copy/save for later
1267 15 100       85 return $ret if !defined($project_id);
1268              
1269             #Filter by project ID
1270 14         34 foreach my $config ( @{ $candidates[0]->{'configs'} } ) {
  14         59  
1271             return $ret
1272 62         221 if ( grep { $_ == $project_id }
1273 34 100       60 @{ $config->{'context'}->{'project_ids'} } );
  34         78  
1274             }
1275              
1276 1         3 return -3;
1277             }
1278              
1279             sub getPossibleTestStatuses {
1280 111     111 1 3890 state $check = compile(Object);
1281 111         13799 my ($self) = $check->(@_);
1282 111 100       1366 return $self->{'status_cache'} if $self->{'status_cache'};
1283              
1284 81         647 $self->{'status_cache'} =
1285             $self->_doRequest('index.php?/api/v2/get_statuses');
1286 81         22773 return clone $self->{'status_cache'};
1287             }
1288              
1289             sub statusNamesToIds {
1290 21     21 1 8855 my ( $self, @names ) = @_;
1291 21         70 return _X_in_my_Y( $self, $self->getPossibleTestStatuses(), 'id', @names );
1292             }
1293              
1294             sub statusNamesToLabels {
1295 5     5 1 474 my ( $self, @names ) = @_;
1296 5         22 return _X_in_my_Y( $self, $self->getPossibleTestStatuses(), 'label',
1297             @names );
1298             }
1299              
1300             # Reduce code duplication with internal methods?
1301             # It's more likely than you think
1302             # Free PC check @ cpan.org
1303             sub _X_in_my_Y {
1304 100     100   235 state $check = compile( Object, ArrayRef, Str, slurpy ArrayRef [Str] );
1305 100         82346 my ( $self, $search_arr, $key, $names ) = $check->(@_);
1306              
1307 97         5424 my @ret;
1308 97         454 foreach my $name (@$names) {
1309 127         243 foreach my $member (@$search_arr) {
1310 527 100       871 if ( $member->{'name'} eq $name ) {
1311 123         227 push @ret, $member->{$key};
1312 123         199 last;
1313             }
1314             }
1315             }
1316 97 100       1789 confess("One or more names provided does not exist in TestRail.")
1317             unless scalar(@$names) == scalar(@ret);
1318 93         654 return @ret;
1319             }
1320              
1321             sub createTestResults {
1322 56     56 1 7093 state $check = compile(
1323             Object, Int, Int,
1324             Optional [ Maybe [Str] ],
1325             Optional [ Maybe [HashRef] ],
1326             Optional [ Maybe [HashRef] ]
1327             );
1328 56         32672 my ( $self, $test_id, $status_id, $comment, $opts, $custom_fields ) =
1329             $check->(@_);
1330              
1331 54         3800 my $stuff = {
1332             status_id => $status_id,
1333             comment => $comment
1334             };
1335              
1336             #Handle options
1337 54 100 66     528 if ( defined($opts) && reftype($opts) eq 'HASH' ) {
1338             $stuff->{'version'} =
1339 44 100       249 defined( $opts->{'version'} ) ? $opts->{'version'} : undef;
1340             $stuff->{'elapsed'} =
1341 44 100       187 defined( $opts->{'elapsed'} ) ? $opts->{'elapsed'} : undef;
1342             $stuff->{'defects'} =
1343             defined( $opts->{'defects'} )
1344 44 50       187 ? join( ',', @{ $opts->{'defects'} } )
  0         0  
1345             : undef;
1346             $stuff->{'assignedto_id'} =
1347             defined( $opts->{'assignedto_id'} )
1348 44 50       180 ? $opts->{'assignedto_id'}
1349             : undef;
1350             }
1351              
1352             #Handle custom fields
1353 54 100 66     314 if ( defined($custom_fields) && reftype($custom_fields) eq 'HASH' ) {
1354 7         41 foreach my $field ( keys(%$custom_fields) ) {
1355 7         42 $stuff->{"custom_$field"} = $custom_fields->{$field};
1356             }
1357             }
1358              
1359 54         343 return $self->_doRequest( "index.php?/api/v2/add_result/$test_id",
1360             'POST', $stuff );
1361             }
1362              
1363             sub bulkAddResults {
1364 6     6 1 6219 state $check = compile( Object, Int, ArrayRef [HashRef] );
1365 6         7724 my ( $self, $run_id, $results ) = $check->(@_);
1366              
1367 4         296 return $self->_doRequest( "index.php?/api/v2/add_results/$run_id",
1368             'POST', { 'results' => $results } );
1369             }
1370              
1371             sub bulkAddResultsByCase {
1372 0     0 1 0 state $check = compile( Object, Int, ArrayRef [HashRef] );
1373 0         0 my ( $self, $run_id, $results ) = $check->(@_);
1374              
1375 0         0 return $self->_doRequest( "index.php?/api/v2/add_results_for_cases/$run_id",
1376             'POST', { 'results' => $results } );
1377             }
1378              
1379             sub getTestResults {
1380 12     12 1 6793 state $check = compile(
1381             Object,
1382             Int,
1383             Optional [ Maybe [Int] ],
1384             Optional [ Maybe [Int] ],
1385             Optional [ Maybe [HashRef] ]
1386             );
1387 12         22660 my ( $self, $test_id, $limit, $offset, $filters ) = $check->(@_);
1388              
1389 11         781 my $url = "index.php?/api/v2/get_results/$test_id";
1390 11 100       40 $url .= "&limit=$limit" if $limit;
1391 11 50       32 $url .= "&offset=$offset" if defined($offset);
1392 11         67 $url .= _convert_filters_to_string($filters);
1393 11         34 my $response = $self->_doRequest($url);
1394 11 100 100     532 return -500 if !$response || ( reftype($response) || 'undef' ) ne 'HASH';
      66        
1395 9         20 my $results = $response->{'results'};
1396 9 50 50     59 return -500 if !$results || ( reftype($results) || 'undef' ) ne 'ARRAY';
      33        
1397 9         42 return $results;
1398             }
1399              
1400             sub getResultsForCase {
1401 0     0 1 0 state $check = compile(
1402             Object, Int,
1403             Int, Optional [ Maybe [Int] ],
1404             Optional [ Maybe [Int] ], Optional [ Maybe [HashRef] ]
1405             );
1406 0         0 my ( $self, $run_id, $case_id, $limit, $offset, $filters ) = $check->(@_);
1407              
1408 0         0 my $url = "index.php?/api/v2/get_results_for_case/$run_id/$case_id";
1409 0 0       0 $url .= "&limit=$limit" if $limit;
1410 0 0       0 $url .= "&offset=$offset" if defined($offset);
1411 0         0 $url .= _convert_filters_to_string($filters);
1412 0         0 my $response = $self->_doRequest($url);
1413 0         0 return $response->{'results'};
1414             }
1415              
1416             sub getConfigurationGroups {
1417 199     199 1 4223 state $check = compile( Object, Int );
1418 199         11548 my ( $self, $project_id ) = $check->(@_);
1419              
1420 198         3299 my $url = "index.php?/api/v2/get_configs/$project_id";
1421 198         678 return $self->_doRequest($url);
1422             }
1423              
1424             sub getConfigurationGroupByName {
1425 4     4 1 682 state $check = compile( Object, Int, Str );
1426 4         3830 my ( $self, $project_id, $name ) = $check->(@_);
1427              
1428 4         100 my $cgroups = $self->getConfigurationGroups($project_id);
1429 4 50       233 return 0 if ref($cgroups) ne 'ARRAY';
1430 4         17 @$cgroups = grep { $_->{'name'} eq $name } @$cgroups;
  12         45  
1431 4 100       24 return 0 unless scalar(@$cgroups);
1432 1         3 return $cgroups->[0];
1433             }
1434              
1435             sub addConfigurationGroup {
1436 4     4 1 612 state $check = compile( Object, Int, Str );
1437 4         2767 my ( $self, $project_id, $name ) = $check->(@_);
1438              
1439 4         107 my $url = "index.php?/api/v2/add_config_group/$project_id";
1440 4         22 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1441             }
1442              
1443             sub editConfigurationGroup {
1444 1     1 1 579 state $check = compile( Object, Int, Str );
1445 1         1272 my ( $self, $config_group_id, $name ) = $check->(@_);
1446              
1447 1         21 my $url = "index.php?/api/v2/update_config_group/$config_group_id";
1448 1         6 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1449             }
1450              
1451             sub deleteConfigurationGroup {
1452 1     1 1 4 state $check = compile( Object, Int );
1453 1         1038 my ( $self, $config_group_id ) = $check->(@_);
1454              
1455 1         17 my $url = "index.php?/api/v2/delete_config_group/$config_group_id";
1456 1         5 return $self->_doRequest( $url, 'POST' );
1457             }
1458              
1459             sub getConfigurations {
1460 195     195 1 5712 state $check = compile( Object, Int );
1461 195         12616 my ( $self, $project_id ) = $check->(@_);
1462              
1463 193         4188 my $cgroups = $self->getConfigurationGroups($project_id);
1464 193         9672 my $configs = [];
1465 193 100 100     1569 return $cgroups unless ( reftype($cgroups) || 'undef' ) eq 'ARRAY';
1466 190         1136 foreach my $cfg (@$cgroups) {
1467 378         659 push( @$configs, @{ $cfg->{'configs'} } );
  378         890  
1468             }
1469 190         1189 return $configs;
1470             }
1471              
1472             sub addConfiguration {
1473 4     4 1 636 state $check = compile( Object, Int, Str );
1474 4         2707 my ( $self, $configuration_group_id, $name ) = $check->(@_);
1475              
1476 4         106 my $url = "index.php?/api/v2/add_config/$configuration_group_id";
1477 4         22 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1478             }
1479              
1480             sub editConfiguration {
1481 1     1 1 621 state $check = compile( Object, Int, Str );
1482 1         1306 my ( $self, $config_id, $name ) = $check->(@_);
1483              
1484 1         22 my $url = "index.php?/api/v2/update_config/$config_id";
1485 1         6 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1486             }
1487              
1488             sub deleteConfiguration {
1489 1     1 1 633 state $check = compile( Object, Int );
1490 1         1049 my ( $self, $config_id ) = $check->(@_);
1491              
1492 1         16 my $url = "index.php?/api/v2/delete_config/$config_id";
1493 1         4 return $self->_doRequest( $url, 'POST' );
1494             }
1495              
1496             sub translateConfigNamesToIds {
1497 61     61 1 4691 my ( $self, $project_id, @names ) = @_;
1498 61 50       217 my $configs = $self->getConfigurations($project_id)
1499             or confess("Could not determine configurations in provided project.");
1500 60         387 return _X_in_my_Y( $self, $configs, 'id', @names );
1501             }
1502              
1503             sub getReports {
1504 1     1 1 320 state $check = compile( Object, Int );
1505 1         1139 my ( $self, $project_id ) = $check->(@_);
1506 1         17 my $url = "index.php?/api/v2/get_reports/$project_id";
1507 1         4 return $self->_doRequest( $url, 'GET' );
1508             }
1509              
1510             sub runReport {
1511 1     1 1 1128 state $check = compile( Object, Int );
1512 1         1043 my ( $self, $report_id ) = $check->(@_);
1513 1         16 my $url = "index.php?/api/v2/run_report/$report_id";
1514 1         4 return $self->_doRequest( $url, 'GET' );
1515             }
1516              
1517             #Convenience method for building stepResults
1518             sub buildStepResults {
1519 16     16 1 106 state $check = compile( Str, Str, Str, Int );
1520 16         5279 my ( $content, $expected, $actual, $status_id ) = $check->(@_);
1521              
1522             return {
1523 16         646 content => $content,
1524             expected => $expected,
1525             actual => $actual,
1526             status_id => $status_id
1527             };
1528             }
1529              
1530             # Convenience method for building filter string from filters Hashref
1531             sub _convert_filters_to_string {
1532 404     404   1045 state $check = compile( Maybe [HashRef] );
1533 404         23663 my ($filters) = $check->(@_);
1534              
1535 404   100     6605 $filters //= {};
1536              
1537 404         1317 my $filter_string = '';
1538 404         2022 foreach my $filter ( keys(%$filters) ) {
1539 29 50       87 if ( ref $filters->{$filter} eq 'ARRAY' ) {
1540             $filter_string .=
1541 0         0 "&$filter=" . join( ',', @{ $filters->{$filter} } );
  0         0  
1542             }
1543             else {
1544             $filter_string .= "&$filter=" . $filters->{$filter}
1545 29 100       123 if defined( $filters->{$filter} );
1546             }
1547             }
1548 404         1553 return $filter_string;
1549             }
1550              
1551             1;
1552              
1553             __END__