Browse Source

v32 changes: add updateCase, testrail-tests orphans and testrail-cases

George S. Baugh 10 years ago
parent
commit
be6d85884d

+ 8 - 0
Changes

@@ -1,5 +1,13 @@
 Revision history for Perl module TestRail::API
 
+0.032 2015-08-17 TEODESIAN
+    - Fix issue in getCases where arrayref filters were not handled properly
+    - Add TestRail::API::typeNamesToIds
+    - Add orphans option to testrail-tests, TestRail::Utils::Find::getTests
+    - Add TestRail::API::updateCase
+    - Add new TestRail::Utils::Find functions; getCases, findCases
+    - Add new script bin/testrail-cases
+
 0.031 2015-08-14 TEODESIAN
     - Update getCases to use testRail 4.0 filters, change filter args to HASHREF
     - Update TestRail::API::getCaseByName to take filter hashref too

+ 1 - 0
README.md

@@ -5,6 +5,7 @@ Perl interface to TestRail's REST API
 
 <img alt="TravisCI Build Status" src="https://travis-ci.org/teodesian/TestRail-Perl.svg"></img>
 <a href='https://coveralls.io/r/teodesian/TestRail-Perl?branch=build%2Fmaster'><img src='https://coveralls.io/repos/teodesian/TestRail-Perl/badge.svg?branch=build%2Fmaster' alt='Coverage Status' /></a>
+<a href="http://cpants.cpanauthors.org/dist/TestRail-API"><img alt="kwalitee" src="http://cpants.cpanauthors.org/dist/TestRail-API.png"></img></a>
 
 Implements most available TestRail API methods:
 

+ 155 - 0
bin/testrail-cases

@@ -0,0 +1,155 @@
+#!/usr/bin/env perl
+# ABSTRACT: get information about cases inside various testsuites/sections.
+# PODNAME: testrail-cases
+
+=head1 SYNOPSIS
+
+  testrail-cases [OPTIONS]
+
+=head1 DESCRIPTION
+
+testrail-cases - get information about cases inside various testsuites/sections.
+
+By default will tell you which cases are in both the testsuite and directory passed.
+
+=head1 PARAMETERS:
+
+=head2 MANDATORY PARAMETERS
+
+=over 4
+
+--apiurl     : full URL to get to TestRail index document
+
+--password   : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
+
+--user       : Your TestRail User Name.
+
+-j --project : desired project name.
+
+-t --testsuite  : desired testsuite name to search for cases within.  May be passed multiple times.
+
+-d --directory : directory to search for tests to correlate with TestRail cases.  May be passed multiple times.
+
+=back
+
+All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
+
+=head2 SEMI-OPTIONAL PARAMETERS
+
+=over 4
+
+-m --missing : Only show cases which are in the directory passed, but not TestRail.  Mutually exclusive with orphans.
+
+-o --orphans : Only show cases which are in TestRail, but not the directory passed.  Mutually exclusive with missing.
+
+-n --no-recurse : do not recurse subdirectories when considering what tests need adding/updating/pruning.
+
+-e --encoding   : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
+
+=back
+
+=head2 OPTIONAL PARAMETERS
+
+=over 4
+
+--type      : Filter cases to make syncing judgements against type(s).  May be passed multiple times.
+
+--section   : Filter cases to make syncing judgements against a specific section.
+
+--extension : only list files ending in the provided string (e.g. .pl, .pm, .t, .test)
+
+=back
+
+=head1 CONFIGURATION FILE
+
+In your \$HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
+Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
+All options specified thereby are overridden by passing the command-line switches above.
+
+=head1 MISCELLANEOUS OPTIONS:
+
+=over 4
+
+--help : show this output
+
+--test : print which tests would be added/updated/removed, but don't actually do anything
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+use utf8;
+
+use TestRail::API;
+use TestRail::Utils;
+use TestRail::Utils::Find;
+
+use Getopt::Long;
+use File::HomeDir qw{my_home};
+
+my $opts ={};
+
+#Parse config file if we are missing api url/key or user
+my $homedir = my_home() || '.';
+if (-e $homedir . '/.testrailrc') {
+    $opts = TestRail::Utils::parseConfig($homedir);
+}
+
+GetOptions(
+    'apiurl=s'        => \$opts->{'apiurl'},
+    'password=s'      => \$opts->{'password'},
+    'user=s'          => \$opts->{'user'},
+    'j|project=s'     => \$opts->{'project'},
+    't|testsuite=s'   => \$opts->{'testsuite'},
+    'd|directory=s'   => \$opts->{'directory'},
+    'm|missing'       => \$opts->{'missing'},
+    'o|orphans'       => \$opts->{'orphans'},
+    'n|no-recurse'    => \$opts->{'no-recurse'},
+    'e|encoding=s'    => \$opts->{'encoding'},
+    'section=s'       => \$opts->{'section'},
+    'type=s@'         => \$opts->{'types'},
+    'extension=s'     => \$opts->{'extension'},
+    'h|help'          => \$opts->{'help'},
+    'test'            => \$opts->{'test'},
+    'mock'            => \$opts->{'mock'} #actually do something, but bogusly
+);
+
+if ($opts->{help}) { TestRail::Utils::help(); }
+
+#Mutual exclusivity
+$opts->{'no-missing'} = !$opts->{'missing'};
+$opts->{'update'} = !($opts->{'orphans'} || $opts->{'missing'});
+die("orphans and mising options are mutually exclusive.") if $opts->{'orphans'} && $opts->{'missing'};
+delete $opts->{'missing'};
+
+TestRail::Utils::interrogateUser($opts,qw{apiurl user password project testsuite directory});
+
+my $tr = TestRail::Utils::getHandle($opts);
+
+my $cases = TestRail::Utils::Find::getCases($opts,$tr);
+die "No cases in TestRail!\n" unless $cases;
+
+my $tests = TestRail::Utils::Find::findCases($opts,@$cases);
+
+my (@update,@add,@orphan);
+@update = map {$_->{'title'}} @{$tests->{'update'}}  if ref $tests->{'update'}  eq 'ARRAY';
+@add = map {$_->{'title'}} @{$tests->{'orphans'}} if ref $tests->{'orphans'} eq 'ARRAY';
+@orphan = @{$tests->{'missing'}} if ref $tests->{'missing'} eq 'ARRAY';
+
+print join("\n",@update);
+print join("\n",@add);
+print join("\n",@orphan);
+print "\n";
+exit 0;
+
+__END__
+
+L<TestRail::API>
+
+L<File::HomeDir> for the finding of .testrailrc
+
+=head1 SPECIAL THANKS
+
+Thanks to cPanel Inc, for graciously funding the creation of this distribution.

