API.pm 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719
  1. # ABSTRACT: Provides an interface to TestRail's REST api via HTTP
  2. # PODNAME: TestRail::API
  3. package TestRail::API;
  4. =head1 SYNOPSIS
  5. use TestRail::API;
  6. my ($username,$password,$host) = ('foo','bar','testlink.baz.foo');
  7. my $tr = TestRail::API->new($username, $password, $host);
  8. =head1 DESCRIPTION
  9. C<TestRail::API> provides methods to access an existing TestRail account using API v2. You can then do things like look up tests, set statuses and create runs from lists of cases.
  10. It is by no means exhaustively implementing every TestRail API function.
  11. =head1 IMPORTANT
  12. All the methods aside from the constructor should not die, but return a false value upon failure.
  13. When the server is not responsive, expect a -500 response, and retry accordingly.
  14. I recommend using the excellent L<Attempt> module for this purpose.
  15. =cut
  16. use 5.010;
  17. use strict;
  18. use warnings;
  19. use Carp qw{cluck confess};
  20. use Scalar::Util qw{reftype looks_like_number};
  21. use Clone 'clone';
  22. use Try::Tiny;
  23. use JSON::XS;
  24. use HTTP::Request;
  25. use LWP::UserAgent;
  26. use Types::Serialiser; #Not necesarily shared by JSON::XS on all platforms
  27. use Data::Validate::URI qw{is_uri};
  28. =head1 CONSTRUCTOR
  29. =head2 B<new (api_url, user, password)>
  30. Creates new C<TestRail::API> object.
  31. =over 4
  32. =item STRING C<API URL> - base url for your TestRail api server.
  33. =item STRING C<USER> - Your TestRail User.
  34. =item STRING C<PASSWORD> - Your TestRail password.
  35. =item BOOLEAN C<DEBUG> - Print the JSON responses from TL with your requests.
  36. =back
  37. Returns C<TestRail::API> object if login is successful.
  38. my $tr = TestRail::API->new('http://tr.test/testrail', 'moo','M000000!');
  39. Dies on all communication errors with the TestRail server.
  40. Does not do above checks if debug is passed.
  41. =cut
  42. sub new {
  43. my ($class,$apiurl,$user,$pass,$debug) = @_;
  44. confess("Constructor must be called statically, not by an instance") if ref($class);
  45. confess("Invalid URI passed to constructor") if !is_uri($apiurl);
  46. $user //= $ENV{'TESTRAIL_USER'};
  47. $pass //= $ENV{'TESTRAIL_PASSWORD'};
  48. $debug //= 0;
  49. my $self = {
  50. user => $user,
  51. pass => $pass,
  52. apiurl => $apiurl,
  53. debug => $debug,
  54. testtree => [],
  55. flattree => [],
  56. user_cache => [],
  57. type_cache => [],
  58. tr_fields => undef,
  59. default_request => undef,
  60. browser => new LWP::UserAgent()
  61. };
  62. #Create default request to pass on to LWP::UserAgent
  63. $self->{'default_request'} = new HTTP::Request();
  64. $self->{'default_request'}->authorization_basic($user,$pass);
  65. bless( $self, $class );
  66. return $self if $self->debug; #For easy class testing without mocks
  67. #Manually do the get_users call to check HTTP status
  68. my $res = $self->_doRequest('index.php?/api/v2/get_users');
  69. confess "Error: network unreachable" if !defined($res);
  70. if ( (reftype($res) || 'undef') ne 'ARRAY') {
  71. confess "Unexpected return from _doRequest: $res" if !looks_like_number($res);
  72. confess "Could not communicate with TestRail Server! Check that your URI is correct, and your TestRail installation is functioning correctly." if $res == -500;
  73. confess "Could not list testRail users! Check that your TestRail installation has it's API enabled, and your credentials are correct" if $res == -403;
  74. confess "Bad user credentials!" if $res == -401;
  75. confess "HTTP error $res encountered while communicating with TestRail server. Resolve issue and try again." if !$res;
  76. confess "Unknown error occurred: $res";
  77. }
  78. confess "No users detected on TestRail Install! Check that your API is functioning correctly." if !scalar(@$res);
  79. $self->{'user_cache'} = $res;
  80. #Check that the User is actually in the list
  81. my $usr = $self->getUserByEmail($user);
  82. confess "Could not find your TestRail user on the system!" if !(reftype($usr) eq 'HASH');
  83. return $self;
  84. }
  85. =head1 GETTERS
  86. =head2 B<apiurl>
  87. =head2 B<debug>
  88. Accessors for these parameters you pass into the constructor, in case you forget.
  89. =cut
  90. sub apiurl {
  91. my $self = shift;
  92. confess("Object methods must be called by an instance") unless ref($self);
  93. return $self->{'apiurl'}
  94. }
  95. sub debug {
  96. my $self = shift;
  97. confess("Object methods must be called by an instance") unless ref($self);
  98. return $self->{'debug'};
  99. }
  100. #Convenient JSON-HTTP fetcher
  101. sub _doRequest {
  102. my ($self,$path,$method,$data) = @_;
  103. confess("Object methods must be called by an instance") unless ref($self);
  104. my $req = clone $self->{'default_request'};
  105. $method //= 'GET';
  106. $req->method($method);
  107. $req->url($self->apiurl.'/'.$path);
  108. warn "$method ".$self->apiurl."/$path" if $self->debug;
  109. #Data sent is JSON
  110. my $content = $data ? encode_json($data) : '';
  111. $req->content($content);
  112. $req->header( "Content-Type" => "application/json" );
  113. my $response = $self->{'browser'}->request($req);
  114. return $response if !defined($response); #worst case
  115. if ($response->code == 403) {
  116. cluck "ERROR: Access Denied.";
  117. return -403;
  118. }
  119. if ($response->code != 200) {
  120. cluck "ERROR: Arguments Bad: ".$response->content;
  121. return -int($response->code);
  122. }
  123. try {
  124. return decode_json($response->content);
  125. } catch {
  126. if ($response->code == 200 && !$response->content) {
  127. return 1; #This function probably just returns no data
  128. } else {
  129. cluck "ERROR: Malformed JSON returned by API.";
  130. cluck $@;
  131. if (!$self->debug) { #Otherwise we've already printed this, but we need to know if we encounter this
  132. cluck "RAW CONTENT:";
  133. cluck $response->content
  134. }
  135. return 0;
  136. }
  137. }
  138. }
  139. =head1 USER METHODS
  140. =head2 B<getUsers ()>
  141. Get all the user definitions for the provided Test Rail install.
  142. Returns ARRAYREF of user definition HASHREFs.
  143. =cut
  144. sub getUsers {
  145. my $self = shift;
  146. confess("Object methods must be called by an instance") unless ref($self);
  147. my $res = $self->_doRequest('index.php?/api/v2/get_users');
  148. return -500 if !$res || (reftype($res) || 'undef') ne 'ARRAY';
  149. $self->{'user_cache'} = $res;
  150. return $res;
  151. }
  152. =head2 B<getUserByID(id)>
  153. =cut
  154. =head2 B<getUserByName(name)>
  155. =cut
  156. =head2 B<getUserByEmail(email)>
  157. Get user definition hash by ID, Name or Email.
  158. Returns user def HASHREF.
  159. =cut
  160. #I'm just using the cache for the following methods because it's more straightforward and faster past 1 call.
  161. sub getUserByID {
  162. my ($self,$user) = @_;
  163. confess("Object methods must be called by an instance") unless ref($self);
  164. confess("User ID must be integer") unless $self->_checkInteger($user);
  165. $self->getUsers() if !defined($self->{'user_cache'});
  166. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  167. foreach my $usr (@{$self->{'user_cache'}}) {
  168. return $usr if $usr->{'id'} == $user;
  169. }
  170. return 0;
  171. }
  172. sub getUserByName {
  173. my ($self,$user) = @_;
  174. confess("Object methods must be called by an instance") unless ref($self);
  175. confess("User must be string") unless $self->_checkString($user);
  176. $self->getUsers() if !defined($self->{'user_cache'});
  177. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  178. foreach my $usr (@{$self->{'user_cache'}}) {
  179. return $usr if $usr->{'name'} eq $user;
  180. }
  181. return 0;
  182. }
  183. sub getUserByEmail {
  184. my ($self,$email) = @_;
  185. confess("Object methods must be called by an instance") unless ref($self);
  186. confess("Email must be string") unless $self->_checkString($email);
  187. $self->getUsers() if !defined($self->{'user_cache'});
  188. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  189. foreach my $usr (@{$self->{'user_cache'}}) {
  190. return $usr if $usr->{'email'} eq $email;
  191. }
  192. return 0;
  193. }
  194. =head1 PROJECT METHODS
  195. =head2 B<createProject (name, [description,send_announcement])>
  196. Creates new Project (Database of testsuites/tests).
  197. Optionally specify an announcement to go out to the users.
  198. Requires TestRail admin login.
  199. =over 4
  200. =item STRING C<NAME> - Desired name of project.
  201. =item STRING C<DESCRIPTION> (optional) - Description of project. Default value is 'res ipsa loquiter'.
  202. =item BOOLEAN C<SEND ANNOUNCEMENT> (optional) - Whether to confront users with an announcement about your awesome project on next login. Default false.
  203. =back
  204. Returns project definition HASHREF on success, false otherwise.
  205. $tl->createProject('Widgetronic 4000', 'Tests for the whiz-bang new product', true);
  206. =cut
  207. sub createProject {
  208. my ($self,$name,$desc,$announce) = @_;
  209. confess("Object methods must be called by an instance") unless ref($self);
  210. confess("Project name must be string") unless $self->_checkString($name);
  211. $desc //= 'res ipsa loquiter';
  212. $announce //= 0;
  213. confess("Project description must be string") unless $self->_checkString($desc);
  214. confess("Announce must be integer") unless $self->_checkInteger($announce);
  215. my $input = {
  216. name => $name,
  217. announcement => $desc,
  218. show_announcement => $announce ? Types::Serialiser::true : Types::Serialiser::false
  219. };
  220. my $result = $self->_doRequest('index.php?/api/v2/add_project','POST',$input);
  221. return $result;
  222. }
  223. =head2 B<deleteProject (id)>
  224. Deletes specified project by ID.
  225. Requires TestRail admin login.
  226. =over 4
  227. =item STRING C<NAME> - Desired name of project.
  228. =back
  229. Returns BOOLEAN.
  230. $success = $tl->deleteProject(1);
  231. =cut
  232. sub deleteProject {
  233. my ($self,$proj) = @_;
  234. confess("Object methods must be called by an instance") unless ref($self);
  235. confess("Project ID must be integer") unless $self->_checkInteger($proj);
  236. my $result = $self->_doRequest('index.php?/api/v2/delete_project/'.$proj,'POST');
  237. return $result;
  238. }
  239. =head2 B<getProjects ()>
  240. Get all available projects
  241. Returns array of project definition HASHREFs, false otherwise.
  242. $projects = $tl->getProjects;
  243. =cut
  244. sub getProjects {
  245. my $self = shift;
  246. confess("Object methods must be called by an instance") unless ref($self);
  247. my $result = $self->_doRequest('index.php?/api/v2/get_projects');
  248. #Save state for future use, if needed
  249. return -500 if !$result || (reftype($result) || 'undef') ne 'ARRAY';
  250. $self->{'testtree'} = $result;
  251. #Note that it's a project for future reference by recursive tree search
  252. return -500 if !$result || (reftype($result) || 'undef') ne 'ARRAY';
  253. foreach my $pj (@{$result}) {
  254. $pj->{'type'} = 'project';
  255. }
  256. return $result;
  257. }
  258. =head2 B<getProjectByName ($project)>
  259. Gets some project definition hash by it's name
  260. =over 4
  261. =item STRING C<PROJECT> - desired project
  262. =back
  263. Returns desired project def HASHREF, false otherwise.
  264. $project = $tl->getProjectByName('FunProject');
  265. =cut
  266. sub getProjectByName {
  267. my ($self,$project) = @_;
  268. confess("Object methods must be called by an instance") unless ref($self);
  269. confess("Project must be string.") unless $self->_checkString($project);
  270. #See if we already have the project list...
  271. my $projects = $self->{'testtree'};
  272. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  273. $projects = $self->getProjects() unless scalar(@$projects);
  274. #Search project list for project
  275. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  276. for my $candidate (@$projects) {
  277. return $candidate if ($candidate->{'name'} eq $project);
  278. }
  279. return 0;
  280. }
  281. =head2 B<getProjectByID ($project)>
  282. Gets some project definition hash by it's ID
  283. =over 4
  284. =item INTEGER C<PROJECT> - desired project
  285. =back
  286. Returns desired project def HASHREF, false otherwise.
  287. $projects = $tl->getProjectByID(222);
  288. =cut
  289. sub getProjectByID {
  290. my ($self,$project) = @_;
  291. confess("Object methods must be called by an instance") unless ref($self);
  292. confess("No project provided.") unless $project;
  293. confess("Project ID must be integer") unless $self->_checkInteger($project);
  294. #See if we already have the project list...
  295. my $projects = $self->{'testtree'};
  296. $projects = $self->getProjects() unless scalar(@$projects);
  297. #Search project list for project
  298. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  299. for my $candidate (@$projects) {
  300. return $candidate if ($candidate->{'id'} eq $project);
  301. }
  302. return 0;
  303. }
  304. =head1 TESTSUITE METHODS
  305. =head2 B<createTestSuite (project_id, name, [description])>
  306. Creates new TestSuite (folder of tests) in the database of test specifications under given project id having given name and details.
  307. =over 4
  308. =item INTEGER C<PROJECT ID> - ID of project this test suite should be under.
  309. =item STRING C<NAME> - Desired name of test suite.
  310. =item STRING C<DESCRIPTION> (optional) - Description of test suite. Default value is 'res ipsa loquiter'.
  311. =back
  312. Returns TS definition HASHREF on success, false otherwise.
  313. $tl->createTestSuite(1, 'broken tests', 'Tests that should be reviewed');
  314. =cut
  315. sub createTestSuite {
  316. my ($self,$project_id,$name,$details) = @_;
  317. confess("Object methods must be called by an instance") unless ref($self);
  318. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  319. confess("Name must be a string") unless $self->_checkString($name);
  320. $details ||= 'res ipsa loquiter';
  321. confess("Project details must be a string") unless $self->_checkString($details);
  322. my $input = {
  323. name => $name,
  324. description => $details
  325. };
  326. my $result = $self->_doRequest('index.php?/api/v2/add_suite/'.$project_id,'POST',$input);
  327. return $result;
  328. }
  329. =head2 B<deleteTestSuite (suite_id)>
  330. Deletes specified testsuite.
  331. =over 4
  332. =item INTEGER C<SUITE ID> - ID of testsuite to delete.
  333. =back
  334. Returns BOOLEAN.
  335. $tl->deleteTestSuite(1);
  336. =cut
  337. sub deleteTestSuite {
  338. my ($self,$suite_id) = @_;
  339. confess("Object methods must be called by an instance") unless ref($self);
  340. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  341. my $result = $self->_doRequest('index.php?/api/v2/delete_suite/'.$suite_id,'POST');
  342. return $result;
  343. }
  344. =head2 B<getTestSuites (project_id)>
  345. Gets the testsuites for a project
  346. =over 4
  347. =item STRING C<PROJECT ID> - desired project's ID
  348. =back
  349. Returns ARRAYREF of testsuite definition HASHREFs, 0 on error.
  350. $suites = $tl->getTestSuites(123);
  351. =cut
  352. sub getTestSuites {
  353. my ($self,$proj) = @_;
  354. confess("Object methods must be called by an instance") unless ref($self);
  355. confess("Project ID must be integer") unless $self->_checkInteger($proj);
  356. return $self->_doRequest('index.php?/api/v2/get_suites/'.$proj);
  357. }
  358. =head2 B<getTestSuiteByName (project_id,testsuite_name)>
  359. Gets the testsuite that matches the given name inside of given project.
  360. =over 4
  361. =item STRING C<PROJECT ID> - ID of project holding this testsuite
  362. =item STRING C<TESTSUITE NAME> - desired parent testsuite name
  363. =back
  364. Returns desired testsuite definition HASHREF, false otherwise.
  365. $suites = $tl->getTestSuitesByName(321, 'hugSuite');
  366. =cut
  367. sub getTestSuiteByName {
  368. my ($self,$project_id,$testsuite_name) = @_;
  369. confess("Object methods must be called by an instance") unless ref($self);
  370. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  371. confess("Testsuite Name must be String") unless $self->_checkString($testsuite_name);
  372. #TODO cache
  373. my $suites = $self->getTestSuites($project_id);
  374. return -500 if !$suites || (reftype($suites) || 'undef') ne 'ARRAY'; #No suites for project, or no project
  375. foreach my $suite (@$suites) {
  376. return $suite if $suite->{'name'} eq $testsuite_name;
  377. }
  378. return 0; #Couldn't find it
  379. }
  380. =head2 B<getTestSuiteByID (testsuite_id)>
  381. Gets the testsuite with the given ID.
  382. =over 4
  383. =item STRING C<TESTSUITE_ID> - TestSuite ID.
  384. =back
  385. Returns desired testsuite definition HASHREF, false otherwise.
  386. $tests = $tl->getTestSuiteByID(123);
  387. =cut
  388. sub getTestSuiteByID {
  389. my ($self,$testsuite_id) = @_;
  390. confess("Object methods must be called by an instance") unless ref($self);
  391. confess("Testsuite ID must be integer") unless $self->_checkInteger($testsuite_id);
  392. my $result = $self->_doRequest('index.php?/api/v2/get_suite/'.$testsuite_id);
  393. return $result;
  394. }
  395. =head1 SECTION METHODS
  396. =head2 B<createSection(project_id,suite_id,name,[parent_id])>
  397. Creates a section.
  398. =over 4
  399. =item INTEGER C<PROJECT ID> - Parent Project ID.
  400. =item INTEGER C<SUITE ID> - Parent TestSuite ID.
  401. =item STRING C<NAME> - desired section name.
  402. =item INTEGER C<PARENT ID> (optional) - parent section id
  403. =back
  404. Returns new section definition HASHREF, false otherwise.
  405. $section = $tr->createSection(1,1,'nugs',1);
  406. =cut
  407. sub createSection {
  408. my ($self,$project_id,$suite_id,$name,$parent_id) = @_;
  409. confess("Object methods must be called by an instance") unless ref($self);
  410. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  411. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  412. confess("Section name must be string") unless $self->_checkString($name);
  413. confess("Parent section ID must be integer") unless !defined($parent_id) || $self->_checkInteger($parent_id);
  414. my $input = {
  415. name => $name,
  416. suite_id => $suite_id
  417. };
  418. $input->{'parent_id'} = $parent_id if $parent_id;
  419. my $result = $self->_doRequest('index.php?/api/v2/add_section/'.$project_id,'POST',$input);
  420. return $result;
  421. }
  422. =head2 B<deleteSection (section_id)>
  423. Deletes specified section.
  424. =over 4
  425. =item INTEGER C<SECTION ID> - ID of section to delete.
  426. =back
  427. Returns BOOLEAN.
  428. $tr->deleteSection(1);
  429. =cut
  430. sub deleteSection {
  431. my ($self,$section_id) = @_;
  432. confess("Object methods must be called by an instance") unless ref($self);
  433. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  434. my $result = $self->_doRequest('index.php?/api/v2/delete_section/'.$section_id,'POST');
  435. return $result;
  436. }
  437. =head2 B<getSections (project_id,suite_id)>
  438. Gets sections for a given project and suite.
  439. =over 4
  440. =item INTEGER C<PROJECT ID> - ID of parent project.
  441. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  442. =back
  443. Returns ARRAYREF of section definition HASHREFs.
  444. $tr->getSections(1,2);
  445. =cut
  446. sub getSections {
  447. my ($self,$project_id,$suite_id) = @_;
  448. confess("Object methods must be called by an instance") unless ref($self);
  449. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  450. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  451. return $self->_doRequest("index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
  452. }
  453. =head2 B<getSectionByID (section_id)>
  454. Gets desired section.
  455. =over 4
  456. =item INTEGER C<PROJECT ID> - ID of parent project.
  457. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  458. =back
  459. Returns section definition HASHREF.
  460. $tr->getSectionByID(344);
  461. =cut
  462. sub getSectionByID {
  463. my ($self,$section_id) = @_;
  464. confess("Object methods must be called by an instance") unless ref($self);
  465. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  466. return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
  467. }
  468. =head2 B<getSectionByName (project_id,suite_id,name)>
  469. Gets desired section.
  470. =over 4
  471. =item INTEGER C<PROJECT ID> - ID of parent project.
  472. =item INTEGER C<SUITE ID> - ID of suite to get section for.
  473. =item STRING C<NAME> - name of section to get
  474. =back
  475. Returns section definition HASHREF.
  476. $tr->getSectionByName(1,2,'nugs');
  477. =cut
  478. sub getSectionByName {
  479. my ($self,$project_id,$suite_id,$section_name) = @_;
  480. confess("Object methods must be called by an instance") unless ref($self);
  481. confess("Project ID must be an integer") unless $self->_checkInteger($project_id);
  482. confess("Suite ID must be an integer") unless $self->_checkInteger($suite_id);
  483. confess("Section Name must be a string") unless $self->_checkString($section_name);
  484. my $sections = $self->getSections($project_id,$suite_id);
  485. return -500 if !$sections || (reftype($sections) || 'undef') ne 'ARRAY';
  486. foreach my $sec (@$sections) {
  487. return $sec if $sec->{'name'} eq $section_name;
  488. }
  489. return 0;
  490. }
  491. =head1 CASE METHODS
  492. =head2 B<getCaseTypes ()>
  493. Gets possible case types.
  494. Returns ARRAYREF of case type definition HASHREFs.
  495. $tr->getCaseTypes();
  496. =cut
  497. sub getCaseTypes {
  498. my $self = shift;
  499. confess("Object methods must be called by an instance") unless ref($self);
  500. my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
  501. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  502. $self->{'type_cache'} = $types if !$self->{'type_cache'}; #We can't change this with API, so assume it is static
  503. return $self->{'type_cache'};
  504. }
  505. =head2 B<getCaseTypeByName (name)>
  506. Gets case type by name.
  507. =over 4
  508. =item STRING C<NAME> - Name of desired case type
  509. =back
  510. Returns case type definition HASHREF.
  511. $tr->getCaseTypeByName();
  512. =cut
  513. sub getCaseTypeByName {
  514. #Useful for marking automated tests, etc
  515. my ($self,$name) = @_;
  516. confess("Object methods must be called by an instance") unless ref($self);
  517. confess("Case type must be string") unless $self->_checkString($name);
  518. my $types = $self->getCaseTypes();
  519. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  520. foreach my $type (@$types) {
  521. return $type if $type->{'name'} eq $name;
  522. }
  523. return 0;
  524. }
  525. =head2 B<createCase(section_id,title,type_id,options,extra_options)>
  526. Creates a test case.
  527. =over 4
  528. =item INTEGER C<SECTION ID> - Parent Project ID.
  529. =item STRING C<TITLE> - Case title.
  530. =item INTEGER C<TYPE_ID> (optional) - desired test type's ID. Defaults to whatever your TR install considers the default type.
  531. =item HASHREF C<OPTIONS> (optional) - Custom fields in the case are the keys, set to the values provided. See TestRail API documentation for more info.
  532. =item HASHREF C<EXTRA OPTIONS> (optional) - contains priority_id, estimate, milestone_id and refs as possible keys. See TestRail API documentation for more info.
  533. =back
  534. Returns new case definition HASHREF, false otherwise.
  535. $custom_opts = {
  536. preconds => "Test harness installed",
  537. steps => "Do the needful",
  538. expected => "cubicle environment transforms into Dali painting"
  539. };
  540. $other_opts = {
  541. priority_id => 4,
  542. milestone_id => 666,
  543. estimate => '2m 45s',
  544. refs => ['TRACE-22','ON-166'] #ARRAYREF of bug IDs.
  545. }
  546. $case = $tr->createCase(1,'Do some stuff',3,$custom_opts,$other_opts);
  547. =cut
  548. sub createCase {
  549. my ($self,$section_id,$title,$type_id,$opts,$extras) = @_;
  550. confess("Object methods must be called by an instance") unless ref($self);
  551. confess("Section ID ($section_id) must be integer") unless $self->_checkInteger($section_id);
  552. confess("title must be string") unless $self->_checkString($title);
  553. confess("Type ID must be integer") unless !defined($type_id) || $self->_checkInteger($type_id);
  554. confess("Options must be HASHREF") unless !defined($opts) || (reftype($opts) || 'undef') ne 'HASH';
  555. confess("Extras must be HASHREF") unless !defined($extras) || (reftype($extras) || 'undef') ne 'HASH';
  556. my $stuff = {
  557. title => $title,
  558. type_id => $type_id
  559. };
  560. #Handle sort of optional but baked in options
  561. if (defined($extras) && reftype($extras) eq 'HASH') {
  562. $stuff->{'priority_id'} = $extras->{'priority_id'} if defined($extras->{'priority_id'});
  563. $stuff->{'estimate'} = $extras->{'estimate'} if defined($extras->{'estimate'});
  564. $stuff->{'milestone_id'} = $extras->{'milestone_id'} if defined($extras->{'milestone_id'});
  565. $stuff->{'refs'} = join(',',@{$extras->{'refs'}}) if defined($extras->{'refs'});
  566. }
  567. #Handle custom fields
  568. if (defined($opts) && reftype($opts) eq 'HASH') {
  569. foreach my $key (keys(%$opts)) {
  570. $stuff->{"custom_$key"} = $opts->{$key};
  571. }
  572. }
  573. my $result = $self->_doRequest("index.php?/api/v2/add_case/$section_id",'POST',$stuff);
  574. return $result;
  575. }
  576. =head2 B<deleteCase (case_id)>
  577. Deletes specified section.
  578. =over 4
  579. =item INTEGER C<CASE ID> - ID of case to delete.
  580. =back
  581. Returns BOOLEAN.
  582. $tr->deleteCase(1324);
  583. =cut
  584. sub deleteCase {
  585. my ($self,$case_id) = @_;
  586. confess("Object methods must be called by an instance") unless ref($self);
  587. confess("Case ID must be integer") unless $self->_checkInteger($case_id);
  588. my $result = $self->_doRequest("index.php?/api/v2/delete_case/$case_id",'POST');
  589. return $result;
  590. }
  591. =head2 B<getCases (project_id,suite_id,section_id)>
  592. Gets cases for provided section.
  593. =over 4
  594. =item INTEGER C<PROJECT ID> - ID of parent project.
  595. =item INTEGER C<SUITE ID> - ID of parent suite.
  596. =item INTEGER C<SECTION ID> - ID of parent section
  597. =back
  598. Returns ARRAYREF of test case definition HASHREFs.
  599. $tr->getCases(1,2,3);
  600. =cut
  601. sub getCases {
  602. my ($self,$project_id,$suite_id,$section_id) = @_;
  603. confess("Object methods must be called by an instance") unless ref($self);
  604. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  605. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  606. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  607. my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
  608. $url .= "&section_id=$section_id" if $section_id;
  609. return $self->_doRequest($url);
  610. }
  611. =head2 B<getCaseByName (project_id,suite_id,section_id,name)>
  612. Gets case by name.
  613. =over 4
  614. =item INTEGER C<PROJECT ID> - ID of parent project.
  615. =item INTEGER C<SUITE ID> - ID of parent suite.
  616. =item INTEGER C<SECTION ID> - ID of parent section.
  617. =item STRING <NAME> - Name of desired test case.
  618. =back
  619. Returns test case definition HASHREF.
  620. $tr->getCaseByName(1,2,3,'nugs');
  621. =cut
  622. sub getCaseByName {
  623. my ($self,$project_id,$suite_id,$section_id,$name) = @_;
  624. confess("Object methods must be called by an instance") unless ref($self);
  625. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  626. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  627. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  628. confess("Test Case name must be string") unless $self->_checkString($name);
  629. my $cases = $self->getCases($project_id,$suite_id,$section_id);
  630. return -500 if !$cases || (reftype($cases) || 'undef') ne 'ARRAY';
  631. foreach my $case (@$cases) {
  632. return $case if $case->{'title'} eq $name;
  633. }
  634. return 0;
  635. }
  636. =head2 B<getCaseByID (case_id)>
  637. Gets case by ID.
  638. =over 4
  639. =item INTEGER C<CASE ID> - ID of case.
  640. =back
  641. Returns test case definition HASHREF.
  642. $tr->getCaseByID(1345);
  643. =cut
  644. sub getCaseByID {
  645. my ($self,$case_id) = @_;
  646. confess("Object methods must be called by an instance") unless ref($self);
  647. confess("Case ID must be integer") unless $self->_checkInteger($case_id);
  648. return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
  649. }
  650. =head1 RUN METHODS
  651. =head2 B<createRun (project_id,suite_id,name,description,milestone_id,assigned_to_id,case_ids)>
  652. Create a run.
  653. =over 4
  654. =item INTEGER C<PROJECT ID> - ID of parent project.
  655. =item INTEGER C<SUITE ID> - ID of suite to base run on
  656. =item STRING C<NAME> - Name of run
  657. =item STRING C<DESCRIPTION> (optional) - Description of run
  658. =item INTEGER C<MILESTONE ID> (optional) - ID of milestone
  659. =item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
  660. =item ARRAYREF C<CASE IDS> (optional) - Array of case IDs in case you don't want to use the whole testsuite when making the build.
  661. =back
  662. Returns run definition HASHREF.
  663. $tr->createRun(1,1345,'RUN AWAY','SO FAR AWAY',22,3,[3,4,5,6]);
  664. =cut
  665. #If you pass an array of case ids, it implies include_all is false
  666. sub createRun {
  667. my ($self,$project_id,$suite_id,$name,$desc,$milestone_id,$assignedto_id,$case_ids) = @_;
  668. confess("Object methods must be called by an instance") unless ref($self);
  669. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  670. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  671. confess("Name must be string") unless $self->_checkString($name);
  672. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  673. confess("Milestone ID must be integer") unless !defined($milestone_id) || $self->_checkInteger($milestone_id);
  674. confess("Assigned To ID must be integer") unless !defined($assignedto_id) || $self->_checkInteger($assignedto_id);
  675. confess("Case IDs must be ARRAYREF") unless !defined($case_ids) || (reftype($case_ids) || 'undef') eq 'ARRAY';
  676. my $stuff = {
  677. suite_id => $suite_id,
  678. name => $name,
  679. description => $desc,
  680. milestone_id => $milestone_id,
  681. assignedto_id => $assignedto_id,
  682. include_all => $case_ids ? Types::Serialiser::false : Types::Serialiser::true,
  683. case_ids => $case_ids
  684. };
  685. my $result = $self->_doRequest("index.php?/api/v2/add_run/$project_id",'POST',$stuff);
  686. return $result;
  687. }
  688. =head2 B<deleteRun (run_id)>
  689. Deletes specified run.
  690. =over 4
  691. =item INTEGER C<RUN ID> - ID of run to delete.
  692. =back
  693. Returns BOOLEAN.
  694. $tr->deleteRun(1324);
  695. =cut
  696. sub deleteRun {
  697. my ($self,$run_id) = @_;
  698. confess("Object methods must be called by an instance") unless ref($self);
  699. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  700. my $result = $self->_doRequest("index.php?/api/v2/delete_run/$run_id",'POST');
  701. return $result;
  702. }
  703. =head2 B<getRuns (project_id)>
  704. Get all runs for specified project.
  705. =over 4
  706. =item INTEGER C<PROJECT_ID> - ID of parent project
  707. =back
  708. Returns ARRAYREF of run definition HASHREFs.
  709. $allRuns = $tr->getRuns(6969);
  710. =cut
  711. sub getRuns {
  712. my ($self,$project_id) = @_;
  713. confess("Object methods must be called by an instance") unless ref($self);
  714. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  715. return $self->_doRequest("index.php?/api/v2/get_runs/$project_id");
  716. }
  717. =head2 B<getRunByName (project_id,name)>
  718. Gets run by name.
  719. =over 4
  720. =item INTEGER C<PROJECT ID> - ID of parent project.
  721. =item STRING <NAME> - Name of desired run.
  722. =back
  723. Returns run definition HASHREF.
  724. $tr->getRunByName(1,'R2');
  725. =cut
  726. sub getRunByName {
  727. my ($self,$project_id,$name) = @_;
  728. confess("Object methods must be called by an instance") unless ref($self);
  729. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  730. confess("Run name must be string") unless $self->_checkString($name);
  731. my $runs = $self->getRuns($project_id);
  732. return -500 if !$runs || (reftype($runs) || 'undef') ne 'ARRAY';
  733. foreach my $run (@$runs) {
  734. return $run if $run->{'name'} eq $name;
  735. }
  736. return 0;
  737. }
  738. =head2 B<getRunByID (run_id)>
  739. Gets run by ID.
  740. =over 4
  741. =item INTEGER C<RUN ID> - ID of desired run.
  742. =back
  743. Returns run definition HASHREF.
  744. $tr->getRunByID(7779311);
  745. =cut
  746. sub getRunByID {
  747. my ($self,$run_id) = @_;
  748. confess("Object methods must be called by an instance") unless ref($self);
  749. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  750. return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
  751. }
  752. =head1 PLAN METHODS
  753. =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
  754. Create a test plan.
  755. =over 4
  756. =item INTEGER C<PROJECT ID> - ID of parent project.
  757. =item STRING C<NAME> - Name of plan
  758. =item STRING C<DESCRIPTION> (optional) - Description of plan
  759. =item INTEGER C<MILESTONE_ID> (optional) - ID of milestone
  760. =item ARRAYREF C<ENTRIES> (optional) - New Runs to initially populate the plan with -- See TestRail API documentation for more advanced inputs here.
  761. =back
  762. Returns test plan definition HASHREF, or false on failure.
  763. $entries = {
  764. suite_id => 345,
  765. include_all => Types::Serialiser::true,
  766. assignedto_id => 1
  767. }
  768. $tr->createPlan(1,'Gosplan','Robo-Signed Soviet 5-year plan',22,$entries);
  769. =cut
  770. sub createPlan {
  771. my ($self,$project_id,$name,$desc,$milestone_id,$entries) = @_;
  772. confess("Object methods must be called by an instance") unless ref($self);
  773. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  774. confess("Plan name must be string") unless $self->_checkString($name);
  775. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  776. confess("Milestone ID must be integer") unless !defined($milestone_id) || $self->_checkInteger($milestone_id);
  777. confess("Entries must be ARRAYREF") unless !defined($entries) || (reftype($entries) || 'undef') eq 'ARRAY';
  778. my $stuff = {
  779. name => $name,
  780. description => $desc,
  781. milestone_id => $milestone_id,
  782. entries => $entries
  783. };
  784. my $result = $self->_doRequest("index.php?/api/v2/add_plan/$project_id",'POST',$stuff);
  785. return $result;
  786. }
  787. =head2 B<deletePlan (plan_id)>
  788. Deletes specified plan.
  789. =over 4
  790. =item INTEGER C<PLAN ID> - ID of plan to delete.
  791. =back
  792. Returns BOOLEAN.
  793. $tr->deletePlan(8675309);
  794. =cut
  795. sub deletePlan {
  796. my ($self,$plan_id) = @_;
  797. confess("Object methods must be called by an instance") unless ref($self);
  798. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  799. my $result = $self->_doRequest("index.php?/api/v2/delete_plan/$plan_id",'POST');
  800. return $result;
  801. }
  802. =head2 B<getPlans (project_id)>
  803. Deletes specified plan.
  804. =over 4
  805. =item INTEGER C<PROJECT ID> - ID of parent project.
  806. =back
  807. Returns ARRAYREF of plan definition HASHREFs.
  808. $tr->getPlans(8);
  809. =cut
  810. sub getPlans {
  811. my ($self,$project_id) = @_;
  812. confess("Object methods must be called by an instance") unless ref($self);
  813. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  814. return $self->_doRequest("index.php?/api/v2/get_plans/$project_id");
  815. }
  816. =head2 B<getPlanByName (project_id,name)>
  817. Gets specified plan by name.
  818. =over 4
  819. =item INTEGER C<PROJECT ID> - ID of parent project.
  820. =item STRING C<NAME> - Name of test plan.
  821. =back
  822. Returns plan definition HASHREF.
  823. $tr->getPlanByName(8,'GosPlan');
  824. =cut
  825. sub getPlanByName {
  826. my ($self,$project_id,$name) = @_;
  827. confess("Object methods must be called by an instance") unless ref($self);
  828. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  829. confess("Plan name must be string") unless $self->_checkString($name);
  830. my $plans = $self->getPlans($project_id);
  831. return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
  832. foreach my $plan (@$plans) {
  833. return $plan if $plan->{'name'} eq $name;
  834. }
  835. return 0;
  836. }
  837. =head2 B<getPlanByID (plan_id)>
  838. Gets specified plan by ID.
  839. =over 4
  840. =item INTEGER C<PLAN ID> - ID of plan.
  841. =back
  842. Returns plan definition HASHREF.
  843. $tr->getPlanByID(2);
  844. =cut
  845. sub getPlanByID {
  846. my ($self,$plan_id) = @_;
  847. confess("Object methods must be called by an instance") unless ref($self);
  848. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  849. return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
  850. }
  851. =head1 MILESTONE METHODS
  852. =head2 B<createMilestone (project_id,name,description,due_on)>
  853. Create a milestone.
  854. =over 4
  855. =item INTEGER C<PROJECT ID> - ID of parent project.
  856. =item STRING C<NAME> - Name of milestone
  857. =item STRING C<DESCRIPTION> (optional) - Description of milestone
  858. =item INTEGER C<DUE_ON> - Date at which milestone should be completed. Unix Timestamp.
  859. =back
  860. Returns milestone definition HASHREF, or false on failure.
  861. $tr->createMilestone(1,'Patriotic victory of world perlism','Accomplish by Robo-Signed Soviet 5-year plan',time()+157788000);
  862. =cut
  863. sub createMilestone {
  864. my ($self,$project_id,$name,$desc,$due_on) = @_;
  865. confess("Object methods must be called by an instance") unless ref($self);
  866. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  867. confess("Name must be string") unless $self->_checkString($name);
  868. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  869. confess("Due on must be unix time stamp (integer)") unless !defined($due_on) || $self->_checkInteger($due_on);
  870. my $stuff = {
  871. name => $name,
  872. description => $desc,
  873. due_on => $due_on # unix timestamp
  874. };
  875. my $result = $self->_doRequest("index.php?/api/v2/add_milestone/$project_id",'POST',$stuff);
  876. return $result;
  877. }
  878. =head2 B<deleteMilestone (milestone_id)>
  879. Deletes specified milestone.
  880. =over 4
  881. =item INTEGER C<MILESTONE ID> - ID of milestone to delete.
  882. =back
  883. Returns BOOLEAN.
  884. $tr->deleteMilestone(86);
  885. =cut
  886. sub deleteMilestone {
  887. my ($self,$milestone_id) = @_;
  888. confess("Object methods must be called by an instance") unless ref($self);
  889. confess("Milestone ID must be integer") unless $self->_checkInteger($milestone_id);
  890. my $result = $self->_doRequest("index.php?/api/v2/delete_milestone/$milestone_id",'POST');
  891. return $result;
  892. }
  893. =head2 B<getMilestones (project_id)>
  894. Get milestones for some project.
  895. =over 4
  896. =item INTEGER C<PROJECT ID> - ID of parent project.
  897. =back
  898. Returns ARRAYREF of milestone definition HASHREFs.
  899. $tr->getMilestones(8);
  900. =cut
  901. sub getMilestones {
  902. my ($self,$project_id) = @_;
  903. confess("Object methods must be called by an instance") unless ref($self);
  904. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  905. return $self->_doRequest("index.php?/api/v2/get_milestones/$project_id");
  906. }
  907. =head2 B<getMilestoneByName (project_id,name)>
  908. Gets specified milestone by name.
  909. =over 4
  910. =item INTEGER C<PROJECT ID> - ID of parent project.
  911. =item STRING C<NAME> - Name of milestone.
  912. =back
  913. Returns milestone definition HASHREF.
  914. $tr->getMilestoneByName(8,'whee');
  915. =cut
  916. sub getMilestoneByName {
  917. my ($self,$project_id,$name) = @_;
  918. confess("Object methods must be called by an instance") unless ref($self);
  919. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  920. confess("Milestone name must be string") unless $self->_checkString($name);
  921. my $milestones = $self->getMilestones($project_id);
  922. return -500 if !$milestones || (reftype($milestones) || 'undef') ne 'ARRAY';
  923. foreach my $milestone (@$milestones) {
  924. return $milestone if $milestone->{'name'} eq $name;
  925. }
  926. return 0;
  927. }
  928. =head2 B<getMilestoneByID (milestone_id)>
  929. Gets specified milestone by ID.
  930. =over 4
  931. =item INTEGER C<MILESTONE ID> - ID of milestone.
  932. =back
  933. Returns milestone definition HASHREF.
  934. $tr->getMilestoneByID(2);
  935. =cut
  936. sub getMilestoneByID {
  937. my ($self,$milestone_id) = @_;
  938. confess("Object methods must be called by an instance") unless ref($self);
  939. confess("Milestone ID must be integer") unless $self->_checkInteger($milestone_id);
  940. return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
  941. }
  942. =head1 TEST METHODS
  943. =head2 B<getTests (run_id)>
  944. Get tests for some run.
  945. =over 4
  946. =item INTEGER C<RUN ID> - ID of parent run.
  947. =back
  948. Returns ARRAYREF of test definition HASHREFs.
  949. $tr->getTests(8);
  950. =cut
  951. sub getTests {
  952. my ($self,$run_id) = @_;
  953. confess("Object methods must be called by an instance") unless ref($self);
  954. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  955. return $self->_doRequest("index.php?/api/v2/get_tests/$run_id");
  956. }
  957. =head2 B<getTestByName (run_id,name)>
  958. Gets specified test by name.
  959. =over 4
  960. =item INTEGER C<RUN ID> - ID of parent run.
  961. =item STRING C<NAME> - Name of milestone.
  962. =back
  963. Returns test definition HASHREF.
  964. $tr->getTestByName(36,'wheeTest');
  965. =cut
  966. sub getTestByName {
  967. my ($self,$run_id,$name) = @_;
  968. confess("Object methods must be called by an instance") unless ref($self);
  969. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  970. confess("Test name must be string") unless $self->_checkString($name);
  971. my $tests = $self->getTests($run_id);
  972. return -500 if !$tests || (reftype($tests) || 'undef') ne 'ARRAY';
  973. foreach my $test (@$tests) {
  974. return $test if $test->{'title'} eq $name;
  975. }
  976. return 0;
  977. }
  978. =head2 B<getTestByID (test_id)>
  979. Gets specified test by ID.
  980. =over 4
  981. =item INTEGER C<TEST ID> - ID of test.
  982. =back
  983. Returns test definition HASHREF.
  984. $tr->getTestByID(222222);
  985. =cut
  986. sub getTestByID {
  987. my ($self,$test_id) = @_;
  988. confess("Object methods must be called by an instance") unless ref($self);
  989. confess("Test ID must be integer") unless $self->_checkInteger($test_id);
  990. return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
  991. }
  992. =head2 B<getTestResultFields()>
  993. Gets custom fields that can be set for tests.
  994. Returns ARRAYREF of result definition HASHREFs.
  995. =cut
  996. sub getTestResultFields {
  997. my $self = shift;
  998. confess("Object methods must be called by an instance") unless ref($self);
  999. return $self->{'tr_fields'} if defined($self->{'tr_fields'}); #cache
  1000. $self->{'tr_fields'} = $self->_doRequest('index.php?/api/v2/get_result_fields');
  1001. return $self->{'tr_fields'};
  1002. }
  1003. =head2 B<getTestResultFieldByName(SYSTEM_NAME,PROJECT_ID)>
  1004. Gets a test result field by it's system name. Optionally filter by project ID.
  1005. =over 4
  1006. =item B<SYSTEM NAME> - STRING: system name of a result field.
  1007. =item B<PROJECT ID> - INTEGER (optional): Filter by whether or not the field is enabled for said project
  1008. =back
  1009. =cut
  1010. sub getTestResultFieldByName {
  1011. my ($self,$system_name,$project_id) = @_;
  1012. confess("Object methods must be called by an instance") unless ref($self);
  1013. confess("System name must be string") unless $self->_checkString($system_name);
  1014. my @candidates = grep {$_->{'name'} eq $system_name} @{$self->getTestResultFields()};
  1015. return 0 if !scalar(@candidates);
  1016. if (defined $project_id) {
  1017. @candidates = grep {
  1018. $_->{'configs'}->[0]->{'context'}->{'is_global'} ||
  1019. ( grep {$_ == $project_id} @{ $_->{'configs'}->[0]->{'context'}->{'project_ids'} } )
  1020. } @candidates;
  1021. }
  1022. return $candidates[0];
  1023. }
  1024. =head2 B<getPossibleTestStatuses()>
  1025. Gets all possible statuses a test can be set to.
  1026. Returns ARRAYREF of status definition HASHREFs.
  1027. =cut
  1028. sub getPossibleTestStatuses {
  1029. my $self = shift;
  1030. confess("Object methods must be called by an instance") unless ref($self);
  1031. return $self->_doRequest('index.php?/api/v2/get_statuses');
  1032. }
  1033. =head2 B<createTestResults(test_id,status_id,comment,options,custom_options)>
  1034. Creates a result entry for a test.
  1035. =over 4
  1036. =item INTEGER C<TEST_ID> - ID of desired test
  1037. =item INTEGER C<STATUS_ID> - ID of desired test result status
  1038. =item STRING C<COMMENT> (optional) - Any comments about this result
  1039. =item HASHREF C<OPTIONS> (optional) - Various "Baked-In" options that can be set for test results. See TR docs for more information.
  1040. =item HASHREF C<CUSTOM OPTIONS> (optional) - Options to set for custom fields. See L<TestRail::API::buildStepResults> for a simple way to post up custom steps.
  1041. =back
  1042. Returns result definition HASHREF.
  1043. $options = {
  1044. elapsed => '30m 22s',
  1045. defects => ['TSR-3','BOOM-44'],
  1046. version => '6969'
  1047. };
  1048. $custom_options = {
  1049. step_results => [
  1050. {
  1051. content => 'Step 1',
  1052. expected => "Bought Groceries",
  1053. actual => "No Dinero!",
  1054. status_id => 2
  1055. },
  1056. {
  1057. content => 'Step 2',
  1058. expected => 'Ate Dinner',
  1059. actual => 'Went Hungry',
  1060. status_id => 2
  1061. }
  1062. ]
  1063. };
  1064. $res = $tr->createTestResults(1,2,'Test failed because it was all like WAAAAAAA when I poked it',$options,$custom_options);
  1065. =cut
  1066. sub createTestResults {
  1067. my ($self,$test_id,$status_id,$comment,$opts,$custom_fields) = @_;
  1068. confess("Object methods must be called by an instance") unless ref($self);
  1069. confess("Test ID must be integer") unless $self->_checkInteger($test_id);
  1070. confess("Status ID must be integer") unless $self->_checkInteger($status_id);
  1071. confess("Comment must be string") unless !defined($comment) || $self->_checkString($comment);
  1072. confess("Options must be HASHREF") unless !defined($opts) || (reftype($opts) || 'undef') eq 'HASH';
  1073. confess("Custom Options must be HASHREF") unless !defined($custom_fields) || (reftype($custom_fields) || 'undef') eq 'HASH';
  1074. my $stuff = {
  1075. status_id => $status_id,
  1076. comment => $comment
  1077. };
  1078. #Handle options
  1079. if (defined($opts) && reftype($opts) eq 'HASH') {
  1080. $stuff->{'version'} = defined($opts->{'version'}) ? $opts->{'version'} : undef;
  1081. $stuff->{'elapsed'} = defined($opts->{'elapsed'}) ? $opts->{'elapsed'} : undef;
  1082. $stuff->{'defects'} = defined($opts->{'defects'}) ? join(',',@{$opts->{'defects'}}) : undef;
  1083. $stuff->{'assignedto_id'} = defined($opts->{'assignedto_id'}) ? $opts->{'assignedto_id'} : undef;
  1084. }
  1085. #Handle custom fields
  1086. if (defined($custom_fields) && reftype($custom_fields) eq 'HASH') {
  1087. foreach my $field (keys(%$custom_fields)) {
  1088. $stuff->{"custom_$field"} = $custom_fields->{$field};
  1089. }
  1090. }
  1091. return $self->_doRequest("index.php?/api/v2/add_result/$test_id",'POST',$stuff);
  1092. }
  1093. =head2 B<getTestResults(test_id,limit)>
  1094. Get the recorded results for desired test, limiting output to 'limit' entries.
  1095. =over 4
  1096. =item INTEGER C<TEST_ID> - ID of desired test
  1097. =item POSITIVE INTEGER C<LIMIT> (OPTIONAL) - provide no more than this number of results.
  1098. =item INTEGER C<OFFSET> (OPTIONAL) - Offset to begin viewing resultset at.
  1099. =back
  1100. Returns ARRAYREF of result definition HASHREFs.
  1101. =cut
  1102. sub getTestResults {
  1103. my ($self,$test_id,$limit,$offset) = @_;
  1104. confess("Object methods must be called by an instance") unless ref($self);
  1105. confess("Test ID must be positive integer") unless $self->_checkInteger($test_id);
  1106. confess("Result limitation must be positive integer") unless !defined($limit) || ($self->_checkInteger($limit) && $limit > 0);
  1107. confess("Result offset must be integer") unless !defined($offset) || $self->_checkInteger($offset);
  1108. my $url = "index.php?/api/v2/get_results/$test_id";
  1109. $url .= "&limit=$limit" if defined($limit);
  1110. $url .= "&offset=$offset" if defined($offset);
  1111. return $self->_doRequest($url);
  1112. }
  1113. =head1 STATIC METHODS
  1114. =head2 B<buildStepResults(content,expected,actual,status_id)>
  1115. Convenience method to build the stepResult hashes seen in the custom options for getTestResults.
  1116. =over 4
  1117. =item STRING C<CONTENT> (optional) - The step itself.
  1118. =item STRING C<EXPECTED> (optional) - Expected result of test step.
  1119. =item STRING C<ACTUAL> (optional) - Actual result of test step
  1120. =item INTEGER C<STATUS ID> (optional) - Status ID of result
  1121. =back
  1122. =cut
  1123. #Convenience method for building stepResults
  1124. sub buildStepResults {
  1125. my ($content,$expected,$actual,$status_id) = @_;
  1126. return {
  1127. content => $content,
  1128. expected => $expected,
  1129. actual => $actual,
  1130. status_id => $status_id
  1131. };
  1132. }
  1133. #Type checks
  1134. sub _checkInteger {
  1135. shift;
  1136. my $integer = shift;
  1137. return ( defined $integer && looks_like_number($integer) && int($integer) == $integer );
  1138. }
  1139. sub _checkString {
  1140. shift;
  1141. my $str = shift;
  1142. return ( defined($str) && !ref($str) );
  1143. }
  1144. 1;
  1145. __END__
  1146. =head1 SEE ALSO
  1147. L<HTTP::Request>
  1148. L<LWP::UserAgent>
  1149. L<JSON::XS>
  1150. L<http://docs.gurock.com/testrail-api2/start>
  1151. =head1 SPECIAL THANKS
  1152. Thanks to cPanel Inc, for graciously funding the creation of this module.