API.pm 31 KB

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