+ 5 - 0
bin/testrail-tests

@@ -41,6 +41,10 @@ All mandatory options not passed with the above switches, or in your ~/.testrail
 
 --no-match      : attempt to find filenames that do not match test names in the provided directory.
 
+--orphans       : attempt to find tests in TestRail which aren't in the provided directory.
+
+The three above options are mutually exclusive.
+
 -n --no-recurse : if match (or no-match) passed, do not recurse subdirectories.
 
 -e --encoding   : Character encoding of arguments.  Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
@@ -108,6 +112,7 @@ GetOptions(
     'a|assignedto=s@' => \$opts->{'users'},
     'm|match=s'       => \$opts->{'match'},
     'no-match=s'      => \$opts->{'no-match'},
+    'orphans=s'       => \$opts->{'orphans'},
     'n|no-recurse'    => \$opts->{'no-recurse'},
     'e|encoding=s'    => \$opts->{'encoding'},
     'extension=s'     => \$opts->{'extension'},

+ 2 - 1
dist.ini

@@ -1,6 +1,6 @@
 name = TestRail-API
 main_module = lib/TestRail/API.pm
-version = 0.031
+version = 0.032
 author = George S. Baugh <teodesian@cpan.org>
 license = Perl_5
 copyright_holder = George S. Baugh
@@ -124,6 +124,7 @@ stopwords = assignee
 stopwords = parseConfig
 stopwords = getPlanSummary
 stopwords = getRunSummary
+stopwords = judgements
 
 [PkgVersion]
 [AutoPrereqs]

BIN
dist/TestRail-API-0.032.tar.gz


+ 65 - 36
lib/TestRail/API.pm

@@ -338,8 +338,7 @@ sub createProject {
         show_announcement => $announce
     };
 
-    my $result = $self->_doRequest('index.php?/api/v2/add_project','POST',$input);
-    return $result;
+    return $self->_doRequest('index.php?/api/v2/add_project','POST',$input);
 }
 
 =head2 B<deleteProject (id)>
@@ -363,8 +362,7 @@ sub deleteProject {
     state $check = compile(Object, Int);
     my ($self,$proj) = $check->(@_);
 
-    my $result = $self->_doRequest('index.php?/api/v2/delete_project/'.$proj,'POST');
-    return $result;
+    return $self->_doRequest('index.php?/api/v2/delete_project/'.$proj,'POST');
 }
 
 =head2 B<getProjects ()>
