# -*- coding: utf-8 -*- from requests import post, get from typing import Any, cast, Dict, List, Optional, Sequence, Union DEFAULT_TESTRAIL_HEADERS = {'Content-Type': 'application/json'} TESTRAIL_STATUS_ID_PASSED = 1 # custom types JsonDict = Dict[str, Any] # noqa: E993 JsonList = List[JsonDict] # noqa: E993 Id = Union[str, int] # noqa: E993 class TestRailAPIClient(object): """Library for working with [http://www.gurock.com/testrail/ | TestRail]. == Dependencies == | requests | https://pypi.python.org/pypi/requests | == Preconditions == 1. [ http://docs.gurock.com/testrail-api2/introduction | Enable TestRail API] """ def __init__(self, server: str, user: str, password: str, run_id: Id, protocol: str = 'http') -> None: """Create TestRailAPIClient instance. *Args:*\n _server_ - name of TestRail server;\n _user_ - name of TestRail user;\n _password_ - password of TestRail user;\n _run_id_ - ID of the test run;\n _protocol_ - connecting protocol to TestRail server: http or https. """ self._url = '{protocol}://{server}/testrail/index.php?/api/v2/'.format(protocol=protocol, server=server) self._user = user self._password = password self.run_id = run_id def _send_post(self, uri: str, data: Dict[str, Any]) -> Union[JsonList, JsonDict]: """Perform post request to TestRail. *Args:* \n _uri_ - URI for test case;\n _data_ - json with test result. *Returns:* \n Request result in json format. """ url = self._url + uri response = post(url, json=data, auth=(self._user, self._password), verify=False) response.raise_for_status() return response.json() def _send_get(self, uri: str, headers: Dict[str, str] = None, params: Dict[str, Any] = None) -> Union[JsonList, JsonDict]: """Perform get request to TestRail. *Args:* \n _uri_ - URI for test case;\n _headers_ - headers for http-request;\n _params_ - parameters for http-request. *Returns:* \n Request result in json format. """ url = self._url + uri response = get(url, headers=headers, params=params, auth=(self._user, self._password), verify=False) response.raise_for_status() return response.json() def get_tests(self, run_id: Id, status_ids: Union[str, Sequence[int]] = None) -> JsonList: """Get tests from TestRail test run by run_id. *Args:* \n _run_id_ - ID of the test run;\n _status_ids_ - list of the required test statuses. *Returns:* \n Tests information in json format. """ uri = 'get_tests/{run_id}'.format(run_id=run_id) if status_ids: status_ids = ','.join(str(status_id) for status_id in status_ids) params = { 'status_id': status_ids } response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS, params=params) return cast(JsonList, response) def get_results_for_case(self, run_id: Id, case_id: Id, limit: int = None) -> JsonList: """Get results for case by run_id and case_id. *Args:* \n _run_id_ - ID of the test run;\n _case_id_ - ID of the test case;\n _limit_ - limit of case results. *Returns:* \n Cases results in json format. """ uri = 'get_results_for_case/{run_id}/{case_id}'.format(run_id=run_id, case_id=case_id) params = { 'limit': limit } response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS, params=params) return cast(JsonList, response) def add_result_for_case(self, run_id: Id, case_id: Id, test_result_fields: Dict[str, Union[str, int]]) -> None: """Add results for case in TestRail test run by run_id and case_id. *Supported request fields for test result:*\n | *Name* | *Type* | *Description* | | status_id | int | The ID of the test status | | comment | string | The comment / description for the test result | | version | string | The version or build you tested against | | elapsed | timespan | The time it took to execute the test, e.g. "30s" or "1m 45s" | | defects | string | A comma-separated list of defects to link to the test result | | assignedto_id | int | The ID of a user the test should be assigned to | | Custom fields are supported as well and must be submitted with their system name, prefixed with 'custom_' | *Args:* \n _run_id_ - ID of the test run;\n _case_id_ - ID of the test case;\n _test_result_fields_ - result of the test fields dictionary. *Example:*\n | Add Result For Case | run_id=321 | case_id=123| test_result={'status_id': 3, 'comment': 'This test is untested', 'defects': 'DEF-123'} | """ uri = 'add_result_for_case/{run_id}/{case_id}'.format(run_id=run_id, case_id=case_id) self._send_post(uri, test_result_fields) def get_statuses(self) -> JsonList: """Get test statuses information from TestRail. *Returns:* \n Statuses information in json format. """ uri = 'get_statuses' response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonList, response) def update_case(self, case_id: Id, request_fields: Dict[str, Union[str, int, None]]) -> JsonDict: """Update an existing test case in TestRail. *Supported request fields:*\n | *Name* | *Type* | *Description* | | title | string | The title of the test case (required) | | template_id | int | The ID of the template (field layout) (requires TestRail 5.2 or later) | | type_id | int | The ID of the case type | | priority_id | int | The ID of the case priority | | estimate | timespan | The estimate, e.g. "30s" or "1m 45s" | | milestone_id | int | The ID of the milestone to link to the test case | | refs | string | A comma-separated list of references/requirements | | Custom fields are supported as well and must be submitted with their system name, prefixed with 'custom_' | *Args:* \n _case_id_ - ID of the test case;\n _request_fields_ - request fields dictionary. *Returns:* \n Case information in json format. *Example:*\n | Update Case | case_id=213 | request_fields={'title': name, 'type_id': 1, 'custom_case_description': description, 'refs': references} | """ uri = 'update_case/{case_id}'.format(case_id=case_id) response = self._send_post(uri, request_fields) return cast(JsonDict, response) def get_status_id_by_status_label(self, status_label: str) -> int: """Get test status id by status label. *Args:* \n _status_label_ - status label of the tests. *Returns:* \n Test status ID. """ statuses_info = self.get_statuses() for status in statuses_info: if status['label'].lower() == status_label.lower(): return status['id'] raise Exception(u"There is no status with label \'{}\' in TestRail".format(status_label)) def get_test_status_id_by_case_id(self, run_id: Id, case_id: Id) -> Optional[int]: """Get test last status id by case id. If there is no last test result returns None. *Args:* \n _run_id_ - ID of the test run;\n _case_id_ - ID of the test case. *Returns:* \n Test status ID. """ last_case_result = self.get_results_for_case(run_id=run_id, case_id=case_id, limit=1) return last_case_result[0]['status_id'] if last_case_result else None def get_project(self, project_id: Id) -> JsonDict: """Get project info by project id. *Args:* \n _project_id_ - ID of the project. *Returns:* \n Request result in json format. """ uri = 'get_project/{project_id}'.format(project_id=project_id) response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonDict, response) def get_suite(self, suite_id: Id) -> JsonDict: """Get suite info by suite id. *Args:* \n _suite_id_ - ID of the test suite. *Returns:* \n Request result in json format. """ uri = 'get_suite/{suite_id}'.format(suite_id=suite_id) response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonDict, response) def get_section(self, section_id: Id) -> JsonDict: """Get section info by section id. *Args:* \n _section_id_ - ID of the section. *Returns:* \n Request result in json format. """ uri = 'get_section/{section_id}'.format(section_id=section_id) response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonDict, response) def add_section(self, project_id: Id, name: str, suite_id: Id = None, parent_id: Id = None, description: str = None) -> JsonDict: """Creates a new section. *Args:* \n _project_id_ - ID of the project;\n _name_ - name of the section;\n _suite_id_ - ID of the test suite(ignored if the project is operating in single suite mode);\n _parent_id_ - ID of the parent section (to build section hierarchies);\n _description_ - description of the section. *Returns:* \n New section information. """ uri = 'add_section/{project_id}'.format(project_id=project_id) data: Dict[str, Union[int, str]] = {'name': name} if suite_id is not None: data['suite_id'] = suite_id if parent_id is not None: data['parent_id'] = parent_id if description is not None: data['description'] = description response = self._send_post(uri=uri, data=data) return cast(JsonDict, response) def get_sections(self, project_id: Id, suite_id: Id) -> JsonList: """Returns existing sections. *Args:* \n _project_id_ - ID of the project;\n _suite_id_ - ID of the test suite. *Returns:* \n Information about section. """ uri = 'get_sections/{project_id}&suite_id={suite_id}'.format(project_id=project_id, suite_id=suite_id) response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonList, response) def get_case(self, case_id: Id) -> JsonDict: """Get case info by case id. *Args:* \n _case_id_ - ID of the test case. *Returns:* \n Request result in json format. """ uri = 'get_case/{case_id}'.format(case_id=case_id) response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS) return cast(JsonDict, response) def get_cases(self, project_id: Id, suite_id: Id = None, section_id: Id = None) -> JsonList: """Returns a list of test cases for a test suite or specific section in a test suite. *Args:* \n _project_id_ - ID of the project;\n _suite_id_ - ID of the test suite (optional if the project is operating in single suite mode);\n _section_id_ - ID of the section (optional). *Returns:* \n Information about test cases in section. """ uri = 'get_cases/{project_id}'.format(project_id=project_id) params = {'project_id': project_id} if suite_id is not None: params['suite_id'] = suite_id if section_id is not None: params['section_id'] = section_id response = self._send_get(uri=uri, headers=DEFAULT_TESTRAIL_HEADERS, params=params) return cast(JsonList, response) def add_case(self, section_id: Id, title: str, steps: List[Dict[str, str]], description: str, refs: str, type_id: Id, priority_id: Id, **additional_data: Any) -> JsonDict: """Creates a new test case. *Args:* \n _section_id_ - ID of the section;\n _title_ - title of the test case;\n _steps_ - test steps;\n _description_ - test description;\n _refs_ - comma-separated list of references;\n _type_id_ - ID of the case type;\n _priority_id_ - ID of the case priority;\n _additional_data_ - additional parameters. *Returns:* \n Information about new test case. """ uri = 'add_case/{section_id}'.format(section_id=section_id) data = { 'title': title, 'custom_case_description': description, 'custom_steps_separated': steps, 'refs': refs, 'type_id': type_id, 'priority_id': priority_id } for key in additional_data: data[key] = additional_data[key] response = self._send_post(uri=uri, data=data) return cast(JsonDict, response)