API.pm 36 KB

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