@@ -494,9 +492,7 @@ sub createTestSuite {
         description => $details
     };
 
-    my $result = $self->_doRequest('index.php?/api/v2/add_suite/'.$project_id,'POST',$input);
-    return $result;
-
+    return $self->_doRequest('index.php?/api/v2/add_suite/'.$project_id,'POST',$input);
 }
 
 =head2 B<deleteTestSuite (suite_id)>
@@ -519,9 +515,7 @@ sub deleteTestSuite {
     state $check = compile(Object, Int);
     my ($self,$suite_id) = $check->(@_);
 
-    my $result = $self->_doRequest('index.php?/api/v2/delete_suite/'.$suite_id,'POST');
-    return $result;
-
+    return $self->_doRequest('index.php?/api/v2/delete_suite/'.$suite_id,'POST');
 }
 
 =head2 B<getTestSuites (project_id)>
@@ -598,8 +592,7 @@ sub getTestSuiteByID {
     state $check = compile(Object, Int);
     my ($self,$testsuite_id) = $check->(@_);
 
-    my $result = $self->_doRequest('index.php?/api/v2/get_suite/'.$testsuite_id);
-    return $result;
+    return $self->_doRequest('index.php?/api/v2/get_suite/'.$testsuite_id);
 }
 
 =head1 SECTION METHODS
@@ -636,8 +629,7 @@ sub createSection {
     };
     $input->{'parent_id'} = $parent_id if $parent_id;
 
-    my $result = $self->_doRequest('index.php?/api/v2/add_section/'.$project_id,'POST',$input);
-    return $result;
+    return $self->_doRequest('index.php?/api/v2/add_section/'.$project_id,'POST',$input);
 }
 
 =head2 B<deleteSection (section_id)>
@@ -660,8 +652,7 @@ sub deleteSection {
     state $check = compile(Object, Int);
     my ($self,$section_id) = $check->(@_);
 
-    my $result = $self->_doRequest('index.php?/api/v2/delete_section/'.$section_id,'POST');
-    return $result;
+    return $self->_doRequest('index.php?/api/v2/delete_section/'.$section_id,'POST');
 }
 
 =head2 B<getSections (project_id,suite_id)>
@@ -825,6 +816,28 @@ sub getCaseTypeByName {
     confess("No such case type '$name'!");
 }
 
+=head2 typeNamesToIds(names)
+
+Convenience method to translate a list of case type names to TestRail case type IDs.
+
+=over 4
+
+=item ARRAY C<NAMES> - Array of status names to translate to IDs.
+
+=back
+
+Returns ARRAY of type IDs in the same order as the type names passed.
+
+Throws an exception in the case of one (or more) of the names not corresponding to a valid case type.
+
+=cut
+
+sub typeNamesToIds {
+    my ($self,@names) = @_;
+    return _X_in_my_Y($self,$self->getCaseTypes(),'id',@names);
+};
+
+
 =head2 B<createCase(section_id,title,type_id,options,extra_options)>
 
 Creates a test case.
@@ -886,13 +899,35 @@ sub createCase {
         }
     }
 
-    my $result = $self->_doRequest("index.php?/api/v2/add_case/$section_id",'POST',$stuff);
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/add_case/$section_id",'POST',$stuff);
+}
+
+=head2 B<updateCase(case_id,options)>
+
+Updates a test case.
+
+=over 4
+
+=item INTEGER C<CASE ID> - Case ID.
+
+=item HASHREF C<OPTIONS> - Various things about a case to set.  Everything except section_id in the output of getCaseBy* methods is a valid input here.
+
+=back
+
+Returns new case definition HASHREF, false otherwise.
+
+=cut
+
+sub updateCase {
+    state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
+    my ($self,$case_id,$options) = $check->(@_);
+
+    return $self->_doRequest("index.php?/api/v2/update_case/$case_id",'POST',$options);
 }
 
 =head2 B<deleteCase (case_id)>
 
-Deletes specified section.
+Deletes specified test case.
 
 =over 4
 
@@ -910,8 +945,7 @@ sub deleteCase {
     state $check = compile(Object, Int);
     my ($self,$case_id) = $check->(@_);
 
-    my $result = $self->_doRequest("index.php?/api/v2/delete_case/$case_id",'POST');
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/delete_case/$case_id",'POST');
 }
 
 =head2 B<getCases (project_id,suite_id,filters)>
