API.pm 31 KB

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