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.050';
6              
7 21     21   600223 use 5.010;
  21         157  
8              
9 21     21   133 use strict;
  21         43  
  21         490  
10 21     21   108 use warnings;
  21         42  
  21         762  
11              
12 21     21   162 use Carp qw{cluck confess};
  21         46  
  21         1623  
13 21     21   161 use Scalar::Util qw{reftype looks_like_number};
  21         51  
  21         1564  
14 21     21   6472 use Clone 'clone';
  21         37344  
  21         1301  
15 21     21   4336 use Try::Tiny;
  21         17607  
  21         1445  
16              
17             use Types::Standard
18 21     21   12463 qw( slurpy ClassName Object Str Int Bool HashRef ArrayRef Maybe Optional);
  21         1667576  
  21         298  
19 21     21   53557 use Type::Params qw( compile );
  21         252036  
  21         247  
20              
21 21     21   12852 use JSON::MaybeXS 1.001000 ();
  21         80632  
  21         592  
22 21     21   6934 use HTTP::Request;
  21         254196  
  21         787  
23 21     21   11548 use LWP::UserAgent;
  21         348745  
  21         952  
24 21     21   13522 use HTTP::CookieJar::LWP;
  21         620658  
  21         1033  
25 21     21   12724 use Data::Validate::URI qw{is_uri};
  21         1061784  
  21         1690  
26 21     21   198 use List::Util 1.33;
  21         662  
  21         1242  
27 21     21   10002 use Encode ();
  21         175962  
  21         253622  
28              
29             sub new {
30 106     106 1 14044 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         202242 $class, $apiurl, $user,
42             $pass, $encoding, $debug,
43             $do_post_redirect, $max_tries, $userfetch_opts
44             ) = $check->(@_);
45              
46 106 100       23825 die("Invalid URI passed to constructor") if !is_uri($apiurl);
47 105   50     33810 $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     3561 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       105170 if ( $self->{do_post_redirect} ) {
74 56         170 push @{ $self->{'browser'}->requests_redirectable }, 'POST';
  56         494  
75             }
76              
77             #Check chara encoding
78             $self->{'encoding-nonaliased'} =
79 105         2594 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       11576 ) 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       589 unless grep { $_ eq $self->{'encoding-nonaliased'} }
  13020         138889  