@@ -949,11 +983,13 @@ sub getCases {
 
     my @valid_keys = qw{section_id created_after created_before created_by milestone_id priority_id type_id updated_after updated_before updated_by};
 
+
     # Add in filters
     foreach my $filter (keys(%$filters)) {
         confess("Invalid filter key '$filter' passed") unless grep {$_ eq $filter} @valid_keys;
         if (ref $filters->{$filter} eq 'ARRAY') {
-            $url .= "&$filter=".join(',',$filters->{$filter});
+            confess "$filter cannot be an ARRAYREF" if grep {$_ eq $filter} qw{created_after created_before updated_after updated_before};
+            $url .= "&$filter=".join(',',@{$filters->{$filter}});
         } else {
             $url .= "&$filter=".$filters->{$filter} if defined($filters->{$filter});
         }
@@ -1064,8 +1100,7 @@ sub createRun {
         case_ids      => $case_ids
     };
 
-    my $result = $self->_doRequest("index.php?/api/v2/add_run/$project_id",'POST',$stuff);
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/add_run/$project_id",'POST',$stuff);
 }
 
 =head2 B<deleteRun (run_id)>
@@ -1088,8 +1123,7 @@ sub deleteRun {
     state $check = compile(Object, Int);
     my ($self,$run_id) = $check->(@_);
 
-    my $result = $self->_doRequest("index.php?/api/v2/delete_run/$run_id",'POST');
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/delete_run/$run_id",'POST');
 }
 
 =head2 B<getRuns (project_id)>
@@ -1403,8 +1437,7 @@ sub createPlan {
         entries       => $entries
     };
 
-    my $result = $self->_doRequest("index.php?/api/v2/add_plan/$project_id",'POST',$stuff);
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/add_plan/$project_id",'POST',$stuff);
 }
 
 =head2 B<deletePlan (plan_id)>
@@ -1427,8 +1460,7 @@ sub deletePlan {
     state $check = compile(Object, Int);
     my ($self,$plan_id) = $check->(@_);
 
-    my $result = $self->_doRequest("index.php?/api/v2/delete_plan/$plan_id",'POST');
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/delete_plan/$plan_id",'POST');
 }
 
 =head2 B<getPlans (project_id)>
@@ -1647,8 +1679,7 @@ sub createRunInPlan {
         config_ids    => $config_ids,
         runs          => $runs
     };
-    my $result = $self->_doRequest("index.php?/api/v2/add_plan_entry/$plan_id",'POST',$stuff);
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/add_plan_entry/$plan_id",'POST',$stuff);
 }
 
 =head2 B<closePlan (plan_id)>
@@ -1708,8 +1739,7 @@ sub createMilestone {
         due_on      => $due_on # unix timestamp
     };
 
-    my $result = $self->_doRequest("index.php?/api/v2/add_milestone/$project_id",'POST',$stuff);
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/add_milestone/$project_id",'POST',$stuff);
 }
 
 =head2 B<deleteMilestone (milestone_id)>
@@ -1732,8 +1762,7 @@ sub deleteMilestone {
     state $check = compile(Object, Int);
     my ($self,$milestone_id) = $check->(@_);
 
-    my $result = $self->_doRequest("index.php?/api/v2/delete_milestone/$milestone_id",'POST');
-    return $result;
+    return $self->_doRequest("index.php?/api/v2/delete_milestone/$milestone_id",'POST');
 }
 
 =head2 B<getMilestones (project_id)>

+ 96 - 5
lib/TestRail/Utils/Find.pm

@@ -165,9 +165,11 @@ Given an ARRAY of tests, find tests meeting your criteria (or not) in the specif
 
 =over 4
 
-=item STRING C<MATCH> - Only return tests which exist in the path provided.  Mutually exclusive with no-match.
+=item STRING C<MATCH> - Only return tests which exist in the path provided, and in TestRail.  Mutually exclusive with no-match, orphans.
 
-=item STRING C<NO-MATCH> - Only return tests which aren't in the path provided (orphan tests).  Mutually exclusive with match.
+=item STRING C<NO-MATCH> - Only return tests which are in the path provided, but not in TestRail.  Mutually exclusive with match, orphans.
+
+=item STRING C<ORPHANS> - Only return tests which are in TestRail, and not in the path provided.  Mutually exclusive with match, no-match
 
 =item BOOL C<NO-RECURSE> - Do not do a recursive scan for files.
 
