File Coverage

blib/lib/TestRail/API.pm
Criterion Covered Total %
statement 661 713 92.7
branch 216 286 75.5
condition 111 157 70.7
subroutine 110 112 98.2
pod 91 91 100.0
total 1189 1359 87.4


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.051';
6              
7 21     21   602690 use 5.010;
  21         166  
8              
9 21     21   128 use strict;
  21         47  
  21         475  
10 21     21   101 use warnings;
  21         43  
  21         814  
11              
12 21     21   143 use Carp qw{cluck confess};
  21         45  
  21         1547  
13 21     21   148 use Scalar::Util qw{reftype looks_like_number};
  21         49  
  21         1508  
14 21     21   6623 use Clone 'clone';
  21         36847  
  21         1248  
15 21     21   4210 use Try::Tiny;
  21         17602  
  21         1458  
16              
17             use Types::Standard
18 21     21   12181 qw( slurpy ClassName Object Str Int Bool HashRef ArrayRef Maybe Optional);
  21         1660065  
  21         264  
19 21     21   52657 use Type::Params qw( compile );
  21         247205  
  21         251  
20              
21 21     21   12261 use JSON::MaybeXS 1.001000 ();
  21         78770  
  21         572  
22 21     21   6871 use HTTP::Request;
  21         251482  
  21         782  
23 21     21   10554 use LWP::UserAgent;
  21         343958  
  21         788  
24 21     21   9977 use HTTP::CookieJar::LWP;
  21         605230  
  21         936  
25 21     21   11730 use Data::Validate::URI qw{is_uri};
  21         1043572  
  21         1733  
26 21     21   192 use List::Util 1.33;
  21         539  
  21         1214  
27 21     21   9413 use Encode ();
  21         174305  
  21         251253  