90             ( Encode->encodings(":all") );
91              
92             #Create default request to pass on to LWP::UserAgent
93 105         2049 $self->{'default_request'} = HTTP::Request->new();
94 105         8303 $self->{'default_request'}->authorization_basic( $user, $pass );
95              
96 105         35606 bless( $self, $class );
97 105 100       528 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       6 if ( !$userfetch_opts->{skip_usercache} ) {
103 1         6 my $res = $self->getUsers( $userfetch_opts->{project_id} );
104 1 50       11 confess "Error: network unreachable" if !defined($res);
105 1 50 50     13 if ( ( reftype($res) || 'undef' ) ne 'ARRAY' ) {
106 1 50       8 confess "Unexpected return from _doRequest: $res"
107             if !looks_like_number($res);
108 1 50       355 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 5170 state $check = compile(Object);
130 1830         20838 my ($self) = $check->(@_);
131 1830         30040 return $self->{'apiurl'};
132             }
133              
134             sub debug {
135 1828     1828 1 3801 state $check = compile(Object);
136 1828         22873 my ($self) = $check->(@_);
137 1828         20342 return $self->{'debug'};
138             }
139              
140             #Convenient JSON-HTTP fetcher
141             sub _doRequest {
142 1722     1722   23042 state $check = compile( Object, Str,
143             Optional [ Maybe [Str] ],
144             Optional [ Maybe [HashRef] ]
145             );
146 1722         91598 my ( $self, $path, $method, $data ) = $check->(@_);
147              
148 1722         41403 $self->{num_tries}++;
149              
150 1722         34504 my $req = clone $self->{'default_request'};
151 1722   100     11028 $method //= 'GET';
152              
153 1722         9266 $req->method($method);
154 1722         25817 $req->url( $self->apiurl . '/' . $path );
155              
156 1722 100       337562 warn "$method " . $self->apiurl . "/$path" if $self->debug;
157              
158 1722         12439 my $coder = JSON::MaybeXS->new;
159              
160             #Data sent is JSON, and encoded per user preference
161             my $content =
162             $data
163 1722 100       38593 ? Encode::encode( $self->{'encoding-nonaliased'}, $coder->encode($data) )
164             : '';
165              
166 1722         22228 $req->content($content);
167             $req->header(
168 1722         45672 "Content-Type" => "application/json; charset=" . $self->{'encoding'} );
169              
170 1722         113117 my $response = eval { $self->{'browser'}->request($req) };
  1722         9231  
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       3759316 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       5190 return $response if !defined($response); #worst case
195              
196 1722 100       4814 if ( $response->code == 403 ) {
197 1         15 confess "ERROR 403: Access Denied: " . $response->content;
198             }
199 1721 100       19794 if ( $response->code == 401 ) {
200 1         14 confess "ERROR 401: Authentication failed: " . $response->content;
201             }
202              
203 1720 100       18312 if ( $response->code != 200 ) {
204              
205             #LWP threw an ex, probably a timeout
206 117 100       1383 if ( $self->{num_tries} >= $self->{max_tries} ) {
207 115         244 $self->{num_tries} = 0;
208 115         315 cluck "ERROR: Arguments Bad? (got code "
209             . $response->code . "): "
210             . $response->content;
211 115         45441 return -int( $response->code );
212             }
213 2         11 cluck "WARNING: TestRail API request failed (got code "
214             . $response->code
215             . "), re-trying request...\n";
216 2 100       5001654 sleep $self->{retry_delay} if $self->{retry_delay};
217 2         67 goto &_doRequest;
218              
219             }
220 1603         17286 $self->{num_tries} = 0;
221              
222             try {
223 1603     1603   75986 return $coder->decode( $response->content );
224             }
225             catch {
226 9 50 33 9   325 if ( $response->code == 200 && !$response->content ) {
227 9         350 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         16156 }
241              
242             sub getUsers {
243 78     78 1 7304 state $check = compile( Object, Optional [ Maybe [Str] ] );
244 78         26665 my ( $self, $project_id ) = $check->(@_);
245              
246             # Return shallow clone of user_cache if set.
247 6         21 return [ @{ $self->{'user_cache'} } ]
248             if ref $self->{'user_cache'} eq 'ARRAY'
249 78 100 50     3042 && scalar( @{ $self->{'user_cache'} } );
  78         406  
250 72 100       343 my $maybe_project = $project_id ? "/$project_id" : '';
251 72         352 my $res = $self->_doRequest("index.php?/api/v2/get_users$maybe_project");
252 70 100 100     3633 return -500 if !$res || ( reftype($res) || 'undef' ) ne 'ARRAY';
      66        
253 60         276 $self->{'user_cache'} = $res;
254 60         1567 return clone($res);
255             }
256              
257             sub getUserByID {
258 4     4 1 8199 state $check = compile( Object, Int );
259 4         4466 my ( $self, $user ) = $check->(@_);
260              
261 3         66 my $users = $self->getUsers();
262 3 100       29 return $users if ref $users ne 'ARRAY';
263 1         4 foreach my $usr (@$users) {
264 1 50       10 return $usr if $usr->{'id'} == $user;
265             }
266 0         0 return 0;
267             }
268              
269             sub getUserByName {
270 4     4 1 7360 state $check = compile( Object, Str );
271 4         4316 my ( $self, $user ) = $check->(@_);
272              
273 3         107 my $users = $self->getUsers();
274 3 100       24 return $users if ref $users ne 'ARRAY';
275 1         4 foreach my $usr (@$users) {
276 1 50       10 return $usr if $usr->{'name'} eq $user;
277             }
278 0         0 return 0;
279             }
280              
281             sub getUserByEmail {
282 4     4 1 7465 state $check = compile( Object, Str );
283 4         4691 my ( $self, $email ) = $check->(@_);
284              
285 3         66 my $users = $self->getUsers();
286 3 100       24 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 12258 state $check = compile( Object, slurpy ArrayRef [Str] );
295 7         27977 my ( $self, $names ) = $check->(@_);
296              
297 7 100       924 confess("At least one user name must be provided") if !scalar(@$names);
298 12         27 my @ret = grep { defined $_ } map {
299 12         22 my $user = $_;
300 12         22 my @list = grep { $user->{'name'} eq $_ } @$names;
  18         50  
301 12 100       48 scalar(@list) ? $user->{'id'} : undef
302 6         14 } @{ $self->getUsers() };
  6         23  
303 6 100       306 confess("One or more user names provided does not exist in TestRail.")
304             unless scalar(@$names) == scalar(@ret);
305 5         22 return @ret;
306             }
307              
308             sub createProject {
309 4     4 1 8926 state $check = compile( Object, Str,
310             Optional [ Maybe [Str] ],
311             Optional [ Maybe [Bool] ]
312             );
313 4         14865 my ( $self, $name, $desc, $announce ) = $check->(@_);
314              
315 3   100     423 $desc //= 'res ipsa loquiter';
316 3   50     32 $announce //= 0;
317              
318 3         14 my $input = {
319             name => $name,
320             announcement => $desc,
321             show_announcement => $announce
322             };
323              
324 3         22 return $self->_doRequest( 'index.php?/api/v2/add_project', 'POST', $input );
325             }
326              
327             sub deleteProject {
328 4     4 1 6991 state $check = compile( Object, Int );
329 4         4491 my ( $self, $proj ) = $check->(@_);
330              
331 3         71 return $self->_doRequest( 'index.php?/api/v2/delete_project/' . $proj,
332             'POST' );
333             }
334              
335             sub getProjects {
336 106     106 1 5195 state $check = compile( Object, Optional [ Maybe [HashRef] ] );
337 106         46489 my ( $self, $filters ) = $check->(@_);
338              
339 106         3788 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     7355 return -500 if !$result || ( reftype($result) || 'undef' ) ne 'ARRAY';
      66        
344 100         395 $self->{'testtree'} = $result;
345              
346             #Note that it's a project for future reference by recursive tree search
347 100 50 50     1203 return -500 if !$result || ( reftype($result) || 'undef' ) ne 'ARRAY';
      33        
348 100         262 foreach my $pj ( @{$result} ) {
  100         489  
349 300         706 $pj->{'type'} = 'project';
350             }
351              
352 100         349 return $result;
353             }
354              
355             sub getProjectByName {
356 121     121 1 7516 state $check = compile( Object, Str );
357 121         19795 my ( $self, $project ) = $check->(@_);
358              
359             #See if we already have the project list...
360 120         2267 my $projects = $self->{'testtree'};
361 120 50 50     1521 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      33        
362 120 100       1107 $projects = $self->getProjects() unless scalar(@$projects);
363              
364             #Search project list for project
365 120 100 100     1063 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
366 118         348 for my $candidate (@$projects) {
367 232 100       983 return $candidate if ( $candidate->{'name'} eq $project );
368             }
369              
370 1         3 return 0;
371             }
372              
373             sub getProjectByID {
374 8     8 1 7005 state $check = compile( Object, Int );
375 8         7471 my ( $self, $project ) = $check->(@_);
376              
377             #See if we already have the project list...
378 7         170 my $projects = $self->{'testtree'};
379 7 100       65 $projects = $self->getProjects() unless scalar(@$projects);
380              
381             #Search project list for project
382 7 100 100     111 return -500 if !$projects || ( reftype($projects) || 'undef' ) ne 'ARRAY';
      66        
383 5         20 for my $candidate (@$projects) {
384 7 100       55 return $candidate if ( $candidate->{'id'} eq $project );
385             }
386              
387 0         0 return 0;
388             }
389              
390             sub createTestSuite {
391 5     5 1 10033 state $check = compile( Object, Int, Str, Optional [ Maybe [Str] ] );
392 5         11227 my ( $self, $project_id, $name, $details ) = $check->(@_);
393              
394 3   100     303 $details //= 'res ipsa loquiter';
395 3         14 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 6942 state $check = compile( Object, Int );
406 4         4445 my ( $self, $suite_id ) = $check->(@_);
407              
408 3         71 return $self->_doRequest( 'index.php?/api/v2/delete_suite/' . $suite_id,
409             'POST' );
410             }
411              
412             sub getTestSuites {
413 15     15 1 8088 state $check = compile( Object, Int );
414 15         10907 my ( $self, $proj ) = $check->(@_);
415              
416 14         344 return $self->_doRequest( 'index.php?/api/v2/get_suites/' . $proj );
417             }
418              
419             sub getTestSuiteByName {
420 13     13 1 9270 state $check = compile( Object, Int, Str );
421 13         17150 my ( $self, $project_id, $testsuite_name ) = $check->(@_);
422              
423             #TODO cache
424 11         318 my $suites = $self->getTestSuites($project_id);
425 11 100 100     934 return -500
      66        
426             if !$suites
427             || ( reftype($suites) || 'undef' ) ne
428             'ARRAY'; #No suites for project, or no project
429 9         32 foreach my $suite (@$suites) {
430 9 50       68 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 7295 state $check = compile( Object, Int );
437 4         4408 my ( $self, $testsuite_id ) = $check->(@_);
438              
439 3         70 return $self->_doRequest( 'index.php?/api/v2/get_suite/' . $testsuite_id );
440             }
441              
442             sub createSection {
443 6     6 1 12626 state $check = compile( Object, Int, Int, Str, Optional [ Maybe [Int] ] );
444 6         11975 my ( $self, $project_id, $suite_id, $name, $parent_id ) = $check->(@_);
445              
446 3         318 my $input = {
447             name => $name,
448             suite_id => $suite_id
449             };
450 3 50       16 $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 6965 state $check = compile( Object, Int );
458 4         4488 my ( $self, $section_id ) = $check->(@_);
459              
460 3         70 return $self->_doRequest( 'index.php?/api/v2/delete_section/' . $section_id,
461             'POST' );
462             }
463              
464             sub getSections {
465 34     34 1 9910 state $check = compile( Object, Int, Int );
466 34         14186 my ( $self, $project_id, $suite_id ) = $check->(@_);
467              
468             #Cache sections to reduce requests in tight loops
469 30 100       823 return $self->{'sections'}->{$suite_id} if $self->{'sections'}->{$suite_id};
470 14         97 $self->{'sections'}->{$suite_id} = $self->_doRequest(
471             "index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
472              
473 14         947 return $self->{'sections'}->{$suite_id};
474             }
475              
476             sub getSectionByID {
477 4     4 1 7445 state $check = compile( Object, Int );
478 4         4343 my ( $self, $section_id ) = $check->(@_);
479              
480 3         75 return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
481             }
482              
483             sub getSectionByName {
484 7     7 1 11485 state $check = compile( Object, Int, Int, Str );
485 7         8183 my ( $self, $project_id, $suite_id, $section_name ) = $check->(@_);
486              
487 4         126 my $sections = $self->getSections( $project_id, $suite_id );
488 4 100 100     57 return -500 if !$sections || ( reftype($sections) || 'undef' ) ne 'ARRAY';
      66        
489 2         8 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 5796 state $check = compile( Object, Int, HashRef );
497 11         6390 my ( $self, $project_id, $section ) = $check->(@_);
498              
499 11         287 my $sections_orig = $self->getSections( $project_id, $section->{suite_id} );
500 11 100 100     175 return []
      66        
501             if !$sections_orig || ( reftype($sections_orig) || 'undef' ) ne 'ARRAY';
502             my @sections =
503 10 100       45 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $section->{'id'} : 0 }
  50         172  
504             @$sections_orig;
505 10         37 foreach my $sec (@sections) {
506             push( @sections,
507 9 100       20 grep { $_->{'parent_id'} ? $_->{'parent_id'} == $sec->{'id'} : 0 }
  72         147  
508             @$sections_orig );
509             }
510 10         46 return \@sections;
511             }
512              
513             sub sectionNamesToIds {
514 14     14 1 9420 my ( $self, $project_id, $suite_id, @names ) = @_;
515 14 50       118 my $sections = $self->getSections( $project_id, $suite_id )
516             or confess("Could not find sections in provided project/suite.");
517 12         138 return _X_in_my_Y( $self, $sections, 'id', @names );
518             }
519              
520             sub getCaseTypes {
521 16     16 1 4420 state $check = compile(Object);
522 16         5040 my ($self) = $check->(@_);
523 16 100       521 return clone( $self->{'type_cache'} ) if defined( $self->{'type_cache'} );
524              
525 7         51 my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
526 7 100 100     325 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
527 3         13 $self->{'type_cache'} = $types;
528              
529 3         186 return clone $types;
530             }
531              
532             sub getCaseTypeByName {
533 13     13 1 10234 state $check = compile( Object, Str );
534 13         7091 my ( $self, $name ) = $check->(@_);
535              
536 12         244 my $types = $self->getCaseTypes();
537 12 100 100     126 return -500 if !$types || ( reftype($types) || 'undef' ) ne 'ARRAY';
      66        
538 10         28 foreach my $type (@$types) {
539 16 100       108 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 6 my ( $self, @names ) = @_;
546 1         3 return _X_in_my_Y( $self, $self->getCaseTypes(), 'id', @names );
547             }
548              
549             sub createCase {
550 5     5 1 9277 state $check = compile( Object, Int, Str,
551             Optional [ Maybe [Int] ],
552             Optional [ Maybe [HashRef] ],
553             Optional [ Maybe [HashRef] ]
554             );
555 5         17642 my ( $self, $section_id, $title, $type_id, $opts, $extras ) = $check->(@_);
556              
557 3         428 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     20 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     14 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         17 return $self->_doRequest( "index.php?/api/v2/add_case/$section_id",
582             'POST', $stuff );
583             }
584              
585             sub updateCase {
586 1     1 1 788 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
587 1         3010 my ( $self, $case_id, $options ) = $check->(@_);
588              
589 1         121 return $self->_doRequest( "index.php?/api/v2/update_case/$case_id",
590             'POST', $options );
591             }
592              
593             sub deleteCase {
594 4     4 1 7470 state $check = compile( Object, Int );
595 4         4630 my ( $self, $case_id ) = $check->(@_);
596              
597 3         73 return $self->_doRequest( "index.php?/api/v2/delete_case/$case_id",
598             'POST' );
599             }
600              
601             sub getCases {
602 24     24 1 12393 state $check = compile( Object, Int, Int, Optional [ Maybe [HashRef] ] );
603 24         28717 my ( $self, $project_id, $suite_id, $filters ) = $check->(@_);
604              
605 22         1398 my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
606 22         106 $url .= _convert_filters_to_string($filters);
607              
608 22         90 return $self->_doRequest($url);
609             }
610              
611             sub getCaseByName {
612 7     7 1 13599 state $check =
613             compile( Object, Int, Int, Str, Optional [ Maybe [HashRef] ] );
614 7         11955 my ( $self, $project_id, $suite_id, $name, $filters ) = $check->(@_);
615              
616 4         321 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         5 foreach my $case (@$cases) {
619 1 50       9 return $case if $case->{'title'} eq $name;
620             }
621 0         0 return 0;
622             }
623              
624             sub getCaseByID {
625 4     4 1 7063 state $check = compile( Object, Int );
626 4         4608 my ( $self, $case_id ) = $check->(@_);
627              
628 3         74 return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
629             }
630              
631             sub getCaseFields {
632 3     3 1 876 state $check = compile(Object);
633 3         865 my ($self) = $check->(@_);
634 3 100       38 return $self->{case_fields} if $self->{case_fields};
635              
636             $self->{case_fields} =
637 2         7 $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         1297 my ( $self, $options ) = $check->(@_);
644 1         20 $self->{case_fields} = undef;
645 1         6 return $self->_doRequest( "index.php?/api/v2/add_case_field", 'POST',
646             $options );
647             }
648              
649             sub getPriorities {
650 4     4 1 4907 state $check = compile(Object);
651 4         2537 my ($self) = $check->(@_);
652             return clone( $self->{'priority_cache'} )
653 4 100       89 if defined( $self->{'priority_cache'} );
654              
655 2         14 my $priorities = $self->_doRequest("index.php?/api/v2/get_priorities");
656 2 100 100     106 return -500
      66        
657             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
658 1         5 $self->{'priority_cache'} = $priorities;
659              
660 1         31 return clone $priorities;
661             }
662              
663             sub getPriorityByName {
664 2     2 1 3838 state $check = compile( Object, Str );
665 2         3168 my ( $self, $name ) = $check->(@_);
666              
667 1         21 my $priorities = $self->getPriorities();
668 1 50 50     13 return -500
      33        
669             if !$priorities || ( reftype($priorities) || 'undef' ) ne 'ARRAY';
670 1         4 foreach my $priority (@$priorities) {
671 1 50       12 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         5 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 26164 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         61513 my ( $self, $project_id, $suite_id, $name, $desc, $milestone_id,
690             $assignedto_id, $case_ids )
691             = $check->(@_);
692              
693 274 100       11982 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         977 return $self->_doRequest( "index.php?/api/v2/add_run/$project_id",
704             'POST', $stuff );
705             }
706              
707             sub deleteRun {
708 4     4 1 6842 state $check = compile( Object, Int );
709 4         4509 my ( $self, $run_id ) = $check->(@_);
710              
711 3         74 return $self->_doRequest( "index.php?/api/v2/delete_run/$run_id", 'POST' );
712             }
713              
714             sub getRuns {
715 112     112 1 8732 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
716 112         45889 my ( $self, $project_id, $filters ) = $check->(@_);
717              
718             my $initial_runs =
719 111         6095 $self->getRunsPaginated( $project_id, $self->{'global_limit'},
720             0, $filters );
721 111 100 100     95960 return $initial_runs
722             unless ( reftype($initial_runs) || 'undef' ) eq 'ARRAY';
723 107         311 my $runs = [];
724 107         1069 push( @$runs, @$initial_runs );
725 107         242 my $offset = 1;
726 107         534 while ( scalar(@$initial_runs) == $self->{'global_limit'} ) {
727             $initial_runs = $self->getRunsPaginated(
728             $project_id,
729             $self->{'global_limit'},
730 22         140 ( $self->{'global_limit'} * $offset ), $filters
731             );
732 22         2085 push( @$runs, @$initial_runs );
733 22         100 $offset++;
734             }
735 107         512 return $runs;
736             }
737              
738             sub getRunsPaginated {
739 135     135 1 6510 state $check = compile( Object,
740             Int,
741             Optional [ Maybe [Int] ],
742             Optional [ Maybe [Int] ],
743             Optional [ Maybe [HashRef] ]
744             );
745 135         78751 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
746              
747             confess( "Limit greater than " . $self->{'global_limit'} )
748 134 50       9618 if $limit > $self->{'global_limit'};
749 134         609 my $apiurl = "index.php?/api/v2/get_runs/$project_id";
750 134 100       675 $apiurl .= "&offset=$offset" if defined($offset);
751 134 100       754 $apiurl .= "&limit=$limit"
752             if $limit; #You have problems if you want 0 results
753 134         1140 $apiurl .= _convert_filters_to_string($filters);
754 134         1310 return $self->_doRequest($apiurl);
755             }
756              
757             sub getRunByName {
758 60     60 1 8410 state $check = compile( Object, Int, Str );
759 60         21588 my ( $self, $project_id, $name ) = $check->(@_);
760              
761 58         1955 my $runs = $self->getRuns($project_id);
762 58 100 100     765 return -500 if !$runs || ( reftype($runs) || 'undef' ) ne 'ARRAY';
      66        
763 56         274 foreach my $run (@$runs) {
764 178 100       2483 return $run if $run->{'name'} eq $name;
765             }
766 19         306 return 0;
767             }
768              
769             sub getRunByID {
770 6     6 1 7669 state $check = compile( Object, Int );
771 6         4513 my ( $self, $run_id ) = $check->(@_);
772              
773 5         116 return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
774             }
775              
776             sub closeRun {
777 4     4 1 5238 state $check = compile( Object, Int );
778 4         4171 my ( $self, $run_id ) = $check->(@_);
779              
780 4         106 return $self->_doRequest( "index.php?/api/v2/close_run/$run_id", 'POST' );
781             }
782              
783             sub getRunSummary {
784 14     14 1 3571 state $check = compile( Object, slurpy ArrayRef [HashRef] );
785 14         58810 my ( $self, $runs ) = $check->(@_);
786 14 100       1968 confess("At least one run must be passed!") unless scalar(@$runs);
787              
788             #Translate custom statuses
789 13         148 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       568 . ( $_->{'id'} - 5 )
799             . "_count"
800             } @$statuses
801 13         56 } = map { $_->{'id'} } @$statuses;
  117         299  
802              
803 13         64 my @sname;
804              
805             #Create listing of keys/values
806             @$runs = map {
807 13         51 my $run = $_;
  282         445  
808 282         2022 @{ $run->{statuses} }{ grep { $_ =~ m/_count$/ } keys(%$run) } =
  7833         15403  
809 282         1916 grep { $_ =~ m/_count$/ } keys(%$run);
  7833         17385  
810 282         988 foreach my $status ( keys( %{ $run->{'statuses'} } ) ) {
  282         996  
811 3348 100       5958 next if !exists( $shash{$status} );
812             @sname = grep {
813 2511         3622 exists( $shash{$status} )
814 22599 50       63012 && $_->{'id'} == $shash{$status}
815             } @$statuses;
816             $run->{'statuses_clean'}->{ $sname[0]->{'label'} } =
817 2511         6195 $run->{$status};
818             }
819 282         795 $run;
820             } @$runs;
821              
822             return map {
823 13         49 {
824             'id' => $_->{'id'},
825             'name' => $_->{'name'},
826             'run_status' => $_->{'statuses_clean'},
827 282         1385 'config_ids' => $_->{'config_ids'}
828             }
829             } @$runs;
830              
831             }
832              
833             sub getRunResults {
834 1     1 1 2722 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
835 1         3601 my ( $self, $run_id, $filters ) = $check->(@_);
836              
837             my $initial_results =
838 1         130 $self->getRunResultsPaginated( $run_id, $self->{'global_limit'},
839             undef, $filters );
840 1 50 50     71 return $initial_results
841             unless ( reftype($initial_results) || 'undef' ) eq 'ARRAY';
842 1         4 my $results = [];
843 1         4 push( @$results, @$initial_results );
844 1         3 my $offset = 1;
845 1         5 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         4 return $results;
855             }
856              
857             sub getRunResultsPaginated {
858 1     1 1 5 state $check = compile( Object,
859             Int,
860             Optional [ Maybe [Int] ],
861             Optional [ Maybe [Int] ],
862             Optional [ Maybe [HashRef] ]
863             );
864 1         5236 my ( $self, $run_id, $limit, $offset, $filters ) = $check->(@_);
865              
866             confess( "Limit greater than " . $self->{'global_limit'} )
867 1 50       202 if $limit > $self->{'global_limit'};
868 1         6 my $apiurl = "index.php?/api/v2/get_results_for_run/$run_id";
869 1 50       4 $apiurl .= "&offset=$offset" if defined($offset);
870 1 50       7 $apiurl .= "&limit=$limit"
871             if $limit; #You have problems if you want 0 results
872 1         5 $apiurl .= _convert_filters_to_string($filters);
873 1         6 return $self->_doRequest($apiurl);
874             }
875              
876             sub getChildRuns {
877 2432     2432 1 9143 state $check = compile( Object, HashRef );
878 2432         17915 my ( $self, $plan ) = $check->(@_);
879              
880             return 0
881             unless defined( $plan->{'entries'} )
882 2431 100 50     27138 && ( reftype( $plan->{'entries'} ) || 'undef' ) eq 'ARRAY';
      66        
883 2429         3827 my $entries = $plan->{'entries'};
884 2429         4075 my $plans = [];
885 2429         3878 foreach my $entry (@$entries) {
886 2458         4970 push( @$plans, @{ $entry->{'runs'} } )
887             if defined( $entry->{'runs'} )
888 2458 50 50     9573 && ( ( reftype( $entry->{'runs'} ) || 'undef' ) eq 'ARRAY' );
      33        
889             }
890 2429         4722 return $plans;
891             }
892              
893             sub getChildRunByName {
894 42     42 1 8761 state $check = compile( Object, HashRef, Str,
895             Optional [ Maybe [ ArrayRef [Str] ] ],
896             Optional [ Maybe [Int] ]
897             );
898 42         61880 my ( $self, $plan, $name, $configurations, $testsuite_id ) = $check->(@_);
899              
900 40         3440 my $runs = $self->getChildRuns($plan);
901 40 100       120 @$runs = grep { $_->{suite_id} == $testsuite_id } @$runs if $testsuite_id;
  2         9  
902 40 100       131 return 0 if !$runs;
903              
904 39         80 my @pconfigs = ();
905              
906             #Figure out desired config IDs
907 39 100       103 if ( defined $configurations ) {
908 38         143 my $avail_configs = $self->getConfigurations( $plan->{'project_id'} );
909 38         82 my ($cname);
910 36         266 @pconfigs = map { $_->{'id'} } grep {
911 38         101 $cname = $_->{'name'};
  158         264  
912 158         282 grep { $_ eq $cname } @$configurations
  144         339  
913             } @$avail_configs; #Get a list of IDs from the names passed
914             }
915 39 50 66     317 confess("One or more configurations passed does not exist in your project!")
916             if defined($configurations)
917             && ( scalar(@pconfigs) != scalar(@$configurations) );
918              
919 39         84 my $found;
920 39         120 foreach my $run (@$runs) {
921 38 100       150 next if $run->{name} ne $name;
922 35 100       66 next if scalar(@pconfigs) != scalar( @{ $run->{'config_ids'} } );
  35         112  
923              
924             #Compare run config IDs against desired, invalidate run if all conditions not satisfied
925 34         69 $found = 0;
926 34         60 foreach my $cid ( @{ $run->{'config_ids'} } ) {
  34         83  
927 33 100       68 $found++ if grep { $_ == $cid } @pconfigs;
  33         166  
928             }
929              
930 34 100       62 return $run if $found == scalar( @{ $run->{'config_ids'} } );
  34         202  
931             }
932 7         96 return 0;
933             }
934              
935             sub createPlan {
936 264     264 1 27652 state $check = compile( Object, Int, Str,
937             Optional [ Maybe [Str] ],
938             Optional [ Maybe [Int] ],
939             Optional [ Maybe [ ArrayRef [HashRef] ] ]
940             );
941 264         53365 my ( $self, $project_id, $name, $desc, $milestone_id, $entries ) =
942             $check->(@_);
943              
944 262         9552 my $stuff = {
945             name => $name,
946             description => $desc,
947             milestone_id => $milestone_id,
948             entries => $entries
949             };
950              
951 262         979 return $self->_doRequest( "index.php?/api/v2/add_plan/$project_id",
952             'POST', $stuff );
953             }
954              
955             sub deletePlan {
956 4     4 1 8341 state $check = compile( Object, Int );
957 4         4786 my ( $self, $plan_id ) = $check->(@_);
958              
959 3         73 return $self->_doRequest( "index.php?/api/v2/delete_plan/$plan_id",
960             'POST' );
961             }
962              
963             sub getPlans {
964 105     105 1 8649 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
965 105         38466 my ( $self, $project_id, $filters ) = $check->(@_);
966              
967             my $initial_plans =
968 104         4410 $self->getPlansPaginated( $project_id, $self->{'global_limit'},
969             0, $filters );
970 104 100 100     67045 return $initial_plans
971             unless ( reftype($initial_plans) || 'undef' ) eq 'ARRAY';
972 100         370 my $plans = [];
973 100         1005 push( @$plans, @$initial_plans );
974 100         269 my $offset = 1;
975 100         507 while ( scalar(@$initial_plans) == $self->{'global_limit'} ) {
976             $initial_plans = $self->getPlansPaginated(
977             $project_id,
978             $self->{'global_limit'},
979 19         146 ( $self->{'global_limit'} * $offset ), $filters
980             );
981 19         1898 push( @$plans, @$initial_plans );
982 19         100 $offset++;
983             }
984 100         514 return $plans;
985             }
986              
987             sub getPlansPaginated {
988 125     125 1 6467 state $check = compile( Object,
989             Int,
990             Optional [ Maybe [Int] ],
991             Optional [ Maybe [Int] ],
992             Optional [ Maybe [HashRef] ]
993             );
994 125         63275 my ( $self, $project_id, $limit, $offset, $filters ) = $check->(@_);
995              
996             confess( "Limit greater than " . $self->{'global_limit'} )
997 124 50       7960 if $limit > $self->{'global_limit'};
998 124         1023 my $apiurl = "index.php?/api/v2/get_plans/$project_id";
999 124 100       1081 $apiurl .= "&offset=$offset" if defined($offset);
1000 124 100       1024 $apiurl .= "&limit=$limit"
1001             if $limit; #You have problems if you want 0 results
1002 124         602 $apiurl .= _convert_filters_to_string($filters);
1003 124         551 return $self->_doRequest($apiurl);
1004             }
1005              
1006             sub getPlanByName {
1007 51     51 1 9062 state $check = compile( Object, Int, Str );
1008 51         19570 my ( $self, $project_id, $name ) = $check->(@_);
1009              
1010 49         1410 my $plans = $self->getPlans($project_id);
1011 49 100 100     649 return -500 if !$plans || ( reftype($plans) || 'undef' ) ne 'ARRAY';
      66        
1012 47         221 foreach my $plan (@$plans) {
1013 856 100       2024 if ( $plan->{'name'} eq $name ) {
1014 39         182 return $self->getPlanByID( $plan->{'id'} );
1015             }
1016             }
1017 8         2601 return 0;
1018             }
1019              
1020             sub getPlanByID {
1021 113     113 1 8619 state $check = compile( Object, Int );
1022 113         15558 my ( $self, $plan_id ) = $check->(@_);
1023              
1024 112         2331 return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
1025             }
1026              
1027             sub getPlanSummary {
1028 6     6 1 5714 state $check = compile( Object, Int );
1029 6         9980 my ( $self, $plan_id ) = $check->(@_);
1030              
1031 5         165 my $runs = $self->getPlanByID($plan_id);
1032 5         858 $runs = $self->getChildRuns($runs);
1033 5         47 @$runs = $self->getRunSummary( @{$runs} );
  5         54  
1034 5         19 my $total_sum = 0;
1035 5         33 my $ret = { plan => $plan_id };
1036              
1037             #Compile totals
1038 5         25 foreach my $summary (@$runs) {
1039 11         28 my @elems = keys( %{ $summary->{'run_status'} } );
  11         68  
1040 11         30 foreach my $key (@elems) {
1041 99 100       266 $ret->{'totals'}->{$key} = 0 if !defined $ret->{'totals'}->{$key};
1042 99         153 $ret->{'totals'}->{$key} += $summary->{'run_status'}->{$key};
1043 99         156 $total_sum += $summary->{'run_status'}->{$key};
1044             }
1045             }
1046              
1047             #Compile percentages
1048 5         16 foreach my $key ( keys( %{ $ret->{'totals'} } ) ) {
  5         29  
1049 45 50       91 next if grep { $_ eq $key } qw{plan configs percentages};
  135         296  
1050             $ret->{"percentages"}->{$key} =
1051 45         393 sprintf( "%.2f%%", ( $ret->{'totals'}->{$key} / $total_sum ) * 100 );
1052             }
1053              
1054 5         62 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 13176 state $check = compile( Object, Int, Int, Str,
1060             Optional [ Maybe [Int] ],
1061             Optional [ Maybe [ ArrayRef [Int] ] ],
1062             Optional [ Maybe [ ArrayRef [Int] ] ]
1063             );
1064 14         31736 my ( $self, $plan_id, $suite_id, $name, $assignedto_id, $config_ids,
1065             $case_ids )
1066             = $check->(@_);
1067              
1068 11 100       1649 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       129 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         79 return $self->_doRequest( "index.php?/api/v2/add_plan_entry/$plan_id",
1086             'POST', $stuff );
1087             }
1088              
1089             sub closePlan {
1090 6     6 1 6620 state $check = compile( Object, Int );
1091 6         6934 my ( $self, $plan_id ) = $check->(@_);
1092              
1093 6         152 return $self->_doRequest( "index.php?/api/v2/close_plan/$plan_id", 'POST' );
1094             }
1095              
1096             sub createMilestone {
1097 5     5 1 10136 state $check = compile( Object, Int, Str,
1098             Optional [ Maybe [Str] ],
1099             Optional [ Maybe [Int] ]
1100             );
1101 5         16164 my ( $self, $project_id, $name, $desc, $due_on ) = $check->(@_);
1102              
1103 3         479 my $stuff = {
1104             name => $name,
1105             description => $desc,
1106             due_on => $due_on # unix timestamp
1107             };
1108              
1109 3         18 return $self->_doRequest( "index.php?/api/v2/add_milestone/$project_id",
1110             'POST', $stuff );
1111             }
1112              
1113             sub deleteMilestone {
1114 4     4 1 7193 state $check = compile( Object, Int );
1115 4         4494 my ( $self, $milestone_id ) = $check->(@_);
1116              
1117 3         74 return $self->_doRequest(
1118             "index.php?/api/v2/delete_milestone/$milestone_id", 'POST' );
1119             }
1120              
1121             sub getMilestones {
1122 7     7 1 8076 state $check = compile( Object, Int, Optional [ Maybe [HashRef] ] );
1123 7         10136 my ( $self, $project_id, $filters ) = $check->(@_);
1124              
1125 6         324 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 9490 state $check = compile( Object, Int, Str );
1131 5         5371 my ( $self, $project_id, $name ) = $check->(@_);
1132              
1133 3         86 my $milestones = $self->getMilestones($project_id);
1134 3 100 100     131 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 7293 state $check = compile( Object, Int );
1144 27         10389 my ( $self, $milestone_id ) = $check->(@_);
1145              
1146 26         528 return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
1147             }
1148              
1149             sub getTests {
1150 91     91 1 10930 state $check = compile( Object, Int,
1151             Optional [ Maybe [ ArrayRef [Int] ] ],
1152             Optional [ Maybe [ ArrayRef [Int] ] ]
1153             );
1154 91         68147 my ( $self, $run_id, $status_ids, $assignedto_ids ) = $check->(@_);
1155              
1156 90         4314 my $query_string = '';
1157 90 50 100     445 $query_string = '&status_id=' . join( ',', @$status_ids )
1158             if defined($status_ids) && scalar(@$status_ids);
1159 90         895 my $results =
1160             $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
1161             @$results = grep {
1162 90 50 100     8022 my $aid = $_->{'assignedto_id'};
  22         43  
1163 22 100       35 grep { defined($aid) && $aid == $_ } @$assignedto_ids
  22         89  
1164             } @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
1165              
1166             #Cache stuff for getTestByName
1167 90   100     489 $self->{tests_cache} //= {};
1168 90         546 $self->{tests_cache}->{$run_id} = $results;
1169              
1170 90         9698 return clone($results);
1171             }
1172              
1173             sub getTestByName {
1174 51     51 1 10912 state $check = compile( Object, Int, Str );
1175 51         13606 my ( $self, $run_id, $name ) = $check->(@_);
1176              
1177 49   100     2474 $self->{tests_cache} //= {};
1178 49         165 my $tests = $self->{tests_cache}->{$run_id};
1179              
1180 49 100       468 $tests = $self->getTests($run_id) if !$tests;
1181 49 100 100     852 return -500 if !$tests || ( reftype($tests) || 'undef' ) ne 'ARRAY';
      66        
1182 47         235 foreach my $test (@$tests) {
1183 137 100       711 return $test if $test->{'title'} eq $name;
1184             }
1185 1         12 return 0;
1186             }
1187              
1188             sub getTestByID {
1189 4     4 1 7228 state $check = compile( Object, Int );
1190 4         4433 my ( $self, $test_id ) = $check->(@_);
1191              
1192 3         73 return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
1193             }
1194              
1195             sub getTestResultFields {
1196 20     20 1 4947 state $check = compile(Object);
1197 20         6024 my ($self) = $check->(@_);
1198              
1199 20 100       333 return $self->{'tr_fields'} if defined( $self->{'tr_fields'} ); #cache
1200 17         231 $self->{'tr_fields'} =
1201             $self->_doRequest('index.php?/api/v2/get_result_fields');
1202 17         2146 return $self->{'tr_fields'};
1203             }
1204              
1205             sub getTestResultFieldByName {
1206 18     18 1 9751 state $check = compile( Object, Str, Optional [ Maybe [Int] ] );
1207 18         19044 my ( $self, $system_name, $project_id ) = $check->(@_);
1208              
1209             my @candidates =
1210 17         1244 grep { $_->{'name'} eq $system_name } @{ $self->getTestResultFields() };
  17         133  
  17         166  
1211 17 100       121 return 0 if !scalar(@candidates); #No such name
1212 15 50       137 return -1 if ref( $candidates[0] ) ne 'HASH';
1213             return -2
1214             if ref( $candidates[0]->{'configs'} ) ne 'ARRAY'
1215 15 50 33     128 && !scalar( @{ $candidates[0]->{'configs'} } ); #bogofilter
  0         0  
1216              
1217             #Give it to the user
1218 15         56 my $ret = $candidates[0]; #copy/save for later
1219 15 100       85 return $ret if !defined($project_id);
1220              
1221             #Filter by project ID
1222 14         44 foreach my $config ( @{ $candidates[0]->{'configs'} } ) {
  14         79  
1223             return $ret
1224 62         301 if ( grep { $_ == $project_id }
1225 34 100       67 @{ $config->{'context'}->{'project_ids'} } );
  34         93  
1226             }
1227              
1228 1         5 return -3;
1229             }
1230              
1231             sub getPossibleTestStatuses {
1232 111     111 1 4462 state $check = compile(Object);
1233 111         16670 my ($self) = $check->(@_);
1234 111 100       1679 return $self->{'status_cache'} if $self->{'status_cache'};
1235              
1236 81         789 $self->{'status_cache'} =
1237             $self->_doRequest('index.php?/api/v2/get_statuses');
1238 81         25343 return clone $self->{'status_cache'};
1239             }
1240              
1241             sub statusNamesToIds {
1242 21     21 1 12915 my ( $self, @names ) = @_;
1243 21         107 return _X_in_my_Y( $self, $self->getPossibleTestStatuses(), 'id', @names );
1244             }
1245              
1246             sub statusNamesToLabels {
1247 5     5 1 586 my ( $self, @names ) = @_;
1248 5         26 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   282 state $check = compile( Object, ArrayRef, Str, slurpy ArrayRef [Str] );
1257 100         98813 my ( $self, $search_arr, $key, $names ) = $check->(@_);
1258              
1259 97         5789 my @ret;
1260 97         528 foreach my $name (@$names) {
1261 127         298 foreach my $member (@$search_arr) {
1262 527 100       1097 if ( $member->{'name'} eq $name ) {
1263 123         252 push @ret, $member->{$key};
1264 123         256 last;
1265             }
1266             }
1267             }
1268 97 100       1921 confess("One or more names provided does not exist in TestRail.")
1269             unless scalar(@$names) == scalar(@ret);
1270 93         745 return @ret;
1271             }
1272              
1273             sub createTestResults {
1274 56     56 1 10109 state $check = compile( Object, Int, Int,
1275             Optional [ Maybe [Str] ],
1276             Optional [ Maybe [HashRef] ],
1277             Optional [ Maybe [HashRef] ]
1278             );
1279 56         39362 my ( $self, $test_id, $status_id, $comment, $opts, $custom_fields ) =
1280             $check->(@_);
1281              
1282 54         4193 my $stuff = {
1283             status_id => $status_id,
1284             comment => $comment
1285             };
1286              
1287             #Handle options
1288 54 100 66     708 if ( defined($opts) && reftype($opts) eq 'HASH' ) {
1289             $stuff->{'version'} =
1290 44 100       307 defined( $opts->{'version'} ) ? $opts->{'version'} : undef;
1291             $stuff->{'elapsed'} =
1292 44 100       212 defined( $opts->{'elapsed'} ) ? $opts->{'elapsed'} : undef;
1293             $stuff->{'defects'} =
1294             defined( $opts->{'defects'} )
1295 44 50       343 ? join( ',', @{ $opts->{'defects'} } )
  0         0  
1296             : undef;
1297             $stuff->{'assignedto_id'} =
1298             defined( $opts->{'assignedto_id'} )
1299 44 50       232 ? $opts->{'assignedto_id'}
1300             : undef;
1301             }
1302              
1303             #Handle custom fields
1304 54 100 66     400 if ( defined($custom_fields) && reftype($custom_fields) eq 'HASH' ) {
1305 7         48 foreach my $field ( keys(%$custom_fields) ) {
1306 7         46 $stuff->{"custom_$field"} = $custom_fields->{$field};
1307             }
1308             }
1309              
1310 54         308 return $self->_doRequest( "index.php?/api/v2/add_result/$test_id",
1311             'POST', $stuff );
1312             }
1313              
1314             sub bulkAddResults {
1315 6     6 1 8481 state $check = compile( Object, Int, ArrayRef [HashRef] );
1316 6         10326 my ( $self, $run_id, $results ) = $check->(@_);
1317              
1318 4         321 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 9394 state $check = compile( Object,
1332             Int,
1333             Optional [ Maybe [Int] ],
1334             Optional [ Maybe [Int] ],
1335             Optional [ Maybe [HashRef] ]
1336             );
1337 12         30962 my ( $self, $test_id, $limit, $offset, $filters ) = $check->(@_);
1338              
1339 11         1136 my $url = "index.php?/api/v2/get_results/$test_id";
1340 11 100       56 $url .= "&limit=$limit" if $limit;
1341 11 50       40 $url .= "&offset=$offset" if defined($offset);
1342 11         71 $url .= _convert_filters_to_string($filters);
1343 11         58 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 6776 state $check = compile( Object, Int );
1362 199         13928 my ( $self, $project_id ) = $check->(@_);
1363              
1364 198         3862 my $url = "index.php?/api/v2/get_configs/$project_id";
1365 198         737 return $self->_doRequest($url);
1366             }
1367              
1368             sub getConfigurationGroupByName {
1369 4     4 1 783 state $check = compile( Object, Int, Str );
1370 4         4608 my ( $self, $project_id, $name ) = $check->(@_);
1371              
1372 4         127 my $cgroups = $self->getConfigurationGroups($project_id);
1373 4 50       337 return 0 if ref($cgroups) ne 'ARRAY';
1374 4         19 @$cgroups = grep { $_->{'name'} eq $name } @$cgroups;
  12         58  
1375 4 100       35 return 0 unless scalar(@$cgroups);
1376 1         4 return $cgroups->[0];
1377             }
1378              
1379             sub addConfigurationGroup {
1380 4     4 1 776 state $check = compile( Object, Int, Str );
1381 4         3699 my ( $self, $project_id, $name ) = $check->(@_);
1382              
1383 4         125 my $url = "index.php?/api/v2/add_config_group/$project_id";
1384 4         32 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1385             }
1386              
1387             sub editConfigurationGroup {
1388 1     1 1 784 state $check = compile( Object, Int, Str );
1389 1         1597 my ( $self, $config_group_id, $name ) = $check->(@_);
1390              
1391 1         27 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 5 state $check = compile( Object, Int );
1397 1         1318 my ( $self, $config_group_id ) = $check->(@_);
1398              
1399 1         20 my $url = "index.php?/api/v2/delete_config_group/$config_group_id";
1400 1         5 return $self->_doRequest( $url, 'POST' );
1401             }
1402              
1403             sub getConfigurations {
1404 195     195 1 8680 state $check = compile( Object, Int );
1405 195         15345 my ( $self, $project_id ) = $check->(@_);
1406              
1407 193         5369 my $cgroups = $self->getConfigurationGroups($project_id);
1408 193         11234 my $configs = [];
1409 193 100 100     1844 return $cgroups unless ( reftype($cgroups) || 'undef' ) eq 'ARRAY';
1410 190         1229 foreach my $cfg (@$cgroups) {
1411 378         735 push( @$configs, @{ $cfg->{'configs'} } );
  378         1088  
1412             }
1413 190         1570 return $configs;
1414             }
1415              
1416             sub addConfiguration {
1417 4     4 1 786 state $check = compile( Object, Int, Str );
1418 4         3730 my ( $self, $configuration_group_id, $name ) = $check->(@_);
1419              
1420 4         127 my $url = "index.php?/api/v2/add_config/$configuration_group_id";
1421 4         32 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1422             }
1423              
1424             sub editConfiguration {
1425 1     1 1 800 state $check = compile( Object, Int, Str );
1426 1         1596 my ( $self, $config_id, $name ) = $check->(@_);
1427              
1428 1         26 my $url = "index.php?/api/v2/update_config/$config_id";
1429 1         6 return $self->_doRequest( $url, 'POST', { 'name' => $name } );
1430             }
1431              
1432             sub deleteConfiguration {
1433 1     1 1 764 state $check = compile( Object, Int );
1434 1         1284 my ( $self, $config_id ) = $check->(@_);
1435              
1436 1         22 my $url = "index.php?/api/v2/delete_config/$config_id";
1437 1         5 return $self->_doRequest( $url, 'POST' );
1438             }
1439              
1440             sub translateConfigNamesToIds {
1441 61     61 1 7611 my ( $self, $project_id, @names ) = @_;
1442 61 50       250 my $configs = $self->getConfigurations($project_id)
1443             or confess("Could not determine configurations in provided project.");
1444 60         399 return _X_in_my_Y( $self, $configs, 'id', @names );
1445             }
1446              
1447             sub getReports {
1448 1     1 1 353 state $check = compile( Object, Int );
1449 1         1538 my ( $self, $project_id ) = $check->(@_);
1450 1         21 my $url = "index.php?/api/v2/get_reports/$project_id";
1451 1         7 return $self->_doRequest( $url, 'GET' );
1452             }
1453              
1454             sub runReport {
1455 1     1 1 1010 state $check = compile( Object, Int );
1456 1         1175 my ( $self, $report_id ) = $check->(@_);
1457 1         21 my $url = "index.php?/api/v2/run_report/$report_id";
1458 1         7 return $self->_doRequest( $url, 'GET' );
1459             }
1460              
1461             #Convenience method for building stepResults
1462             sub buildStepResults {
1463 16     16 1 121 state $check = compile( Str, Str, Str, Int );
1464 16         5794 my ( $content, $expected, $actual, $status_id ) = $check->(@_);
1465              
1466             return {
1467 16         752 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   1193 state $check = compile( Maybe [HashRef] );
1477 404         28558 my ($filters) = $check->(@_);
1478              
1479 404   100     8033 $filters //= {};
1480              
1481 404         1390 my $filter_string = '';
1482 404         2357 foreach my $filter ( keys(%$filters) ) {
1483 29 50       133 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       154 if defined( $filters->{$filter} );
1490             }
1491             }
1492 404         1869 return $filter_string;
1493             }
1494              
1495             1;
1496              
1497             __END__