@@ -191,13 +193,16 @@ sub findTests {
     my ($opts,@cases) = @_;
 
     confess "Error! match and no-match options are mutually exclusive.\n" if ($opts->{'match'} && $opts->{'no-match'});
+    confess "Error! match and orphans options are mutually exclusive.\n" if ($opts->{'match'} && $opts->{'orphans'});
+    confess "Error! no-match and orphans options are mutually exclusive.\n" if ($opts->{'orphans'} && $opts->{'no-match'});
     my @tests = @cases;
     my (@realtests);
     my $ext = $opts->{'extension'} // '';
 
-    if ($opts->{'match'} || $opts->{'no-match'}) {
+    if ($opts->{'match'} || $opts->{'no-match'} || $opts->{'orphans'}) {
         my @tmpArr = ();
-        my $dir = $opts->{'match'} ? $opts->{'match'} : $opts->{'no-match'};
+        my $dir = ($opts->{'match'} || $opts->{'orphans'}) ? ($opts->{'match'} || $opts->{'orphans'}) : $opts->{'no-match'};
+        confess "No such directory '$dir'" if ! -d $dir;
         if (!$opts->{'no-recurse'}) {
             File::Find::find( sub { push(@realtests,$File::Find::name) if -f && m/\Q$ext\E$/ }, $dir );
         } else {
@@ -211,7 +216,8 @@ sub findTests {
                 last;
             }
         }
-        @tests = @tmpArr; #XXX if you have dups in your tree, be-ware
+        @tmpArr = grep {my $otest = $_; !(grep {$otest->{'title'} eq $_->{'title'}} @tmpArr) } @tests if $opts->{'orphans'};
+        @tests = @tmpArr;
         @tests = map {{'title' => $_}} grep {my $otest = basename($_); scalar(grep {basename($_->{'title'}) eq $otest} @tests) == 0} @realtests if $opts->{'no-match'}; #invert the list in this case.
     }
 
@@ -222,6 +228,91 @@ sub findTests {
     return @tests;
 }
 
+=head2 getCases
+
+Get cases in a testsuite matching your parameters passed
+
+=cut
+
+sub getCases {
+    my ($opts,$tr) = @_;
+    confess("First argument must be instance of TestRail::API") unless blessed($tr) eq 'TestRail::API';
+
+    my $project = $tr->getProjectByName($opts->{'project'});
+    confess "No such project '$opts->{project}'.\n" if !$project;
+
+    my $suite = $tr->getTestSuiteByName($project->{'id'},$opts->{'testsuite'});
+    confess "No such testsuite '$opts->{testsuite}'.\n" if !$suite;
+    $opts->{'testsuite_id'} = $suite->{'id'};
+
+    my $section;
+    $section = $tr->getSectionByName($project->{'id'},$suite->{'id'},$opts->{'section'}) if $opts->{'section'};
+    confess "No such section '$opts->{section}.\n" if $opts->{'section'} && !$section;
+
+    my $section_id;
+    $section_id = $section->{'id'} if ref $section eq "HASH";
+
+    my $type_ids;
+    @$type_ids = $tr->typeNamesToIds(@{$opts->{'types'}}) if ref $opts->{'types'} eq 'ARRAY';
+    #Above will confess if anything's the matter
+
+    #TODO Translate opts into filters
+    my $filters = {
+        'section_id' => $section_id,
+        'type_id'    => $type_ids
+    };
+
+    return $tr->getCases($project->{'id'},$suite->{'id'},$filters);
+}
+
+=head2 findCases(opts,@cases)
+
+Find orphan, missing and needing-update cases.
+They are returned as the hash keys 'orphans', 'missing', and 'updates' respectively.
+The testsuite_id is also returned in the output hashref.
+
+Option hash keys for input are 'no-missing', 'orphans', and 'update'.
+
+Returns HASHREF.
+
+=cut
+
+sub findCases {
+    my ($opts,@cases) = @_;
+
+    confess('testsuite_id parameter mandatory in options HASHREF') unless defined $opts->{'testsuite_id'};
+    confess('Directory parameter mandatory in options HASHREF.') unless defined $opts->{'directory'};
+    confess('No such directory "'.$opts->{'directory'}."\"\n") unless -d $opts->{'directory'};
+
+    my $ret = {'testsuite_id' => $opts->{'testsuite_id'}};
+    if (!$opts->{'no-missing'}) {
+        my $mopts = {
+            'no-match'   => $opts->{'directory'},
+            'names-only' => 1,
+            'extension'  => $opts->{'extension'}
+        };
+        my @missing = findTests($mopts,@cases);
+        $ret->{'missing'} = \@missing;
+    }
+    if ($opts->{'orphans'}) {
+        my $oopts = {
+            'orphans'    => $opts->{'directory'},
+            'extension'  => $opts->{'extension'}
+        };
+        my @orphans = findTests($oopts,@cases);
+        $ret->{'orphans'} = \@orphans;
+    }
+    if ($opts->{'update'}) {
+        my $uopts = {
+            'match'     => $opts->{'directory'},
+            'extension' => $opts->{'extension'}
+        };
+        my @updates = findTests($uopts,@cases);
+        $ret->{'update'} = \@updates;
+    }
+    return $ret;
+}
+
 1;
 
 __END__

+ 9 - 1
t/TestRail-API.t

@@ -7,7 +7,7 @@ use lib "$FindBin::Bin/lib";
 use TestRail::API;
 use Test::LWP::UserAgent::TestRailMock;
 
-use Test::More tests => 78;
+use Test::More tests => 81;
 use Test::Fatal;
 use Test::Deep;
 use Scalar::Util ();
@@ -62,6 +62,11 @@ isnt(exception {$tr->userNamesToIds(@user_names,'potzrebie'); }, undef, "Passing
 #Test CASE TYPE method
 my $caseTypes = $tr->getCaseTypes();
 is(ref($caseTypes),'ARRAY',"getCaseTypes returns ARRAY of case types");
+my @type_names = map {$_->{'name'}} @$caseTypes;
+my @type_ids = map {$_->{'id'}} @$caseTypes;
+is($tr->getCaseTypeByName($type_names[0])->{'id'},$type_ids[0],"Can get case type by name correctly");
+my @computed_type_ids = $tr->typeNamesToIds(@type_names);
+cmp_deeply(\@computed_type_ids,\@type_ids,"typeNamesToIds returns the correct type IDs in the correct order");
 
 #Test PROJECT methods
 my $project_name = 'CRUSH ALL HUMANS';
@@ -101,6 +106,9 @@ my $case_name = 'STROGGIFY POPULATION CENTERS';
 my $new_case = $tr->createCase($new_section->{'id'},$case_name);
 is($new_case->{'title'},$case_name,"Can create new test case");
 
+my $updated_case = $tr->updateCase($new_case->{'id'}, {'custom_preconds' => 'do some stuff'});
+is($updated_case->{'custom_preconds'},'do some stuff',"updateCase works");
+
 my $case_filters = {
     'section_id' => $new_section->{'id'}
 };

+ 72 - 1
t/TestRail-Utils-Find.t

@@ -4,7 +4,7 @@ use warnings;
 use FindBin;
 use lib "$FindBin::Bin/lib";
 
-use Test::More 'tests' => 28;
+use Test::More 'tests' => 52;
 use Test::Fatal;
 use Test::Deep;
 use File::Basename qw{dirname};
@@ -127,7 +127,19 @@ is(scalar(@tests),10,"Correct number of non-existant cases shown (no-match, name
 $opts->{'match'} = $FindBin::Bin;
 ($cases) = TestRail::Utils::Find::getTests($opts,$tr);
 isnt(exception {TestRail::Utils::Find::findTests($opts,@$cases)},undef,"match and no-match are mutually exclusive");
+
+delete $opts->{'no-match'};
+$opts->{'orphans'} = $FindBin::Bin;
+($cases) = TestRail::Utils::Find::getTests($opts,$tr);
+isnt(exception {TestRail::Utils::Find::findTests($opts,@$cases)},undef,"match and orphans are mutually exclusive");
+
+delete $opts->{'match'};
+$opts->{'no-match'} = $FindBin::Bin;
+($cases) = TestRail::Utils::Find::getTests($opts,$tr);
+isnt(exception {TestRail::Utils::Find::findTests($opts,@$cases)},undef,"orphans and no-match are mutually exclusive");
+delete $opts->{'orphans'};
 delete $opts->{'no-match'};
+$opts->{'match'} = $FindBin::Bin;
 
 delete $opts->{'plan'};
 $opts->{'run'} = 'TestingSuite';
@@ -161,3 +173,62 @@ delete $opts->{'match'};
 ($cases) = TestRail::Utils::Find::getTests($opts,$tr);
 @tests = TestRail::Utils::Find::findTests($opts,@$cases);
 is(scalar(@tests),0,"Correct number of cases shown (match, plan run, failed)");
+
+$opts = {
+    'project' => 'TestProject',
+    'testsuite' => 'HAMBURGER-IZE HUMANITY',
+    'directory' => $FindBin::Bin,
+    'extension' => '.test'
+};
+
+#Test getCases
+$cases = TestRail::Utils::Find::getCases($opts,$tr);
+is(scalar(@$cases),2,'Case search returns correct number of cases');
+
+#Test findCases
+$opts->{'no-missing'} = 1;
+my $output = TestRail::Utils::Find::findCases($opts,@$cases);
+is($output->{'testsuite_id'},9,'Correct testsuite_id returned by findCases');
+is($output->{'missing'},undef,'No missing cases returned');
+is($output->{'orphans'},undef,'No orphan cases returned');
+is($output->{'update'},undef,'No update cases returned');
+
+delete $opts->{'no-missing'};
+$output = TestRail::Utils::Find::findCases($opts,@$cases);
+is(scalar(@{$output->{'missing'}}),10,'Correct number of missing cases returned');
+is($output->{'orphans'},undef,'No orphan cases returned');
+is($output->{'update'},undef,'No update cases returned');
+
+$opts->{'no-missing'} = 1;
+$opts->{'orphans'} = 1;
+$output = TestRail::Utils::Find::findCases($opts,@$cases);
+is($output->{'missing'},undef,'No missing cases returned');
+is(scalar(@{$output->{'orphans'}}),1,'1 orphan case returned');
+is($output->{'orphans'}->[0]->{'title'},'nothere.test',"Correct orphan case return");
+is($output->{'update'},undef,'No update cases returned');
+
+delete $opts->{'orphans'};
+$opts->{'update'} = 1;
+$output = TestRail::Utils::Find::findCases($opts,@$cases);
+is($output->{'missing'},undef,'No missing cases returned');
+is($output->{'orphans'},undef,'No orphan cases returned');
+is(scalar(@{$output->{'update'}}),1,'1 update case returned');
+is($output->{'update'}->[0]->{'title'},'fake.test',"Correct update case return");
+
+delete $opts->{'no-missing'};
+$opts->{'orphans'} = 1;
+$output = TestRail::Utils::Find::findCases($opts,@$cases);
+is(scalar(@{$output->{'missing'}}),10,'Correct number of missing cases returned');
+is(scalar(@{$output->{'orphans'}}),1,'1 orphan case returned');
+is(scalar(@{$output->{'update'}}),1,'1 update case returned');
+
+delete $opts->{'testsuite_id'};
+like(exception {TestRail::Utils::Find::findCases($opts,@$cases)},qr/testsuite_id parameter mandatory/i,"No testsuite_id being passed results in error");
+$opts->{'testsuite_id'} = 9;
+
+delete $opts->{'directory'};
+like(exception {TestRail::Utils::Find::findCases($opts,@$cases)},qr/Directory parameter mandatory/i,"No directory being passed results in error");
+$opts->{'directory'} = 'bogoDir/';
+like(exception {TestRail::Utils::Find::findCases($opts,@$cases)},qr/No such directory/i,"Bad directory being passed results in error");
+
+#Test synchronize

+ 44 - 0
t/lib/Test/LWP/UserAgent/TestRailMock.pm

@@ -422,6 +422,34 @@ $mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4,
 
 {
 
+$VAR1 = 'index.php?/api/v2/get_cases/10&suite_id=9';
+$VAR2 = '200';
+$VAR3 = 'OK';
+$VAR4 = bless( {
+                 'connection' => 'close',
+                 'x-powered-by' => 'PHP/5.5.9-1ubuntu4.5',
+                 'client-response-num' => 1,
+                 'date' => 'Tue, 23 Dec 2014 20:02:09 GMT',
+                 'client-peer' => '192.168.122.217:80',
+                 'content-length' => '322',
+                 '::std_case' => {
+                                   'client-date' => 'Client-Date',
+                                   'x-powered-by' => 'X-Powered-By',
+                                   'client-response-num' => 'Client-Response-Num',
+                                   'client-peer' => 'Client-Peer'
+                                 },
+                 'client-date' => 'Tue, 23 Dec 2014 20:02:09 GMT',
+                 'content-type' => 'application/json; charset=utf-8',
+                 'server' => 'Apache/2.4.7 (Ubuntu)'
+               }, 'HTTP::Headers' );
+$VAR5 = '[{"id":8,"title":"fake.test","section_id":9,"type_id":6,"priority_id":4,"milestone_id":null,"refs":null,"created_by":1,"created_on":1419364929,"updated_by":1,"updated_on":1419364929,"estimate":null,"estimate_forecast":null,"suite_id":9,"custom_preconds":null,"custom_steps":null,"custom_expected":null},
+{"id":9,"title":"nothere.test","section_id":9,"type_id":6,"priority_id":4,"milestone_id":null,"refs":null,"created_by":1,"created_on":1419364929,"updated_by":1,"updated_on":1419364929,"estimate":null,"estimate_forecast":null,"suite_id":9,"custom_preconds":null,"custom_steps":null,"custom_expected":null}]';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
+{
+
 $VAR1 = 'index.php?/api/v2/get_cases/9&suite_id=9&section_id=9';
 $VAR2 = '200';
 $VAR3 = 'OK';
@@ -3080,4 +3108,20 @@ return $cloned;
 
 }
 
+{
+
+$VAR1 = 'index.php?/api/v2/update_case/8';
+$VAR2 = '200';
+$VAR3 = 'OK';
+$VAR4 = bless( {
+                 'client-date' => 'Sun, 30 Aug 2015 18:25:10 GMT',
+                 '::std_case' => {
+                                   'client-date' => 'Client-Date'
+                                 }
+               }, 'HTTP::Headers' );
+$VAR5 = '{"id":8,"title":"STROGGIFY POPULATION CENTERS","section_id":9,"type_id":6,"priority_id":4,"milestone_id":null,"refs":null,"created_by":1,"created_on":1419364929,"updated_by":1,"updated_on":1419364929,"estimate":null,"estimate_forecast":null,"suite_id":9,"custom_preconds":"do some stuff","custom_steps":null,"custom_expected":null}';
+$mockObject->map_response(qr/\Q$VAR1\E$/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));
+
+}
+
 1;

+ 27 - 0
t/testrail-cases.t

@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+use Test::More "tests" => 6;
+
+#check plan mode
+my @args = ($^X,qw{bin/testrail-cases -j "TestProject" -t "HAMBURGER-IZE HUMANITY" -d t/ --mock --test --extension .test});
+my $out = `@args`;
+is($? >> 8, 0, "Exit code OK running add, update, orphans");
+chomp $out;
+like($out,qr/fake\.test/,"Shows existing tests by default");
+
+@args = ($^X,qw{bin/testrail-cases -j 'TestProject' -t 'HAMBURGER-IZE HUMANITY' -d t/ --mock  -o --extension .test});
+$out = `@args`;
+chomp $out;
+like($out,qr/nothere\.test/,"Shows orphan tests");
+
+@args = ($^X,qw{bin/testrail-cases -j 'TestProject' -t 'HAMBURGER-IZE HUMANITY' -d t/ --mock  -m --extension .test});
+$out = `@args`;
+chomp $out;
+like($out,qr/t\/skipall\.test/,"Shows missing tests");
+
+#Verify no-match returns non path
+@args = ($^X,qw{bin/testrail-cases --help});
+$out = `@args`;
+is($? >> 8, 0, "Exit code OK asking for help");
+like($out,qr/encoding of arguments/i,"Help output OK");

+ 9 - 2
t/testrail-tests.t

@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use Test::More "tests" => 30;
+use Test::More "tests" => 32;
 
 #check plan mode
 my @args = ($^X,qw{bin/testrail-tests --apiurl http://testrail.local --user "test@fake.fake" --password "fake" -j TestProject -p "GosPlan" -r "Executing the great plan" -m t --config testConfig --mock --no-recurse});
@@ -80,9 +80,16 @@ is($out,"","Gets no tests correctly when filtering by wrong status");
 #Verify no-match returns non path
 @args = ($^X,qw{bin/testrail-tests --apiurl http://testrail.local --user "test@fake.fake" --password "fake" -j TestProject -r "TestingSuite" --mock});
 $out = `@args`;
+is($? >> 8, 0, "Exit code OK running no plan mode, no-match");
+chomp $out;
+like($out,qr/\nskipall\.test$/,"Gets test correctly in no plan mode, no-match");
+
+#Verify no-match returns non path
+@args = ($^X,qw{bin/testrail-tests --apiurl http://testrail.local --user "test@fake.fake" --password "fake" -j TestProject -r "TestingSuite" --orphans t/ --mock});
+$out = `@args`;
 is($? >> 8, 0, "Exit code OK running no plan mode, no recurse");
 chomp $out;
-like($out,qr/\nskipall\.test$/,"Gets test correctly in no plan mode, no recurse");
+like($out,qr/NOT SO SEARED AFTER ARR/,"Gets test correctly in orphan mode");
 
 #Verify no-match returns non path
 @args = ($^X,qw{bin/testrail-tests --help});