28              
29             sub new {
30 106     106 1 14396 state $check = compile( ClassName,
31             Str,
32             Str,
33             Str,
34             Optional [ Maybe [Str] ],
35             Optional [ Maybe [Bool] ],
36             Optional [ Maybe [Bool] ],
37             Optional [ Maybe [Int] ],
38             Optional [ Maybe [HashRef] ]
39             );
40             my (
41 106         198273 $class, $apiurl, $user,
42             $pass, $encoding, $debug,
43             $do_post_redirect, $max_tries, $userfetch_opts
44             ) = $check->(@_);
45              
46 106 100       22937 die("Invalid URI passed to constructor") if !is_uri($apiurl);
47 105   50     32805 $debug //= 0;
48              
49             my $self = {
50             user => $user,
51             pass => $pass,
52             apiurl => $apiurl,
53             debug => $debug,
54             encoding => $encoding || 'UTF-8',
55             testtree => [],
56             flattree => [],
57             user_cache => [],
58             configurations => {},
59             tr_fields => undef,
60 105   50     3266 tr_project_id => $userfetch_opts->{'project_id'},
      100        
61             default_request => undef,
62             global_limit => 250, #Discovered by experimentation
63             browser => LWP::UserAgent->new(
64             keep_alive => 10,
65             cookie_jar => HTTP::CookieJar::LWP->new(),
66             ),
67             do_post_redirect => $do_post_redirect,
68             max_tries => $max_tries // 1,
69             retry_delay => 5,
70             };
71              
72             #Allow POST redirects
73 105 100       103744 if ( $self->{do_post_redirect} ) {
74 56         173 push @{ $self->{'browser'}->requests_redirectable }, 'POST';
  56         415  
75             }
76              
77             #Check chara encoding
78             $self->{'encoding-nonaliased'} =
79 105         2523 Encode::resolve_alias( $self->{'encoding'} );
80             die( "Invalid encoding alias '"
81             . $self->{'encoding'}
82             . "' passed, see Encoding::Supported for a list of allowed encodings"
83 105 50       11162 ) unless $self->{'encoding-nonaliased'};
84              
85             die( "Invalid encoding '"
86             . $self->{'encoding-nonaliased'}
87             . "' passed, see Encoding::Supported for a list of allowed encodings"
88             )
89 105 50       877 unless grep { $_ eq $self->{'encoding-nonaliased'} }
  13020         137587  
90             ( Encode->encodings(":all") );
91              
92             #Create default request to pass on to LWP::UserAgent
93 105         1900 $self->{'default_request'} = HTTP::Request->new();
94 105         7878 $self->{'default_request'}->authorization_basic( $user, $pass );
95              
96 105         36393 bless( $self, $class );
97 105 100       520 return $self if $self->debug; #For easy class testing without mocks
98              
99             # Manually do the get_users call to check HTTP status...
100             # Allow users to skip the check if you have a zillion users etc,
101             # as apparently that is fairly taxing on TR itself.
102 1 50       5 if ( !$userfetch_opts->{skip_usercache} ) {
103 1         5 my $res = $self->getUsers( $userfetch_opts->{project_id} );
104 1 50       8 confess "Error: network unreachable" if !defined($res);
105 1 50 50     10 if ( ( reftype($res) || 'undef' ) ne 'ARRAY' ) {
106 1 50       6 confess "Unexpected return from _doRequest: $res"
107             if !looks_like_number($res);
108 1 50       331 confess
109             "Could not communicate with TestRail Server! Check that your URI is correct, and your TestRail installation is functioning correctly."
110             if $res == -500;
111 0 0       0 confess
112             "Could not list testRail users! Check that your TestRail installation has it's API enabled, and your credentials are correct"
113             if $res == -403;
114 0 0       0 confess "Bad user credentials!" if $res == -401;
115 0 0       0 confess
116             "HTTP error $res encountered while communicating with TestRail server. Resolve issue and try again."
117             if $res < 0;
118 0         0 confess "Unknown error occurred: $res";
119             }
120             confess
121 0 0       0 "No users detected on TestRail Install! Check that your API is functioning correctly."
122             if !scalar(@$res);
123             }
124              
125 0         0 return $self;
126             }
127              
128             sub apiurl {
129 1830     1830 1 5136 state $check = compile(Object);
130 1830         19091 my ($self) = $check->(@_);
131 1830         26229 return $self->{'apiurl'};
132             }
133              
134             sub debug {
135 1828     1828 1 3350 state $check = compile(Object);
136 1828         20789 my ($self) = $check->(@_);
137 1828         19660 return $self->{'debug'};
138             }
139              
140             #Convenient JSON-HTTP fetcher
141             sub _doRequest {
142 1722     1722   9089 state $check = compile( Object, Str,
143             Optional [ Maybe [Str] ],
144             Optional [ Maybe [HashRef] ]
145             );
146 1722         89643 my ( $self, $path, $method, $data ) = $check->(@_);
147              
148 1722         39389 $self->{num_tries}++;
149              
150 1722         32030 my $req = clone $self->{'default_request'};
151 1722   100     10213 $method //= 'GET';
152              
153 1722         8588 $req->method($method);
154 1722         24834 $req->url( $self->apiurl . '/' . $path );
155              
156 1722 100       318633 warn "$method " . $self->apiurl . "/$path" if $self->debug;
157              
158 1722         11722 my $coder = JSON::MaybeXS->new;
159              
160             #Data sent is JSON, and encoded per user preference
161             my $content =
162             $data
163 1722 100       36896 ? Encode::encode( $self->{'encoding-nonaliased'}, $coder->encode($data) )
164             : '';
165              
166 1722         21180 $req->content($content);
167             $req->header(
168 1722         44597 "Content-Type" => "application/json; charset=" . $self->{'encoding'} );
169              
170 1722         111425 my $response = eval { $self->{'browser'}->request($req) };
  1722         8742  
171              
172             #Uncomment to generate mocks
173             #use Data::Dumper;
174             #open(my $fh, '>>', 'mock.out');
175             #print $fh "{\n\n";
176             #print $fh Dumper($path,'200','OK',$response->headers,$response->content);
177             #print $fh '$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));';
178             #print $fh "\n\n}\n\n";
179             #close $fh;
180              
181 1722 50       3718501 if ($@) {
182              
183             #LWP threw an ex, probably a timeout
184 0 0       0 if ( $self->{num_tries} >= $self->{max_tries} ) {
185 0         0 $self->{num_tries} = 0;
186 0         0 confess "Failed to satisfy request after $self->{num_tries} tries!";
187             }
188             cluck
189 0         0 "WARNING: TestRail API request failed due to timeout, or other LWP fatal condition, re-trying request...\n";
190 0 0       0 sleep $self->{retry_delay} if $self->{retry_delay};
191 0         0 goto &_doRequest;
192             }
193              
194 1722 50       4886 return $response if !defined($response); #worst case
195              
196 1722 100       4573 if ( $response->code == 403 ) {
197 1         14 confess "ERROR 403: Access Denied: " . $response->content;
198             }
199 1721 100       19421 if ( $response->code == 401 ) {
200 1         13 confess "ERROR 401: Authentication failed: " . $response->content;
201             }
202              
203 1720 100       17845 if ( $response->code != 200 ) {
204              
205             #LWP threw an ex, probably a timeout
206 117 100       1295 if ( $self->{num_tries} >= $self->{max_tries} ) {
207 115         193 $self->{num_tries} = 0;
208 115         263 cluck "ERROR: Arguments Bad? (got code "
209             . $response->code . "): "
210             . $response->content;
211 115         42726 return -int( $response->code );
212             }
213 2         8 cluck "WARNING: TestRail API request failed (got code "
214             . $response->code
215             . "), re-trying request...\n";
216 2 100       5001622 sleep $self->{retry_delay} if $self->{retry_delay};
217 2         63 goto &_doRequest;
218              
219             }
220 1603         17040 $self->{num_tries} = 0;
221              
222             try {
223 1603     1603   73240 return $coder->decode( $response->content );
224             }
225             catch {
226 9 50 33 9   328 if ( $response->code == 200 && !$response->content ) {
227 9         343 return 1; #This function probably just returns no data
228             }
229             else {
230 0         0 cluck "ERROR: Malformed JSON returned by API.";
231 0         0 cluck $@;
232 0 0       0 if ( !$self->debug )
233             { #Otherwise we've already printed this, but we need to know if we encounter this
234 0         0 cluck "RAW CONTENT:";
235 0         0 cluck $response->content;
236             }
237 0         0 return 0;
238             }
239             }
240 1603         14140 }
241              
242             sub getUsers {
243 78     78 1 6531 state $check = compile( Object, Optional [ Maybe [Str] ] );
244 78         24814 my ( $self, $project_id ) = $check->(@_);
245              
246             # Return shallow clone of user_cache if set.
247 6         20 return [ @{ $self->{'user_cache'} } ]
248             if ref $self->{'user_cache'} eq 'ARRAY'
249 78 100 50     3070 && scalar( @{ $self->{'user_cache'} } );
  78         435  
250 72 100       397 my $maybe_project = $project_id ? "/$project_id" : '';
251 72         390 my $res = $self->_doRequest("index.php?/api/v2/get_users$maybe_project");
252 70 100 100     3625 return -500 if !$res || ( reftype($res) || 'undef' ) ne 'ARRAY';
      66        
253 60         286 $self->{'user_cache'} = $res;
254 60         1491 return clone($res);
255             }
256              
257             sub getUserByID {
258 4     4 1 6753 state $check = compile( Object, Int );
259 4         3974 my ( $self, $user ) = $check->(@_);
260              
261 3         61 my $users = $self->getUsers();
262 3 100       26 return $users if ref $users ne 'ARRAY';
263 1         4 foreach my $usr (@$users) {
264 1 50       9 return $usr if $usr->{'id'} == $user;
265             }
266 0         0 return 0;
267             }
268              
269             sub getUserByName {
270 4     4 1 5897 state $check = compile( Object, Str );
271 4         3909 my ( $self, $user ) = $check->(@_);
272              
273 3         61 my $users = $self->getUsers();
274 3 100       22 return $users if ref $users ne 'ARRAY';
275 1         5 foreach my $usr (@$users) {
276 1 50       9 return $usr if $usr->{'name'} eq $user;
277             }
278 0         0 return 0;
279             }
280              
281             sub getUserByEmail {
282 4     4 1 6060 state $check = compile( Object, Str );
283 4         4222 my ( $self, $email ) = $check->(@_);
284              
285 3         63 my $users = $self->getUsers();
286 3 100       20 return $users if ref $users ne 'ARRAY';
287 1         4 foreach my $usr (@$users) {
288 1 50       6 return $usr if $usr->{'email'} eq $email;
289             }
290 0         0 return 0;
291             }
292              
293             sub userNamesToIds {
294 7     7 1 11324 state $check = compile( Object, slurpy ArrayRef [Str] );
295 7         26599 my ( $self, $names ) = $check->(@_);
296              
297 7 100       819 confess("At least one user name must be provided") if !scalar(@$names);
298 12         27 my @ret = grep { defined $_ } map {
299 12         23 my $user = $_;
300 12         22 my @list = grep { $user->{'name'} eq $_ } @$names;
  18         48  
301 12 100       45 scalar(@list) ? $user->{'id'} : undef
302 6         16 } @{ $self->getUsers() };
  6         22  
303 6 100       251 confess("One or more user names provided does not exist in TestRail.")
304             unless scalar(@$names) == scalar(@ret);
305 5         23 return @ret;
306             }
307              
308             sub createProject {
309 4     4 1 7553 state $check = compile( Object, Str,
310             Optional [ Maybe [Str] ],
311             Optional [ Maybe [Bool] ]
312             );
313 4         13604 my ( $self, $name, $desc, $announce ) = $check->(@_);
314              
315 3   100     406 $desc //= 'res ipsa loquiter';
316 3   50     30 $announce //= 0;
317              
318 3         18 my $input = {
319             name => $name,
320             announcement => $desc,
321             show_announcement => $announce
322             };
323              
324 3         19 return $self->_doRequest( 'index.php?/api/v2/add_project', 'POST', $input );
325             }
326              
327             sub deleteProject {
328 4     4 1 5632 state $check = compile( Object, Int );
329 4         3965 my ( $self, $proj ) = $check->(@_);
330              
331 3         67 return $self->_doRequest( 'index.php?/api/v2/delete_project/' . $proj,
332             'POST' );
333             }
334              
335             sub getProjects {
336 106     106 1 4492 state $check = compile( Object, Optional [ Maybe [HashRef] ] );
337 106         44992 my ( $self, $filters ) = $check->(@_);
338              
339 106         3901 my $result = $self->_doRequest( 'index.php?/api/v2/get_projects'
340             . _convert_filters_to_string($filters) );
341              
342             #Save state for future use, if needed
343 106 100 100     6755 return -500 if !$result || ( reftype($result) || 'undef' ) ne 'ARRAY';
      66        
344 100         342 $self->{'testtree'} = $result;
345              
346             #Note that it's a project for future reference by recursive tree search
347 100 50 50     1318 return -500 if !$result || ( reftype($result) || 'undef' ) ne 'ARRAY';
      33        
348 100         289 foreach my $pj ( @{$result} ) {
  100         399  
349 300         675 $pj->{'type'} = 'project';
350             }
351              
352 100         311 return $result;
353             }
354              
355             sub getProjectByName {
356 121     121 1 6055 state $check = compile( Object, Str );
357 121         18967 my ( $self, $project ) = $check->(@_);
358              
359             #See if we already have the project list...
360 120         2243 my $projects = $self->{'testtree'};
361 120 50 50     1335 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      33        
362 120 100       756 $projects = $self->getProjects() unless scalar(@$projects);
363              
364             #Search project list for project
365 120 100 100     827 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
366 118         444 for my $candidate (@$projects) {
367 232 100       945 return $candidate if ( $candidate->{'name'} eq $project );
368             }
369              
370 1         4 return 0;
371             }
372              
373             sub getProjectByID {
374 8     8 1 5647 state $check = compile( Object, Int );
375 8         6113 my ( $self, $project ) = $check->(@_);
376              
377             #See if we already have the project list...
378 7         146 my $projects = $self->{'testtree'};
379 7 100       63 $projects = $self->getProjects() unless scalar(@$projects);
380              
381             #Search project list for project
382 7 100 100     105 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
383 5         32 for my $candidate (@$projects) {
384 7 100       45 return $candidate if ( $candidate->{'id'} eq $project );
385             }
386              
387 0         0 return 0;
388             }
389              
390             sub createTestSuite {
391 5     5 1 9003 state $check = compile( Object, Int, Str, Optional [ Maybe [Str] ] );
392 5         9670 my ( $self, $project_id, $name, $details ) = $check->(@_);
393              
394 3   100     294 $details //= 'res ipsa loquiter';
395 3         13 my $input = {
396             name => $name,
397             description => $details
398             };
399              
400 3         19 return $self->_doRequest( 'index.php?/api/v2/add_suite/' . $project_id,
401             'POST', $input );
402             }
403              
404             sub deleteTestSuite {
405 4     4 1 5568 state $check = compile( Object, Int );
406 4         3961 my ( $self, $suite_id ) = $check->(@_);
407              
408 3         68 return $self->_doRequest( 'index.php?/api/v2/delete_suite/' . $suite_id,
409             'POST' );
410             }
411              
412             sub getTestSuites {
413 15     15 1 8080 state $check = compile( Object, Int );
414 15         10320 my ( $self, $proj ) = $check->(@_);
415              
416 14         340 return $self->_doRequest( 'index.php?/api/v2/get_suites/' . $proj );
417             }
418              
419             sub getTestSuiteByName {
420 13     13 1 8397 state $check = compile( Object, Int, Str );
421 13         18258 my ( $self, $project_id, $testsuite_name ) = $check->(@_);
422              
423             #TODO cache
424 11         303 my $suites = $self->getTestSuites($project_id);
425 11 100 100     615 return -500
      66        
426             if !$suites
427             || ( reftype($suites) || 'undef' ) ne
428             'ARRAY'; #No suites for project, or no project
429 9         37 foreach my $suite (@$suites) {
430 9 50       65 return $suite if $suite->{'name'} eq $testsuite_name;
431             }
432 0         0 return 0; #Couldn't find it
433             }
434              
435             sub getTestSuiteByID {
436 4     4 1 5720 state $check = compile( Object, Int );
437 4         3780 my ( $self, $testsuite_id ) = $check->(@_);
438              
439 3         67 return $self->_doRequest( 'index.php?/api/v2/get_suite/' . $testsuite_id );
440             }
441              
442             sub createSection {
443 6     6 1 12077 state $check = compile( Object, Int, Int, Str, Optional [ Maybe [Int] ] );
444 6         10795 my ( $self, $project_id, $suite_id, $name, $parent_id ) = $check->(@_);
445              
446 3         292 my $input = {
447             name => $name,
448             suite_id => $suite_id
449             };
450 3 50       19 $input->{'parent_id'} = $parent_id if $parent_id;
451              
452 3         19 return $self->_doRequest( 'index.php?/api/v2/add_section/' . $project_id,
453             'POST', $input );
454             }
455              
456             sub deleteSection {
457 4     4 1 6008 state $check = compile( Object, Int );
458 4         3884 my ( $self, $section_id ) = $check->(@_);
459              
460 3         69 return $self->_doRequest( 'index.php?/api/v2/delete_section/' . $section_id,
461             'POST' );
462             }
463              
464             sub getSections {
465 34     34 1 8837 state $check = compile( Object, Int, Int );
466 34         13773 my ( $self, $project_id, $suite_id ) = $check->(@_);
467              
468             #Cache sections to reduce requests in tight loops
469 30 100       779 return $self->{'sections'}->{$suite_id} if $self->{'sections'}->{$suite_id};
470 14         103 $self->{'sections'}->{$suite_id} = $self->_doRequest(
471             "index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
472              
473 14         785 return $self->{'sections'}->{$suite_id};
474             }
475              
476             sub getSectionByID {
477 4     4 1 6221 state $check = compile( Object, Int );
478 4         4269 my ( $self, $section_id ) = $check->(@_);
479              
480 3         67 return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
481             }
482              
483             sub getSectionByName {
484 7     7 1 11977 state $check = compile( Object, Int, Int, Str );
485 7         8198 my ( $self, $project_id, $suite_id, $section_name ) = $check->(@_);
486              
487 4         152 my $sections = $self->getSections( $project_id, $suite_id );
488 4 100 100     56 return -500 if !$sections || ( reftype($sections) || 'undef' ) ne 'ARRAY';
      66        
489 2         7 foreach my $sec (@$sections) {
490 6 100       22 return $sec if $sec->{'name'} eq $section_name;
491             }
492 0         0 return 0;
493             }
494              
495             sub getChildSections {
496 11     11 1 5470 state $check = compile( Object, Int, HashRef );
497 11         6149 my ( $self, $project_id, $section ) = $check->(@_);
498              
499 11         259 my $sections_orig = $self->getSections( $project_id, $section->{suite_id} );
500 11 100 100     159 return []
      66        
501             if !$sections_orig || ( reftype($sections_orig) || 'undef' ) ne 'ARRAY';
502             my @sections =
503 10 100       36 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $section->{'id'} : 0 }
  50         144  
504             @$sections_orig;
505 10         38 foreach my $sec (@sections) {
506             push( @sections,
507 9 100       15 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $sec->{'id'} : 0 }
  72         140  
508             @$sections_orig );
509             }
510 10         43 return \@sections;
511             }
512              
513             sub sectionNamesToIds {
514 14     14 1 9895 my ( $self, $project_id, $suite_id, @names ) = @_;
515 14 50       108 my $sections = $self->getSections( $project_id, $suite_id )
516             or confess("Could not find sections in provided project/suite.");
517 12         141 return _X_in_my_Y( $self, $sections, 'id', @names );
518             }
519              
520             sub getCaseTypes {
521 16     16 1 4006 state $check = compile(Object);
522 16         4586 my ($self) = $check->(@_);
523 16 100       436 return clone( $self->{'type_cache'} ) if defined( $self->{'type_cache'} );
524              
525 7         40 my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
526 7 100 100     298 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
527 3         13 $self->{'type_cache'} = $types;
528              
529 3         159 return clone $types;
530             }
531              
532             sub getCaseTypeByName {
533 13     13 1 8530 state $check = compile( Object, Str );
534 13         6328 my ( $self, $name ) = $check->(@_);
535              
536 12         220 my $types = $self->getCaseTypes();
537 12 100 100     112 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
538 10         25 foreach my $type (@$types) {
539 16 100       95 return $type if $type->{'name'} eq $name;
540             }
541 0         0 confess("No such case type '$name'!");
542             }
543              
544             sub typeNamesToIds {
545 1     1 1 8 my ( $self, @names ) = @_;
546 1         5 return _X_in_my_Y( $self, $self->getCaseTypes(), 'id', @names );
547             }
548              
549             sub createCase {
550 5     5 1 8416 state $check = compile( Object, Int, Str,
551             Optional [ Maybe [Int] ],
552             Optional [ Maybe [HashRef] ],
553             Optional [ Maybe [HashRef] ]
554             );
555 5         16615 my ( $self, $section_id, $title, $type_id, $opts, $extras ) = $check->(@_);
556              
557 3         431 my $stuff = {
558             title => $title,
559             type_id => $type_id
560             };
561              
562             #Handle sort of optional but baked in options
563 3 50 33     16 if ( defined($extras) && reftype($extras) eq 'HASH' ) {
564             $stuff->{'priority_id'} = $extras->{'priority_id'}
565 0 0       0 if defined( $extras->{'priority_id'} );
566             $stuff->{'estimate'} = $extras->{'estimate'}
567 0 0       0 if defined( $extras->{'estimate'} );
568             $stuff->{'milestone_id'} = $extras->{'milestone_id'}
569 0 0       0 if defined( $extras->{'milestone_id'} );
570 0         0 $stuff->{'refs'} = join( ',', @{ $extras->{'refs'} } )
571 0 0       0 if defined( $extras->{'refs'} );
572             }
573              
574             #Handle custom fields
575 3 50 33     16 if ( defined($opts) && reftype($opts) eq 'HASH' ) {
576 0         0 foreach my $key ( keys(%$opts) ) {
577 0         0 $stuff->{"custom_$key"} = $opts->{$key};
578             }
579             }
580              
581 3         18 return $self->_doRequest( "index.php?/api/v2/add_case/$section_id",
582             'POST', $stuff );
583             }
584              
585             sub updateCase {
586 1     1 1 854 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
587 1         2846 my ( $self, $case_id, $options ) = $check->(@_);
588              
589 1         114 return $self->_doRequest( "index.php?/api/v2/update_case/$case_id",
590             'POST', $options );
591             }
592              
593             sub deleteCase {
594 4     4 1 5842 state $check = compile( Object, Int );
595 4         3822 my ( $self, $case_id ) = $check->(@_);
596              
597 3         70 return $self->_doRequest( "index.php?/api/v2/delete_case/$case_id",
598             'POST' );
599             }
600              
601             sub getCases {
602 24     24 1 12244 state $check = compile( Object, Int, Int, Optional [ Maybe [HashRef] ] );
603 24         27239 my ( $self, $project_id, $suite_id, $filters ) = $check->(@_);
604              
605 22         1381 my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
606 22         99 $url .= _convert_filters_to_string($filters);
607              
608 22         88 return $self->_doRequest($url);
609             }
610              
611             sub getCaseByName {
612 7     7 1 14511 state $check =
613             compile( Object, Int, Int, Str, Optional [ Maybe [HashRef] ] );
614 7         10722 my ( $self, $project_id, $suite_id, $name, $filters ) = $check->(@_);
615              
616 4         320 my $cases = $self->getCases( $project_id, $suite_id, $filters );
617 4 100 100     152 return -500 if !$cases || ( reftype($cases) || 'undef' ) ne 'ARRAY';
      66        
618 1         4 foreach my $case (@$cases) {
619 1 50       10 return $case if $case->{'title'} eq $name;
620             }
621 0         0 return 0;
622             }
623              
624             sub getCaseByID {
625 4     4 1 5552 state $check = compile( Object, Int );
626 4         4093 my ( $self, $case_id ) = $check->(@_);
627              
628 3         69 return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
629             }
630              
631             sub getCaseFields {
632 3     3 1 746 state $check = compile(Object);
633 3         894 my ($self) = $check->(@_);
634 3 100       37 return $self->{case_fields} if $self->{case_fields};
635              
636             $self->{case_fields} =
637 2         8 $self->_doRequest("index.php?/api/v2/get_case_fields");
638 2         15 return $self->{case_fields};
639             }
640              
641             sub addCaseField {
642 1     1 1 5 state $check = compile( Object, slurpy HashRef );
643 1         1266 my ( $self, $options ) = $check->(@_);
644 1         20 $self->{case_fields} = undef;
645 1         5 return $self->_doRequest( "index.php?/api/v2/add_case_field", 'POST',
646             $options );
647             }
648              
649             sub getPriorities {
650 4     4 1 3759 state $check = compile(Object);
651 4         1976 my ($self) = $check->(@_);
652             return clone( $self->{'priority_cache'} )
653 4 100       84 if defined( $self->{'priority_cache'} );
654              
655 2         9 my $priorities = $self->_doRequest("index.php?/api/v2/get_priorities");
656 2 100 100     85 return -500
      66        
657             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
658 1         3 $self->{'priority_cache'} = $priorities;
659              
660 1         26 return clone $priorities;
661             }
662              
663             sub getPriorityByName {
664 2     2 1 3163 state $check = compile( Object, Str );
665 2         2623 my ( $self, $name ) = $check->(@_);
666              
667 1         17 my $priorities = $self->getPriorities();
668 1 50 50     11 return -500
      33        
669             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
670 1         4 foreach my $priority (@$priorities) {
671 1 50       10 return $priority if $priority->{'name'} eq $name;
672             }
673 0         0 confess("No such priority '$name'!");
674             }
675              
676             sub priorityNamesToIds {
677 1     1 1 5 my ( $self, @names ) = @_;
678 1         4 return _X_in_my_Y( $self, $self->getPriorities(), 'id', @names );
679             }
680              
681             #If you pass an array of case ids, it implies include_all is false
682             sub createRun {
683 277     277 1 26176 state $check = compile( Object, Int, Int, Str,
684             Optional [ Maybe [Str] ],
685             Optional [ Maybe [Int] ],
686             Optional [ Maybe [Int] ],
687             Optional [ Maybe [ ArrayRef [Int] ] ]
688             );
689 277         59628 my ( $self, $project_id, $suite_id, $name, $desc, $milestone_id,
690             $assignedto_id, $case_ids )
691             = $check->(@_);
692              
693 274 100       11821 my $stuff = {
694             suite_id => $suite_id,
695             name => $name,
696             description => $desc,
697             milestone_id => $milestone_id,
698             assignedto_id => $assignedto_id,
699             include_all => defined($case_ids) ? 0 : 1,
700             case_ids => $case_ids
701             };
702              
703 274         944 return $self->_doRequest( "index.php?/api/v2/add_run/$project_id",
704             'POST', $stuff );
705             }
706              
707             sub deleteRun {
708 4     4 1 5622 state $check = compile( Object, Int );
709 4         4263 my ( $self, $run_id ) = $check->(@_);
710              
711 3         68 return $self->_doRequest( "index.php?/api/v2/delete_run/$run_id", 'POST' );
712             }
713              
714             sub getRuns {
715 112     112 1 7656 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
716 112         44052 my ( $self, $project_id, $filters ) = $check->(@_);
717              
718             my $initial_runs =
719 111         5457 $self->getRunsPaginated( $project_id, $self->{'global_limit'},
720             0, $filters );
721 111 100 100     94679 return $initial_runs
722             unless ( reftype($initial_runs) || 'undef' ) eq 'ARRAY';
723 107         298 my $runs = [];
724 107         1075 push( @$runs, @$initial_runs );
725 107         296 my $offset = 1;
726 107         506 while ( scalar(@$initial_runs) == $self->{'global_limit'} ) {
727             $initial_runs = $self->getRunsPaginated(
728             $project_id,
729             $self->{'global_limit'},
730 22         129 ( $self->{'global_limit'} * $offset ), $filters
731             );
732 22         2213 push( @$runs, @$initial_runs );
733 22         101 $offset++;
734             }
735 107         453 return $runs;
736             }
737              
738             sub getRunsPaginated {
739 135     135 1 5021 state $check = compile( Object,
740             Int,
741             Optional [ Maybe [Int] ],
742             Optional [ Maybe [Int] ],
743             Optional [ Maybe [HashRef] ]
744             );
745 135         79436 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
746              
747             confess( "Limit greater than " . $self->{'global_limit'} )
748 134 50       9363 if $limit > $self->{'global_limit'};
749 134         769 my $apiurl = "index.php?/api/v2/get_runs/$project_id";
750 134 100       718 $apiurl .= "&offset=$offset" if defined($offset);
751 134 100       816 $apiurl .= "&limit=$limit"
752             if $limit; #You have problems if you want 0 results
753 134         1128 $apiurl .= _convert_filters_to_string($filters);
754 134         1269 return $self->_doRequest($apiurl);
755             }
756              
757             sub getRunByName {
758 60     60 1 8507 state $check = compile( Object, Int, Str );
759 60         20287 my ( $self, $project_id, $name ) = $check->(@_);
760              
761 58         1833 my $runs = $self->getRuns($project_id);
762 58 100 100     645 return -500 if !$runs || ( reftype($runs) || 'undef' ) ne 'ARRAY';
      66        
763 56         249 foreach my $run (@$runs) {
764 178 100       2425 return $run if $run->{'name'} eq $name;
765             }
766 19         370 return 0;
767             }
768              
769             sub getRunByID {
770 6     6 1 6514 state $check = compile( Object, Int );
771 6         3880 my ( $self, $run_id ) = $check->(@_);
772              
773 5         106 return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
774             }
775              
776             sub closeRun {
777 4     4 1 5116 state $check = compile( Object, Int );
778 4         4166 my ( $self, $run_id ) = $check->(@_);
779              
780 4         104 return $self->_doRequest( "index.php?/api/v2/close_run/$run_id", 'POST' );
781             }
782              
783             sub getRunSummary {
784 14     14 1 2549 state $check = compile( Object, slurpy ArrayRef [HashRef] );
785 14         55325 my ( $self, $runs ) = $check->(@_);
786 14 100       1713 confess("At least one run must be passed!") unless scalar(@$runs);
787              
788             #Translate custom statuses
789 13         119 my $statuses = $self->getPossibleTestStatuses();
790 13         41 my %shash;
791              
792             #XXX so, they do these tricks with the status names, see...so map the counts to their relevant status ids.
793             @shash{
794             map {
795             ( $_->{'id'} < 6 )
796             ? $_->{'name'} . "_count"
797             : "custom_status"
798 117 100       535 . ( $_->{'id'} - 5 )
799             . "_count"
800             } @$statuses
801 13         48 } = map { $_->{'id'} } @$statuses;
  117         268  
802              
803 13         68 my @sname;
804              
805             #Create listing of keys/values
806             @$runs = map {
807 13         54 my $run = $_;
  282         469  
808 282         2044 @{ $run->{statuses} }{ grep { $_ =~ m/_count$/ } keys(%$run) } =
  7833         13699  
809 282         2140 grep { $_ =~ m/_count$/ } keys(%$run);
  7833         15458  
810 282         940 foreach my $status ( keys( %{ $run->{'statuses'} } ) ) {
  282         946  
811 3348 100       6031 next if !exists( $shash{$status} );
812             @sname = grep {
813 2511         3701 exists( $shash{$status} )
814 22599 50       59957 && $_->{'id'} == $shash{$status}
815             } @$statuses;
816             $run->{'statuses_clean'}->{ $sname[0]->{'label'} } =
817 2511         6231 $run->{$status};
818             }
819 282         756 $run;
820             } @$runs;
821              
822             return map {
823 13         51 {
824             'id' => $_->{'id'},
825             'name' => $_->{'name'},
826             'run_status' => $_->{'statuses_clean'},
827 282         1344 'config_ids' => $_->{'config_ids'}
828             }
829             } @$runs;
830              
831             }
832              
833             sub getRunResults {
834 1     1 1 3264 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
835 1         3545 my ( $self, $run_id, $filters ) = $check->(@_);
836              
837             my $initial_results =
838 1         126 $self->getRunResultsPaginated( $run_id, $self->{'global_limit'},
839             undef, $filters );
840 1 50 50     70 return $initial_results
841             unless ( reftype($initial_results) || 'undef' ) eq 'ARRAY';
842 1         3 my $results = [];
843 1         3 push( @$results, @$initial_results );
844 1         3 my $offset = 1;
845 1         6 while ( scalar(@$initial_results) == $self->{'global_limit'} ) {
846             $initial_results = $self->getRunResultsPaginated(
847             $run_id,
848             $self->{'global_limit'},
849 0         0 ( $self->{'global_limit'} * $offset ), $filters
850             );
851 0         0 push( @$results, @$initial_results );
852 0         0 $offset++;
853             }
854 1         5 return $results;
855             }
856              
857             sub getRunResultsPaginated {
858 1     1 1 6 state $check = compile( Object,
859             Int,
860             Optional [ Maybe [Int] ],
861             Optional [ Maybe [Int] ],
862             Optional [ Maybe [HashRef] ]
863             );
864 1         5098 my ( $self, $run_id, $limit, $offset, $filters ) = $check->(@_);
865              
866             confess( "Limit greater than " . $self->{'global_limit'} )
867 1 50       205 if $limit > $self->{'global_limit'};
868 1         4 my $apiurl = "index.php?/api/v2/get_results_for_run/$run_id";
869 1 50       5 $apiurl .= "&offset=$offset" if defined($offset);
870 1 50       4 $apiurl .= "&limit=$limit"
871             if $limit; #You have problems if you want 0 results
872 1         6 $apiurl .= _convert_filters_to_string($filters);
873 1         9 return $self->_doRequest($apiurl);
874             }
875              
876             sub getChildRuns {
877 2432     2432 1 8497 state $check = compile( Object, HashRef );
878 2432         17518 my ( $self, $plan ) = $check->(@_);
879              
880             return 0
881             unless defined( $plan->{'entries'} )
882 2431 100 50     26738 && ( reftype( $plan->{'entries'} ) || 'undef' ) eq 'ARRAY';
      66        
883 2429         3802 my $entries = $plan->{'entries'};
884 2429         3755 my $plans = [];
885 2429         3951 foreach my $entry (@$entries) {
886 2458         5021 push( @$plans, @{ $entry->{'runs'} } )
887             if defined( $entry->{'runs'} )
888 2458 50 50     9401 && ( ( reftype( $entry->{'runs'} ) || 'undef' ) eq 'ARRAY' );
      33        
889             }
890 2429         4594 return $plans;
891             }
892              
893             sub getChildRunByName {
894 42     42 1 8039 state $check = compile( Object, HashRef, Str,
895             Optional [ Maybe [ ArrayRef [Str] ] ],
896             Optional [ Maybe [Int] ]
897             );
898 42         57679 my ( $self, $plan, $name, $configurations, $testsuite_id ) = $check->(@_);
899              
900 40         3363 my $runs = $self->getChildRuns($plan);
901 40 100       113 @$runs = grep { $_->{suite_id} == $testsuite_id } @$runs if $testsuite_id;
  2         8  
902 40 100       118 return 0 if !$runs;
903              
904 39         89 my @pconfigs = ();
905              
906             #Figure out desired config IDs
907 39 100       101 if ( defined $configurations ) {
908 38         140 my $avail_configs = $self->getConfigurations( $plan->{'project_id'} );
909 38         85 my ($cname);
910 36         235 @pconfigs = map { $_->{'id'} } grep {
911 38         119 $cname = $_->{'name'};
  158         270  
912 158         277 grep { $_ eq $cname } @$configurations
  144         338  
913             } @$avail_configs; #Get a list of IDs from the names passed
914             }
915 39 50 66     336 confess("One or more configurations passed does not exist in your project!")
916             if defined($configurations)
917             && ( scalar(@pconfigs) != scalar(@$configurations) );
918              
919 39         82 my $found;
920 39         132 foreach my $run (@$runs) {
921 38 100       133 next if $run->{name} ne $name;
922 35 100       65 next if scalar(@pconfigs) != scalar( @{ $run->{'config_ids'} } );
  35         116  
923              
924             #Compare run config IDs against desired, invalidate run if all conditions not satisfied
925 34         66 $found = 0;
926 34         63 foreach my $cid ( @{ $run->{'config_ids'} } ) {
  34         82  
927 33 100       67 $found++ if grep { $_ == $cid } @pconfigs;
  33         157  
928             }
929              
930 34 100       74 return $run if $found == scalar( @{ $run->{'config_ids'} } );
  34         190  
931             }
932 7         114 return 0;
933             }
934              
935             sub createPlan {
936 264     264 1 28491 state $check = compile( Object, Int, Str,
937             Optional [ Maybe [Str] ],
938             Optional [ Maybe [Int] ],
939             Optional [ Maybe [ ArrayRef [HashRef] ] ]
940             );
941 264         50974 my ( $self, $project_id, $name, $desc, $milestone_id, $entries ) =
942             $check->(@_);
943              
944 262         9772 my $stuff = {
945             name => $name,
946             description => $desc,
947             milestone_id => $milestone_id,
948             entries => $entries
949             };
950              
951 262         930 return $self->_doRequest( "index.php?/api/v2/add_plan/$project_id",
952             'POST', $stuff );
953             }
954              
955             sub deletePlan {
956 4     4 1 7205 state $check = compile( Object, Int );
957 4         4299 my ( $self, $plan_id ) = $check->(@_);
958              
959 3         69 return $self->_doRequest( "index.php?/api/v2/delete_plan/$plan_id",
960             'POST' );
961             }
962              
963             sub getPlans {
964 105     105 1 7135 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
965 105         37678 my ( $self, $project_id, $filters ) = $check->(@_);
966              
967             my $initial_plans =
968 104         4595 $self->getPlansPaginated( $project_id, $self->{'global_limit'},
969             0, $filters );
970 104 100 100     64793 return $initial_plans
971             unless ( reftype($initial_plans) || 'undef' ) eq 'ARRAY';
972 100         447 my $plans = [];
973 100         963 push( @$plans, @$initial_plans );
974 100         268 my $offset = 1;
975 100         531 while ( scalar(@$initial_plans) == $self->{'global_limit'} ) {
976             $initial_plans = $self->getPlansPaginated(
977             $project_id,
978             $self->{'global_limit'},
979 19         127 ( $self->{'global_limit'} * $offset ), $filters
980             );
981 19         1906 push( @$plans, @$initial_plans );
982 19         101 $offset++;
983             }
984 100         622 return $plans;
985             }
986              
987             sub getPlansPaginated {
988 125     125 1 4974 state $check = compile( Object,
989             Int,
990             Optional [ Maybe [Int] ],
991             Optional [ Maybe [Int] ],
992             Optional [ Maybe [HashRef] ]
993             );
994 125         62561 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
995              
996             confess( "Limit greater than " . $self->{'global_limit'} )
997 124 50       7718 if $limit > $self->{'global_limit'};
998 124         659 my $apiurl = "index.php?/api/v2/get_plans/$project_id";
999 124 100       770 $apiurl .= "&offset=$offset" if defined($offset);
1000 124 100       656 $apiurl .= "&limit=$limit"
1001             if $limit; #You have problems if you want 0 results
1002 124         565 $apiurl .= _convert_filters_to_string($filters);
1003 124         489 return $self->_doRequest($apiurl);
1004             }
1005              
1006             sub getPlanByName {
1007 51     51 1 8412 state $check = compile( Object, Int, Str );
1008 51         18713 my ( $self, $project_id, $name ) = $check->(@_);
1009              
1010 49         1405 my $plans = $self->getPlans($project_id);
1011 49 100 100     571 return -500 if !$plans || ( reftype($plans) || 'undef' ) ne 'ARRAY';
      66        
1012 47         219 foreach my $plan (@$plans) {
1013 856 100       1628 if ( $plan->{'name'} eq $name ) {
1014 39         222 return $self->getPlanByID( $plan->{'id'} );
1015             }
1016             }
1017 8         1074 return 0;
1018             }
1019              
1020             sub getPlanByID {
1021 113     113 1 6995 state $check = compile( Object, Int );
1022 113         14077 my ( $self, $plan_id ) = $check->(@_);
1023              
1024 112         2189 return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
1025             }
1026              
1027             sub getPlanSummary {
1028 6     6 1 4335 state $check = compile( Object, Int );
1029 6         9381 my ( $self, $plan_id ) = $check->(@_);
1030              
1031 5         155 my $runs = $self->getPlanByID($plan_id);
1032 5         756 $runs = $self->getChildRuns($runs);
1033 5         81 @$runs = $self->getRunSummary( @{$runs} );
  5         45  
1034 5         23 my $total_sum = 0;
1035 5         30 my $ret = { plan => $plan_id };
1036              
1037             #Compile totals
1038 5         29 foreach my $summary (@$runs) {
1039 11         22 my @elems = keys( %{ $summary->{'run_status'} } );
  11         53  
1040 11         31 foreach my $key (@elems) {
1041 99 100       219 $ret->{'totals'}->{$key} = 0 if !defined $ret->{'totals'}->{$key};
1042 99         160 $ret->{'totals'}->{$key} += $summary->{'run_status'}->{$key};
1043 99         164 $total_sum += $summary->{'run_status'}->{$key};
1044             }
1045             }
1046              
1047             #Compile percentages
1048 5         13 foreach my $key ( keys( %{ $ret->{'totals'} } ) ) {
  5         35  
1049 45 50       84 next if grep { $_ eq $key } qw{plan configs percentages};
  135         275  
1050             $ret->{"percentages"}->{$key} =
1051 45         301 sprintf( "%.2f%%", ( $ret->{'totals'}->{$key} / $total_sum ) * 100 );
1052             }
1053              
1054 5         51 return $ret;
1055             }
1056              
1057             #If you pass an array of case ids, it implies include_all is false
1058             sub createRunInPlan {
1059 14     14 1 11671 state $check = compile( Object, Int, Int, Str,
1060             Optional [ Maybe [Int] ],
1061             Optional [ Maybe [ ArrayRef [Int] ] ],
1062             Optional [ Maybe [ ArrayRef [Int] ] ]
1063             );
1064 14         33663 my ( $self, $plan_id, $suite_id, $name, $assignedto_id, $config_ids,
1065             $case_ids )
1066             = $check->(@_);
1067              
1068 11 100       1831 my $runs = [
1069             {
1070             config_ids => $config_ids,
1071             include_all => defined($case_ids) ? 0 : 1,
1072             case_ids => $case_ids
1073             }
1074             ];
1075              
1076 11 100       113 my $stuff = {
1077             suite_id => $suite_id,
1078             name => $name,
1079             assignedto_id => $assignedto_id,
1080             include_all => defined($case_ids) ? 0 : 1,
1081             case_ids => $case_ids,
1082             config_ids => $config_ids,
1083             runs => $runs
1084             };
1085 11         75 return $self->_doRequest( "index.php?/api/v2/add_plan_entry/$plan_id",
1086             'POST', $stuff );
1087             }
1088              
1089             sub closePlan {
1090 6     6 1 5759 state $check = compile( Object, Int );
1091 6         6628 my ( $self, $plan_id ) = $check->(@_);
1092              
1093 6         144 return $self->_doRequest( "index.php?/api/v2/close_plan/$plan_id", 'POST' );
1094             }
1095              
1096             sub createMilestone {
1097 5     5 1 9209 state $check = compile( Object, Int, Str,
1098             Optional [ Maybe [Str] ],
1099             Optional [ Maybe [Int] ]
1100             );
1101 5         14773 my ( $self, $project_id, $name, $desc, $due_on ) = $check->(@_);
1102              
1103 3         445 my $stuff = {
1104             name => $name,
1105             description => $desc,
1106             due_on => $due_on # unix timestamp
1107             };
1108              
1109 3         19 return $self->_doRequest( "index.php?/api/v2/add_milestone/$project_id",
1110             'POST', $stuff );
1111             }
1112              
1113             sub deleteMilestone {
1114 4     4 1 5643 state $check = compile( Object, Int );
1115 4         3844 my ( $self, $milestone_id ) = $check->(@_);
1116              
1117 3         69 return $self->_doRequest(
1118             "index.php?/api/v2/delete_milestone/$milestone_id", 'POST' );
1119             }
1120              
1121             sub getMilestones {
1122 7     7 1 6589 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
1123 7         8710 my ( $self, $project_id, $filters ) = $check->(@_);
1124              
1125 6         307 return $self->_doRequest( "index.php?/api/v2/get_milestones/$project_id"
1126             . _convert_filters_to_string($filters) );
1127             }
1128              
1129             sub getMilestoneByName {
1130 5     5 1 8382 state $check = compile( Object, Int, Str );
1131 5         4742 my ( $self, $project_id, $name ) = $check->(@_);
1132              
1133 3         83 my $milestones = $self->getMilestones($project_id);
1134 3 100 100     125 return -500
      66        
1135             if !$milestones || ( reftype($milestones) || 'undef' ) ne 'ARRAY';
1136 1         4 foreach my $milestone (@$milestones) {
1137 1 50       9 return $milestone if $milestone->{'name'} eq $name;
1138             }
1139 0         0 return 0;
1140             }
1141              
1142             sub getMilestoneByID {
1143 27     27 1 6199 state $check = compile( Object, Int );
1144 27         9085 my ( $self, $milestone_id ) = $check->(@_);
1145              
1146 26         517 return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
1147             }
1148              
1149             sub getTests {
1150 91     91 1 9946 state $check = compile( Object, Int,
1151             Optional [ Maybe [ ArrayRef [Int] ] ],
1152             Optional [ Maybe [ ArrayRef [Int] ] ]
1153             );
1154 91         66666 my ( $self, $run_id, $status_ids, $assignedto_ids ) = $check->(@_);
1155              
1156 90         4305 my $query_string = '';
1157 90 50 100     447 $query_string = '&status_id=' . join( ',', @$status_ids )
1158             if defined($status_ids) && scalar(@$status_ids);
1159 90         910 my $results =
1160             $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
1161             @$results = grep {
1162 90 50 100     7519 my $aid = $_->{'assignedto_id'};
  22         38  
1163 22 100       31 grep { defined($aid) && $aid == $_ } @$assignedto_ids
  22         93  
1164             } @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
1165              
1166             #Cache stuff for getTestByName
1167 90   100     461 $self->{tests_cache} //= {};
1168 90         523 $self->{tests_cache}->{$run_id} = $results;
1169              
1170 90         9277 return clone($results);
1171             }
1172              
1173             sub getTestByName {
1174 51     51 1 10466 state $check = compile( Object, Int, Str );
1175 51         13994 my ( $self, $run_id, $name ) = $check->(@_);
1176              
1177 49   100     2562 $self->{tests_cache} //= {};
1178 49         176 my $tests = $self->{tests_cache}->{$run_id};
1179              
1180 49 100       453 $tests = $self->getTests($run_id) if !$tests;
1181 49 100 100     840 return -500 if !$tests || ( reftype($tests) || 'undef' ) ne 'ARRAY';
      66        
1182 47         217 foreach my $test (@$tests) {
1183 137 100       689 return $test if $test->{'title'} eq $name;
1184             }
1185 1         10 return 0;
1186             }
1187              
1188             sub getTestByID {
1189 4     4 1 6418 state $check = compile( Object, Int );
1190 4         4324 my ( $self, $test_id ) = $check->(@_);
1191              
1192 3         68 return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
1193             }
1194              
1195             sub getTestResultFields {
1196 20     20 1 4193 state $check = compile(Object);
1197 20         5451 my ($self) = $check->(@_);
1198              
1199 20 100       319 return $self->{'tr_fields'} if defined( $self->{'tr_fields'} ); #cache
1200 17         238 $self->{'tr_fields'} =
1201             $self->_doRequest('index.php?/api/v2/get_result_fields');
1202 17         1810 return $self->{'tr_fields'};
1203             }
1204              
1205             sub getTestResultFieldByName {
1206 18     18 1 7436 state $check = compile( Object, Str, Optional [ Maybe [Int] ] );
1207 18         17652 my ( $self, $system_name, $project_id ) = $check->(@_);
1208              
1209             my @candidates =
1210 17         1118 grep { $_->{'name'} eq $system_name } @{ $self->getTestResultFields() };
  17         149  
  17         147  
1211 17 100       452 return 0 if !scalar(@candidates); #No such name
1212 15 50       111 return -1 if ref( $candidates[0] ) ne 'HASH';
1213             return -2
1214             if ref( $candidates[0]->{'configs'} ) ne 'ARRAY'
1215 15 50 33     150 && !scalar( @{ $candidates[0]->{'configs'} } ); #bogofilter
  0         0  
1216              
1217             #Give it to the user
1218 15         54 my $ret = $candidates[0]; #copy/save for later
1219 15 100       74 return $ret if !defined($project_id);
1220              
1221             #Filter by project ID
1222 14         35 foreach my $config ( @{ $candidates[0]->{'configs'} } ) {
  14         61  
1223             return $ret
1224 62         220 if ( grep { $_ == $project_id }
1225 34 100       61 @{ $config->{'context'}->{'project_ids'} } );
  34         95  
1226             }
1227              
1228 1         4 return -3;
1229             }
1230              
1231             sub getPossibleTestStatuses {
1232 111     111 1 4610 state $check = compile(Object);
1233 111         16728 my ($self) = $check->(@_);
1234 111 100       1611 return $self->{'status_cache'} if $self->{'status_cache'};
1235              
1236 81         780 $self->{'status_cache'} =
1237             $self->_doRequest('index.php?/api/v2/get_statuses');
1238 81         23885 return clone $self->{'status_cache'};
1239             }
1240              
1241             sub statusNamesToIds {
1242 21     21 1 11563 my ( $self, @names ) = @_;
1243 21         99 return _X_in_my_Y( $self, $self->getPossibleTestStatuses(), 'id', @names );
1244             }
1245              
1246             sub statusNamesToLabels {
1247 5     5 1 689 my ( $self, @names ) = @_;
1248 5         21 return _X_in_my_Y( $self, $self->getPossibleTestStatuses(), 'label',
1249             @names );
1250             }
1251              
1252             # Reduce code duplication with internal methods?
1253             # It's more likely than you think
1254             # Free PC check @ cpan.org
1255             sub _X_in_my_Y {
1256 100     100   276 state $check = compile( Object, ArrayRef, Str, slurpy ArrayRef [Str] );
1257 100         96220 my ( $self, $search_arr, $key, $names ) = $check->(@_);
1258              
1259 97         5900 my @ret;
1260 97         409 foreach my $name (@$names) {
1261 127         270 foreach my $member (@$search_arr) {
1262 527 100       1075 if ( $member->{'name'} eq $name ) {
1263 123         248 push @ret, $member->{$key};
1264 123         269 last;
1265             }
1266             }
1267             }
1268 97 100       3571 confess("One or more names provided does not exist in TestRail.")
1269             unless scalar(@$names) == scalar(@ret);
1270 93         872 return @ret;
1271             }
1272              
1273             sub createTestResults {
1274 56     56 1 8717 state $check = compile( Object, Int, Int,
1275             Optional [ Maybe [Str] ],
1276             Optional [ Maybe [HashRef] ],
1277             Optional [ Maybe [HashRef] ]
1278             );
1279 56         43155 my ( $self, $test_id, $status_id, $comment, $opts, $custom_fields ) =
1280             $check->(@_);
1281              
1282 54         4000 my $stuff = {
1283             status_id => $status_id,
1284             comment => $comment
1285             };
1286              
1287             #Handle options
1288 54 100 66     684 if ( defined($opts) && reftype($opts) eq 'HASH' ) {
1289             $stuff->{'version'} =
1290 44 100       284 defined( $opts->{'version'} ) ? $opts->{'version'} : undef;
1291             $stuff->{'elapsed'} =
1292 44 100       240 defined( $opts->{'elapsed'} ) ? $opts->{'elapsed'} : undef;
1293             $stuff->{'defects'} =
1294             defined( $opts->{'defects'} )
1295 44 50       314 ? join( ',', @{ $opts->{'defects'} } )
  0         0  
1296             : undef;
1297             $stuff->{'assignedto_id'} =
1298             defined( $opts->{'assignedto_id'} )
1299 44 50       188 ? $opts->{'assignedto_id'}
1300             : undef;
1301             }
1302              
1303             #Handle custom fields
1304 54 100 66     278 if ( defined($custom_fields) && reftype($custom_fields) eq 'HASH' ) {
1305 7         43 foreach my $field ( keys(%$custom_fields) ) {
1306 7         42 $stuff->{"custom_$field"} = $custom_fields->{$field};
1307             }
1308             }
1309              
1310 54         343 return $self->_doRequest( "index.php?/api/v2/add_result/$test_id",
1311             'POST', $stuff );
1312             }
1313              
1314             sub bulkAddResults {
1315 6     6 1 7817 state $check = compile( Object, Int, ArrayRef [HashRef] );
1316 6         9477 my ( $self, $run_id, $results ) = $check->(@_);
1317              
1318 4         318 return $self->_doRequest( "index.php?/api/v2/add_results/$run_id",
1319             'POST', { 'results' => $results } );
1320             }
1321              
1322             sub bulkAddResultsByCase {
1323 0     0 1 0 state $check = compile( Object, Int, ArrayRef [HashRef] );
1324 0         0 my ( $self, $run_id, $results ) = $check->(@_);
1325              
1326 0         0 return $self->_doRequest( "index.php?/api/v2/add_results_for_cases/$run_id",
1327             'POST', { 'results' => $results } );
1328             }
1329              
1330             sub getTestResults {
1331 12     12 1 9202 state $check = compile( Object,
1332             Int,
1333             Optional [ Maybe [Int] ],
1334             Optional [ Maybe [Int] ],
1335             Optional [ Maybe [HashRef] ]
1336             );
1337 12         27692 my ( $self, $test_id, $limit, $offset, $filters ) = $check->(@_);
1338              
1339 11         975 my $url = "index.php?/api/v2/get_results/$test_id";
1340 11 100       48 $url .= "&limit=$limit" if $limit;
1341 11 50       43 $url .= "&offset=$offset" if defined($offset);
1342 11         43 $url .= _convert_filters_to_string($filters);
1343 11         45 return $self->_doRequest($url);
1344             }
1345              
1346             sub getResultsForCase {
1347 0     0 1 0 state $check = compile( Object, Int,
1348             Int, Optional [ Maybe [Int] ],
1349             Optional [ Maybe [Int] ], Optional [ Maybe [HashRef] ]
1350             );
1351 0         0 my ( $self, $run_id, $case_id, $limit, $offset, $filters ) = $check->(@_);
1352              
1353 0         0 my $url = "index.php?/api/v2/get_results_for_case/$run_id/$case_id";
1354 0 0       0 $url .= "&limit=$limit" if $limit;
1355 0 0       0 $url .= "&offset=$offset" if defined($offset);
1356 0         0 $url .= _convert_filters_to_string($filters);
1357 0         0 return $self->_doRequest($url);
1358             }
1359              
1360             sub getConfigurationGroups {
1361 199     199 1 5391 state $check = compile( Object, Int );
1362 199         13613 my ( $self, $project_id ) = $check->(@_);
1363              
1364 198         4243 my $url = "index.php?/api/v2/get_configs/$project_id";
1365 198         686 return $self->_doRequest($url);
1366             }
1367              
1368             sub getConfigurationGroupByName {
1369 4     4 1 861 state $check = compile( Object, Int, Str );
1370 4         4461 my ( $self, $project_id, $name ) = $check->(@_);
1371              
1372 4         126 my $cgroups = $self->getConfigurationGroups($project_id);
1373 4 50       228 return 0 if ref($cgroups) ne 'ARRAY';
1374 4         15 @$cgroups = grep { $_->{'name'} eq $name } @$cgroups;
  12         49  
1375 4 100       25 return 0 unless scalar(@$cgroups);
1376 1         5 return $cgroups->[0];
1377             }
1378              
1379             sub addConfigurationGroup {
1380 4     4 1 864 state $check = compile( Object, Int, Str );
1381 4         3197 my ( $self, $project_id, $name ) = $check->(@_);
1382              
1383 4         109 my $url = "index.php?/api/v2/add_config_group/$project_id";
1384 4         24 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1385             }
1386              
1387             sub editConfigurationGroup {
1388 1     1 1 824 state $check = compile( Object, Int, Str );
1389 1         1528 my ( $self, $config_group_id, $name ) = $check->(@_);
1390              
1391 1         30 my $url = "index.php?/api/v2/update_config_group/$config_group_id";
1392 1         7 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1393             }
1394              
1395             sub deleteConfigurationGroup {
1396 1     1 1 7 state $check = compile( Object, Int );
1397 1         1316 my ( $self, $config_group_id ) = $check->(@_);
1398              
1399 1         20 my $url = "index.php?/api/v2/delete_config_group/$config_group_id";
1400 1         4 return $self->_doRequest( $url, 'POST' );
1401             }
1402              
1403             sub getConfigurations {
1404 195     195 1 7080 state $check = compile( Object, Int );
1405 195         14321 my ( $self, $project_id ) = $check->(@_);
1406              
1407 193         4821 my $cgroups = $self->getConfigurationGroups($project_id);
1408 193         10470 my $configs = [];
1409 193 100 100     1750 return $cgroups unless ( reftype($cgroups) || 'undef' ) eq 'ARRAY';
1410 190         1028 foreach my $cfg (@$cgroups) {
1411 378         695 push( @$configs, @{ $cfg->{'configs'} } );
  378         1036  
1412             }
1413 190         1264 return $configs;
1414             }
1415              
1416             sub addConfiguration {
1417 4     4 1 868 state $check = compile( Object, Int, Str );
1418 4         3176 my ( $self, $configuration_group_id, $name ) = $check->(@_);
1419              
1420 4         117 my $url = "index.php?/api/v2/add_config/$configuration_group_id";
1421 4         27 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1422             }
1423              
1424             sub editConfiguration {
1425 1     1 1 858 state $check = compile( Object, Int, Str );
1426 1         1571 my ( $self, $config_id, $name ) = $check->(@_);
1427              
1428 1         26 my $url = "index.php?/api/v2/update_config/$config_id";
1429 1         5 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1430             }
1431              
1432             sub deleteConfiguration {
1433 1     1 1 1126 state $check = compile( Object, Int );
1434 1         1480 my ( $self, $config_id ) = $check->(@_);
1435              
1436 1         20 my $url = "index.php?/api/v2/delete_config/$config_id";
1437 1         6 return $self->_doRequest( $url, 'POST' );
1438             }
1439              
1440             sub translateConfigNamesToIds {
1441 61     61 1 6358 my ( $self, $project_id, @names ) = @_;
1442 61 50       201 my $configs = $self->getConfigurations($project_id)
1443             or confess("Could not determine configurations in provided project.");
1444 60         418 return _X_in_my_Y( $self, $configs, 'id', @names );
1445             }
1446              
1447             sub getReports {
1448 1     1 1 357 state $check = compile( Object, Int );
1449 1         1357 my ( $self, $project_id ) = $check->(@_);
1450 1         19 my $url = "index.php?/api/v2/get_reports/$project_id";
1451 1         6 return $self->_doRequest( $url, 'GET' );
1452             }
1453              
1454             sub runReport {
1455 1     1 1 1030 state $check = compile( Object, Int );
1456 1         1167 my ( $self, $report_id ) = $check->(@_);
1457 1         20 my $url = "index.php?/api/v2/run_report/$report_id";
1458 1         6 return $self->_doRequest( $url, 'GET' );
1459             }
1460              
1461             #Convenience method for building stepResults
1462             sub buildStepResults {
1463 16     16 1 118 state $check = compile( Str, Str, Str, Int );
1464 16         6285 my ( $content, $expected, $actual, $status_id ) = $check->(@_);
1465              
1466             return {
1467 16         722 content => $content,
1468             expected => $expected,
1469             actual => $actual,
1470             status_id => $status_id
1471             };
1472             }
1473              
1474             # Convenience method for building filter string from filters Hashref
1475             sub _convert_filters_to_string {
1476 404     404   1119 state $check = compile( Maybe [HashRef] );
1477 404         28622 my ($filters) = $check->(@_);
1478              
1479 404   100     7549 $filters //= {};
1480              
1481 404         1365 my $filter_string = '';
1482 404         2189 foreach my $filter ( keys(%$filters) ) {
1483 29 50       94 if ( ref $filters->{$filter} eq 'ARRAY' ) {
1484             $filter_string .=
1485 0         0 "&$filter=" . join( ',', @{ $filters->{$filter} } );
  0         0  
1486             }
1487             else {
1488             $filter_string .= "&$filter=" . $filters->{$filter}
1489 29 100       140 if defined( $filters->{$filter} );
1490             }
1491             }
1492 404         1747 return $filter_string;
1493             }
1494              
1495             1;
1496              
1497             __END__