diff --git a/.gitignore b/.gitignore index f0f81ac..6f809d7 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,11 @@ venv.bak/ # mypy .mypy_cache/ + +# Pycharm .idea/ -/.pypirc -todo.txt -/dev_tools \ No newline at end of file + +# Other +Pipfile.lock +/dev_tools +testrail_api/__version__.py \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0599e9f..0ae7582 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,30 @@ +dist: xenial language: python -jobs: - include: - - stage: deploy - dist: xenial - python: 3.8 - sudo: required - deploy: - provider: pypi - user: $PYPI_USER - password: $PYPI_PASSWORD - skip_cleanup: true - distributions: "twine upload dist/*" - on: - branch: master - repo: tolstislon/testrail-api +python: + - "3.6" + - "3.7" + - "3.8" install: - - pip install m2r==0.2.1 - - pip install twine==3.1.1 - + - pip install pipenv + - pipenv install pytest==5.4.1 + - pipenv install pytest-cov==2.8.1 + - pipenv install responses==0.10.12 + - pipenv install -e . script: - - python setup.py bdist_wheel + - pytest tests/unit +deploy: + provider: pypi + user: $PYPI_USER + password: $PYPI_PASSWORD + skip_existing: true + distributions: bdist_wheel --universal + on: + tags: true + repo: tolstislon/testrail-api + python: "3.8" -branches: - only: - - master \ No newline at end of file +notifications: + email: + on_success: never + on_failure: always \ No newline at end of file diff --git a/Pipfile b/Pipfile index d0195ed..f7fccc4 100644 --- a/Pipfile +++ b/Pipfile @@ -4,12 +4,18 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -m2r = "==0.2.1" pytest = "==5.4.1" pytest-cov = "==2.8.1" +responses = "==0.10.12" +pylint = "==2.4.4" +mypy = "==0.770" +black = "==19.10b0" [packages] requests = "==2.23.0" [requires] python_version = "3.8" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index a98a24c..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,203 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "2f34d64605fac2fdebc353d4294c602a862d202a38cb24affdf4e1fc7fbf868a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" - ], - "version": "==2019.11.28" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" - ], - "version": "==2.9" - }, - "requests": { - "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" - ], - "index": "pypi", - "version": "==2.23.0" - }, - "urllib3": { - "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" - ], - "version": "==1.25.8" - } - }, - "develop": { - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "markers": "sys_platform == 'win32'", - "version": "==1.3.0" - }, - "attrs": { - "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" - ], - "version": "==19.3.0" - }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.3" - }, - "coverage": { - "hashes": [ - "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", - "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", - "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", - "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", - "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", - "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", - "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", - "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", - "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", - "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", - "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", - "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", - "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", - "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", - "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", - "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", - "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", - "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", - "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", - "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", - "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", - "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", - "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", - "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", - "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", - "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", - "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", - "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", - "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", - "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", - "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" - ], - "version": "==5.0.3" - }, - "docutils": { - "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" - ], - "version": "==0.16" - }, - "m2r": { - "hashes": [ - "sha256:bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99" - ], - "index": "pypi", - "version": "==0.2.1" - }, - "mistune": { - "hashes": [ - "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", - "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4" - ], - "version": "==0.8.4" - }, - "more-itertools": { - "hashes": [ - "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", - "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" - ], - "version": "==8.2.0" - }, - "packaging": { - "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" - ], - "version": "==20.3" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "version": "==0.13.1" - }, - "py": { - "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" - ], - "version": "==1.8.1" - }, - "pyparsing": { - "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" - ], - "version": "==2.4.6" - }, - "pytest": { - "hashes": [ - "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", - "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" - ], - "index": "pypi", - "version": "==5.4.1" - }, - "pytest-cov": { - "hashes": [ - "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", - "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" - ], - "index": "pypi", - "version": "==2.8.1" - }, - "six": { - "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" - ], - "version": "==1.14.0" - }, - "wcwidth": { - "hashes": [ - "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", - "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" - ], - "version": "==0.1.8" - } - } -} diff --git a/README.md b/README.md index 464b410..c46be9a 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,25 @@ ![PyPI](https://img.shields.io/pypi/v/testrail-api?color=%2301a001&label=version&logo=version) [![Downloads](https://pepy.tech/badge/testrail-api)](https://github.com/tolstislon/testrail-api) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/testrail-api.svg) -![PyPI - Python Version](https://img.shields.io/badge/TestRail-6.2.1.1005-blue) [![Build Status](https://travis-ci.com/tolstislon/testrail-api.svg?branch=master)](https://travis-ci.com/tolstislon/testrail-api) This is a Python wrapper of the TestRail API(v2) according to [the official documentation](http://docs.gurock.com/testrail-api2/start) -### Install +Install +---- ```bash pip install testrail-api ``` ##### Support environment variables -* TESTRAIL_URL -* TESTRAIL_EMAIL -* TESTRAIL_PASSWORD - -### Example +* `TESTRAIL_URL` +* `TESTRAIL_EMAIL` +* `TESTRAIL_PASSWORD` +Example +---- ```python from datetime import datetime @@ -30,7 +30,7 @@ from testrail_api import TestRailAPI api = TestRailAPI('https://example.testrail.com/', 'example@mail.com', 'password') # if use environment variables -api = TestRailAPI() +# api = TestRailAPI() new_milestone = api.milestones.add_milestone( @@ -61,3 +61,7 @@ api.runs.close_run(my_test_run['id']) api.milestones.update_milestone(new_milestone['id'], is_completed=True) ``` + +Contributing +---- +Contributions are very welcome. diff --git a/setup.py b/setup.py index 14aac57..8a6c63f 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,23 @@ -from m2r import parse_from_file -from setuptools import setup, find_packages +from pathlib import Path -import testrail_api +from setuptools import find_packages, setup + +readme = Path('.', 'README.md').absolute() +with readme.open('r', encoding='utf-8') as file: + long_description = file.read() setup( name='testrail_api', - version=testrail_api.__version__, packages=find_packages(exclude=('tests', 'dev_tools')), - url=testrail_api.__url__, - license=testrail_api.__license__, - author=testrail_api.__author__, - author_email=testrail_api.__author_email__, - description=testrail_api.__description__, - long_description=parse_from_file('README.md'), + url='https://github.com/tolstislon/testrail-api', + license='MIT License', + author='tolstislon', + author_email='tolstislon@gmail.com', + description='Python wrapper of the TestRail API', + long_description=long_description, + long_description_content_type='text/markdown', + use_scm_version={"write_to": "testrail_api/__version__.py"}, + setup_requires=['setuptools_scm'], install_requires=[ 'requests>=2.20.1' ], diff --git a/testrail_api/__init__.py b/testrail_api/__init__.py index aff3c3a..34ffdc9 100644 --- a/testrail_api/__init__.py +++ b/testrail_api/__init__.py @@ -13,30 +13,17 @@ """ -from .__version__ import ( - __version__, - __author__, - __author_email__, - __description__, - __license__, - __url__ -) - -from ._testrail_api import TestRailAPI -from ._session import StatusCodeError +try: + from .__version__ import version as __version__ +except ImportError: + __version__ = "unknown" import logging from logging import NullHandler +from ._exception import StatusCodeError +from ._testrail_api import TestRailAPI + logging.getLogger(__name__).addHandler(NullHandler()) -__all__ = [ - 'TestRailAPI', - '__version__', - '__author__', - '__author_email__', - '__description__', - '__license__', - '__url__', - 'StatusCodeError' -] +__all__ = ["TestRailAPI", "StatusCodeError", "__version__"] diff --git a/testrail_api/__version__.py b/testrail_api/__version__.py deleted file mode 100644 index 1777af5..0000000 --- a/testrail_api/__version__.py +++ /dev/null @@ -1,6 +0,0 @@ -__version__ = '1.4.11' -__url__ = 'https://github.com/tolstislon/testrail-api' -__description__ = 'Python wrapper of the TestRail API' -__author__ = 'tolstislon' -__author_email__ = 'tolstislon@gmail.com' -__license__ = 'MIT License' diff --git a/testrail_api/_category.py b/testrail_api/_category.py index 821cdcd..850f830 100644 --- a/testrail_api/_category.py +++ b/testrail_api/_category.py @@ -1,21 +1,23 @@ +# pylint: disable=C0302 """ TestRail API categories """ -import warnings from pathlib import Path from typing import List, Optional, Union from ._enums import METHODS -class BaseCategory: +class _MetaCategory: + """Meta Category""" - def __init__(self, session): + def __init__(self, session) -> None: self._session = session -class Cases(BaseCategory): +class Cases(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-cases""" def get_case(self, case_id: int) -> dict: """ @@ -25,7 +27,7 @@ def get_case(self, case_id: int) -> dict: :param case_id: The ID of the test case :return: response """ - return self._session.request(METHODS.GET, f'get_case/{case_id}') + return self._session.request(METHODS.GET, f"get_case/{case_id}") def get_cases(self, project_id: int, **kwargs) -> List[dict]: """ @@ -40,7 +42,9 @@ def get_cases(self, project_id: int, **kwargs) -> List[dict]: :key filter: Only return cases with matching filter string in the case title :return: response """ - return self._session.request(METHODS.GET, f'get_cases/{project_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_cases/{project_id}", params=kwargs + ) def add_case(self, section_id: int, title: str, **kwargs) -> dict: """ @@ -65,7 +69,7 @@ def add_case(self, section_id: int, title: str, **kwargs) -> dict: :return: response """ data = dict(title=title, **kwargs) - return self._session.request(METHODS.POST, f'add_case/{section_id}', json=data) + return self._session.request(METHODS.POST, f"add_case/{section_id}", json=data) def update_case(self, case_id: int, **kwargs) -> dict: """ @@ -77,7 +81,9 @@ def update_case(self, case_id: int, **kwargs) -> dict: :param kwargs: This method supports the same POST fields as add_case (except section_id). :return: response """ - return self._session.request(METHODS.POST, f'update_case/{case_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_case/{case_id}", json=kwargs + ) def delete_case(self, case_id: int) -> None: """ @@ -87,10 +93,11 @@ def delete_case(self, case_id: int) -> None: :param case_id: The ID of the test case :return: response """ - return self._session.request(METHODS.POST, f'delete_case/{case_id}') + return self._session.request(METHODS.POST, f"delete_case/{case_id}") -class CaseFields(BaseCategory): +class CaseFields(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-cases-fields""" def get_case_fields(self) -> List[dict]: """ @@ -99,9 +106,11 @@ def get_case_fields(self) -> List[dict]: Returns a list of available test case custom fields. :return: response """ - return self._session.request(METHODS.GET, 'get_case_fields') + return self._session.request(METHODS.GET, "get_case_fields") - def add_case_field(self, type: str, name: str, label: str, **kwargs) -> dict: + def add_case_field( + self, type: str, name: str, label: str, **kwargs # pylint: disable=W0622 + ) -> dict: """ http://docs.gurock.com/testrail-api2/reference-cases-fields#add_case_field @@ -122,10 +131,11 @@ def add_case_field(self, type: str, name: str, label: str, **kwargs) -> dict: :return: response """ data = dict(type=type, name=name, label=label, **kwargs) - return self._session.request(METHODS.POST, 'add_case_field', json=data) + return self._session.request(METHODS.POST, "add_case_field", json=data) -class CaseTypes(BaseCategory): +class CaseTypes(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-cases-types""" def get_case_types(self) -> List[dict]: """ @@ -134,10 +144,11 @@ def get_case_types(self) -> List[dict]: Returns a list of available case types. :return: response """ - return self._session.request(METHODS.GET, 'get_case_types') + return self._session.request(METHODS.GET, "get_case_types") -class Configurations(BaseCategory): +class Configurations(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-configs""" def get_configs(self, project_id: int) -> List[dict]: """ @@ -147,7 +158,7 @@ def get_configs(self, project_id: int) -> List[dict]: :param project_id: The ID of the project :return: response """ - return self._session.request(METHODS.GET, f'get_configs/{project_id}') + return self._session.request(METHODS.GET, f"get_configs/{project_id}") def add_config_group(self, project_id: int, name: str) -> None: """ @@ -158,7 +169,9 @@ def add_config_group(self, project_id: int, name: str) -> None: :param name: The name of the configuration group (required) :return: response """ - return self._session.request(METHODS.POST, f'add_config_group/{project_id}', json={'name': name}) + return self._session.request( + METHODS.POST, f"add_config_group/{project_id}", json={"name": name} + ) def add_config(self, config_group_id: int, name: str) -> None: """ @@ -169,7 +182,9 @@ def add_config(self, config_group_id: int, name: str) -> None: :param name: The name of the configuration (required) :return: response """ - return self._session.request(METHODS.POST, f'add_config/{config_group_id}', json={'name': name}) + return self._session.request( + METHODS.POST, f"add_config/{config_group_id}", json={"name": name} + ) def update_config_group(self, config_group_id: int, name: str) -> None: """ @@ -180,7 +195,9 @@ def update_config_group(self, config_group_id: int, name: str) -> None: :param name: The name of the configuration group :return: response """ - return self._session.request(METHODS.POST, f'update_config_group/{config_group_id}', json={'name': name}) + return self._session.request( + METHODS.POST, f"update_config_group/{config_group_id}", json={"name": name} + ) def update_config(self, config_id: int, name: str) -> None: """ @@ -191,7 +208,9 @@ def update_config(self, config_id: int, name: str) -> None: :param name: The name of the configuration :return: response """ - return self._session.request(METHODS.POST, f'update_config/{config_id}', json={'name': name}) + return self._session.request( + METHODS.POST, f"update_config/{config_id}", json={"name": name} + ) def delete_config_group(self, config_group_id: int) -> None: """ @@ -201,7 +220,9 @@ def delete_config_group(self, config_group_id: int) -> None: :param config_group_id: The ID of the configuration group :return: response """ - return self._session.request(METHODS.POST, f'delete_config_group/{config_group_id}') + return self._session.request( + METHODS.POST, f"delete_config_group/{config_group_id}" + ) def delete_config(self, config_id: int) -> None: """ @@ -211,10 +232,11 @@ def delete_config(self, config_id: int) -> None: :param config_id: The ID of the configuration :return: response """ - return self._session.request(METHODS.POST, f'delete_config/{config_id}') + return self._session.request(METHODS.POST, f"delete_config/{config_id}") -class Milestones(BaseCategory): +class Milestones(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-milestones""" def get_milestone(self, milestone_id: int) -> dict: """ @@ -224,7 +246,7 @@ def get_milestone(self, milestone_id: int) -> dict: :param milestone_id: The ID of the milestone :return: response """ - return self._session.request(METHODS.GET, f'get_milestone/{milestone_id}') + return self._session.request(METHODS.GET, f"get_milestone/{milestone_id}") def get_milestones(self, project_id: int, **kwargs) -> List[dict]: """ @@ -238,7 +260,9 @@ def get_milestones(self, project_id: int, **kwargs) -> List[dict]: (available since TestRail 5.3). :return: response """ - return self._session.request(METHODS.GET, f'get_milestones/{project_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_milestones/{project_id}", params=kwargs + ) def add_milestone(self, project_id: int, name: str, **kwargs) -> dict: """ @@ -257,7 +281,9 @@ def add_milestone(self, project_id: int, name: str, **kwargs) -> dict: :return: response """ data = dict(name=name, **kwargs) - return self._session.request(METHODS.POST, f'add_milestone/{project_id}', json=data) + return self._session.request( + METHODS.POST, f"add_milestone/{project_id}", json=data + ) def update_milestone(self, milestone_id: int, **kwargs) -> dict: """ @@ -274,7 +300,9 @@ def update_milestone(self, milestone_id: int, **kwargs) -> dict: (available since TestRail 5.3) :return: response """ - return self._session.request(METHODS.POST, f'update_milestone/{milestone_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_milestone/{milestone_id}", json=kwargs + ) def delete_milestone(self, milestone_id: int) -> None: """ @@ -284,10 +312,11 @@ def delete_milestone(self, milestone_id: int) -> None: :param milestone_id: The ID of the milestone :return: response """ - return self._session.request(METHODS.POST, f'delete_milestone/{milestone_id}') + return self._session.request(METHODS.POST, f"delete_milestone/{milestone_id}") -class Plans(BaseCategory): +class Plans(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-plans""" def get_plan(self, plan_id: int) -> dict: """ @@ -297,7 +326,7 @@ def get_plan(self, plan_id: int) -> dict: :param plan_id: The ID of the test plan :return: response """ - return self._session.request(METHODS.GET, f'get_plan/{plan_id}') + return self._session.request(METHODS.GET, f"get_plan/{plan_id}") def get_plans(self, project_id: int, **kwargs) -> List[dict]: """ @@ -314,7 +343,9 @@ def get_plans(self, project_id: int, **kwargs) -> List[dict]: :key milestone_id: int(list) - A comma-separated list of milestone IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_plans/{project_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_plans/{project_id}", params=kwargs + ) def add_plan(self, project_id: int, name: str, **kwargs) -> dict: """ @@ -330,7 +361,7 @@ def add_plan(self, project_id: int, name: str, **kwargs) -> dict: :return: response """ data = dict(name=name, **kwargs) - return self._session.request(METHODS.POST, f'add_plan/{project_id}', json=data) + return self._session.request(METHODS.POST, f"add_plan/{project_id}", json=data) def add_plan_entry(self, plan_id: int, suite_id: int, **kwargs) -> dict: """ @@ -352,7 +383,9 @@ def add_plan_entry(self, plan_id: int, suite_id: int, **kwargs) -> dict: :return: response """ data = dict(suite_id=suite_id, **kwargs) - return self._session.request(METHODS.POST, f'add_plan_entry/{plan_id}', json=data) + return self._session.request( + METHODS.POST, f"add_plan_entry/{plan_id}", json=data + ) def update_plan(self, plan_id: int, **kwargs) -> dict: """ @@ -364,7 +397,9 @@ def update_plan(self, plan_id: int, **kwargs) -> dict: :param kwargs: With the exception of the entries field, this method supports the same POST fields as add_plan. :return: response """ - return self._session.request(METHODS.POST, f'update_plan/{plan_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_plan/{plan_id}", json=kwargs + ) def update_plan_entry(self, plan_id: int, entry_id: int, **kwargs) -> dict: """ @@ -382,7 +417,9 @@ def update_plan_entry(self, plan_id: int, entry_id: int, **kwargs) -> dict: :key case_ids: list - An array of case IDs for the custom case selection :return: response """ - return self._session.request(METHODS.POST, f'update_plan_entry/{plan_id}/{entry_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_plan_entry/{plan_id}/{entry_id}", json=kwargs + ) def close_plan(self, plan_id: int) -> dict: """ @@ -392,7 +429,7 @@ def close_plan(self, plan_id: int) -> dict: :param plan_id: The ID of the test plan :return: response """ - return self._session.request(METHODS.POST, f'close_plan/{plan_id}') + return self._session.request(METHODS.POST, f"close_plan/{plan_id}") def delete_plan(self, plan_id: int) -> None: """ @@ -402,7 +439,7 @@ def delete_plan(self, plan_id: int) -> None: :param plan_id: The ID of the test plan :return: response """ - return self._session.request(METHODS.POST, f'delete_plan/{plan_id}') + return self._session.request(METHODS.POST, f"delete_plan/{plan_id}") def delete_plan_entry(self, plan_id: int, entry_id: int) -> None: """ @@ -413,10 +450,13 @@ def delete_plan_entry(self, plan_id: int, entry_id: int) -> None: :param entry_id: The ID of the test plan entry (note: not the test run ID) :return: response """ - return self._session.request(METHODS.POST, f'delete_plan_entry/{plan_id}/{entry_id}') + return self._session.request( + METHODS.POST, f"delete_plan_entry/{plan_id}/{entry_id}" + ) -class Priorities(BaseCategory): +class Priorities(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-priorities""" def get_priorities(self) -> List[dict]: """ @@ -425,10 +465,11 @@ def get_priorities(self) -> List[dict]: Returns a list of available priorities. :return: response """ - return self._session.request(METHODS.GET, 'get_priorities') + return self._session.request(METHODS.GET, "get_priorities") -class Projects(BaseCategory): +class Projects(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-projects""" def get_project(self, project_id: int) -> dict: """ @@ -439,7 +480,7 @@ def get_project(self, project_id: int) -> dict: :param project_id: The ID of the project :return: response """ - return self._session.request(METHODS.GET, f'get_project/{project_id}') + return self._session.request(METHODS.GET, f"get_project/{project_id}") def get_projects(self, **kwargs) -> List[dict]: """ @@ -451,7 +492,7 @@ def get_projects(self, **kwargs) -> List[dict]: :key is_completed: int - 1 to return completed projects only. 0 to return active projects only. :return: response """ - return self._session.request(METHODS.GET, 'get_projects', params=kwargs) + return self._session.request(METHODS.GET, "get_projects", params=kwargs) def add_project(self, name: str, **kwargs) -> dict: """ @@ -468,7 +509,7 @@ def add_project(self, name: str, **kwargs) -> dict: :return: response """ data = dict(name=name, **kwargs) - return self._session.request(METHODS.POST, 'add_project', json=data) + return self._session.request(METHODS.POST, "add_project", json=data) def update_project(self, project_id: int, **kwargs) -> dict: """ @@ -482,7 +523,9 @@ def update_project(self, project_id: int, **kwargs) -> dict: :key is_completed: bool - Specifies whether a project is considered completed or not :return: response """ - return self._session.request(METHODS.POST, f'update_project/{project_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_project/{project_id}", json=kwargs + ) def delete_project(self, project_id: int) -> None: """ @@ -493,10 +536,11 @@ def delete_project(self, project_id: int) -> None: :param project_id: The ID of the project :return: response """ - return self._session.request(METHODS.POST, f'delete_project/{project_id}') + return self._session.request(METHODS.POST, f"delete_project/{project_id}") -class Results(BaseCategory): +class Results(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-results""" def get_results(self, test_id: int, **kwargs) -> List[dict]: """ @@ -510,7 +554,9 @@ def get_results(self, test_id: int, **kwargs) -> List[dict]: :key status_id: int(list) - A comma-separated list of status IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_results/{test_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_results/{test_id}", params=kwargs + ) def get_results_for_case(self, run_id: int, case_id: int, **kwargs) -> List[dict]: """ @@ -532,7 +578,9 @@ def get_results_for_case(self, run_id: int, case_id: int, **kwargs) -> List[dict :key status_id: int(list) - A comma-separated list of status IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_results_for_case/{run_id}/{case_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_results_for_case/{run_id}/{case_id}", params=kwargs + ) def get_results_for_run(self, run_id: int, **kwargs) -> List[dict]: """ @@ -550,7 +598,9 @@ def get_results_for_run(self, run_id: int, **kwargs) -> List[dict]: :key status_id: int(list) - A comma-separated list of status IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_results_for_run/{run_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_results_for_run/{run_id}", params=kwargs + ) def add_result(self, test_id: int, **kwargs) -> List[dict]: """ @@ -574,7 +624,7 @@ def add_result(self, test_id: int, **kwargs) -> List[dict]: :key assignedto_id: int - The ID of a user the test should be assigned to :return: response """ - return self._session.request(METHODS.POST, f'add_result/{test_id}', json=kwargs) + return self._session.request(METHODS.POST, f"add_result/{test_id}", json=kwargs) def add_result_for_case(self, run_id: int, case_id: int, **kwargs) -> List[dict]: """ @@ -595,7 +645,9 @@ def add_result_for_case(self, run_id: int, case_id: int, **kwargs) -> List[dict] :param kwargs: This method supports the same POST fields as add_result. :return: response """ - return self._session.request(METHODS.POST, f'add_result_for_case/{run_id}/{case_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"add_result_for_case/{run_id}/{case_id}", json=kwargs + ) def add_results(self, run_id: int, results: List[dict]) -> List[dict]: """ @@ -616,7 +668,9 @@ def add_results(self, run_id: int, results: List[dict]) -> List[dict]: Please note that all referenced tests must belong to the same test run. :return: response """ - return self._session.request(METHODS.POST, f'add_results/{run_id}', json={'results': results}) + return self._session.request( + METHODS.POST, f"add_results/{run_id}", json={"results": results} + ) def add_results_for_cases(self, run_id: int, results: List[dict]) -> List[dict]: """ @@ -639,10 +693,13 @@ def add_results_for_cases(self, run_id: int, results: List[dict]) -> List[dict]: Please note that all referenced tests must belong to the same test run. :return: response """ - return self._session.request(METHODS.POST, f'add_results_for_cases/{run_id}', json={'results': results}) + return self._session.request( + METHODS.POST, f"add_results_for_cases/{run_id}", json={"results": results} + ) -class ResultFields(BaseCategory): +class ResultFields(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-results-fields""" def get_result_fields(self) -> List[dict]: """ @@ -652,10 +709,11 @@ def get_result_fields(self) -> List[dict]: :return: response """ - return self._session.request(METHODS.GET, 'get_result_fields') + return self._session.request(METHODS.GET, "get_result_fields") -class Runs(BaseCategory): +class Runs(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-runs""" def get_run(self, run_id: int) -> dict: """ @@ -666,7 +724,7 @@ def get_run(self, run_id: int) -> dict: :param run_id: The ID of the test run :return: response """ - return self._session.request(METHODS.GET, f'get_run/{run_id}') + return self._session.request(METHODS.GET, f"get_run/{run_id}") def get_runs(self, project_id: int, **kwargs) -> List[dict]: """ @@ -686,7 +744,9 @@ def get_runs(self, project_id: int, **kwargs) -> List[dict]: :key suite_id: int(list) - A comma-separated list of test suite IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_runs/{project_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_runs/{project_id}", params=kwargs + ) def add_run(self, project_id: int, **kwargs) -> dict: """ @@ -706,7 +766,7 @@ def add_run(self, project_id: int, **kwargs) -> dict: :key case_ids: list - An array of case IDs for the custom case selection :return: response """ - return self._session.request(METHODS.POST, f'add_run/{project_id}', json=kwargs) + return self._session.request(METHODS.POST, f"add_run/{project_id}", json=kwargs) def update_run(self, run_id: int, **kwargs) -> dict: """ @@ -720,7 +780,7 @@ def update_run(self, run_id: int, **kwargs) -> dict: this method supports the same POST fields as add_run. :return: response """ - return self._session.request(METHODS.POST, f'update_run/{run_id}', json=kwargs) + return self._session.request(METHODS.POST, f"update_run/{run_id}", json=kwargs) def close_run(self, run_id: int) -> Optional[dict]: """ @@ -731,7 +791,7 @@ def close_run(self, run_id: int) -> Optional[dict]: :param run_id: The ID of the test run :return: response """ - return self._session.request(METHODS.POST, f'close_run/{run_id}') + return self._session.request(METHODS.POST, f"close_run/{run_id}") def delete_run(self, run_id: int) -> None: """ @@ -742,10 +802,11 @@ def delete_run(self, run_id: int) -> None: :param run_id: The ID of the test run :return: response """ - return self._session.request(METHODS.POST, f'delete_run/{run_id}') + return self._session.request(METHODS.POST, f"delete_run/{run_id}") -class Sections(BaseCategory): +class Sections(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-runs""" def get_section(self, section_id: int) -> dict: """ @@ -756,7 +817,7 @@ def get_section(self, section_id: int) -> dict: :param section_id: The ID of the section :return: response """ - return self._session.request(METHODS.GET, f'get_section/{section_id}') + return self._session.request(METHODS.GET, f"get_section/{section_id}") def get_sections(self, project_id: int, **kwargs) -> List[dict]: """ @@ -769,7 +830,9 @@ def get_sections(self, project_id: int, **kwargs) -> List[dict]: :key suite_id: The ID of the test suite (optional if the project is operating in single suite mode) :return: response """ - return self._session.request(METHODS.GET, f'get_sections/{project_id}', params=kwargs) + return self._session.request( + METHODS.GET, f"get_sections/{project_id}", params=kwargs + ) def add_section(self, project_id: int, name: str, **kwargs) -> dict: """ @@ -786,7 +849,9 @@ def add_section(self, project_id: int, name: str, **kwargs) -> dict: :return: response """ data = dict(name=name, **kwargs) - return self._session.request(METHODS.POST, f'add_section/{project_id}', json=data) + return self._session.request( + METHODS.POST, f"add_section/{project_id}", json=data + ) def update_section(self, section_id: int, **kwargs) -> dict: """ @@ -800,7 +865,9 @@ def update_section(self, section_id: int, **kwargs) -> dict: :key description: str - The description of the section (added with TestRail 4.0) :return: response """ - return self._session.request(METHODS.POST, f'update_section/{section_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_section/{section_id}", json=kwargs + ) def delete_section(self, section_id: int) -> None: """ @@ -811,10 +878,11 @@ def delete_section(self, section_id: int) -> None: :param section_id: The ID of the section :return: response """ - return self._session.request(METHODS.POST, f'delete_section/{section_id}') + return self._session.request(METHODS.POST, f"delete_section/{section_id}") -class Statuses(BaseCategory): +class Statuses(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-sections""" def get_statuses(self) -> List[dict]: """ @@ -824,10 +892,11 @@ def get_statuses(self) -> List[dict]: :return: response """ - return self._session.request(METHODS.GET, 'get_statuses') + return self._session.request(METHODS.GET, "get_statuses") -class Suites(BaseCategory): +class Suites(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-suites""" def get_suite(self, suite_id: int) -> dict: """ @@ -838,7 +907,7 @@ def get_suite(self, suite_id: int) -> dict: :param suite_id: The ID of the test suite :return: response """ - return self._session.request(METHODS.GET, f'get_suite/{suite_id}') + return self._session.request(METHODS.GET, f"get_suite/{suite_id}") def get_suites(self, project_id: int) -> List[dict]: """ @@ -849,7 +918,7 @@ def get_suites(self, project_id: int) -> List[dict]: :param project_id: The ID of the project :return: response """ - return self._session.request(METHODS.GET, f'get_suites/{project_id}') + return self._session.request(METHODS.GET, f"get_suites/{project_id}") def add_suite(self, project_id: int, name: str, **kwargs) -> dict: """ @@ -863,7 +932,7 @@ def add_suite(self, project_id: int, name: str, **kwargs) -> dict: :return: response """ data = dict(name=name, **kwargs) - return self._session.request(METHODS.POST, f'add_suite/{project_id}', json=data) + return self._session.request(METHODS.POST, f"add_suite/{project_id}", json=data) def update_suite(self, suite_id: int, **kwargs) -> dict: """ @@ -875,7 +944,9 @@ def update_suite(self, suite_id: int, **kwargs) -> dict: :param kwargs: This methods supports the same POST fields as add_suite. :return: response """ - return self._session.request(METHODS.POST, f'update_suite/{suite_id}', json=kwargs) + return self._session.request( + METHODS.POST, f"update_suite/{suite_id}", json=kwargs + ) def delete_suite(self, suite_id: int) -> None: """ @@ -886,10 +957,11 @@ def delete_suite(self, suite_id: int) -> None: :param suite_id: The ID of the test suite :return: response """ - return self._session.request(METHODS.POST, f'delete_suite/{suite_id}') + return self._session.request(METHODS.POST, f"delete_suite/{suite_id}") -class Template(BaseCategory): +class Template(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-templates""" def get_templates(self, project_id: int) -> List[dict]: """ @@ -900,10 +972,11 @@ def get_templates(self, project_id: int) -> List[dict]: :param project_id: The ID of the project :return: response """ - return self._session.request(METHODS.GET, f'get_templates/{project_id}') + return self._session.request(METHODS.GET, f"get_templates/{project_id}") -class Tests(BaseCategory): +class Tests(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-tests""" def get_test(self, test_id: int) -> dict: """ @@ -915,7 +988,7 @@ def get_test(self, test_id: int) -> dict: :param test_id: The ID of the test :return: response """ - return self._session.request(METHODS.GET, f'get_test/{test_id}') + return self._session.request(METHODS.GET, f"get_test/{test_id}") def get_tests(self, run_id: int, **kwargs) -> List[dict]: """ @@ -928,10 +1001,11 @@ def get_tests(self, run_id: int, **kwargs) -> List[dict]: :key status_id: int(list) - A comma-separated list of status IDs to filter by. :return: response """ - return self._session.request(METHODS.GET, f'get_tests/{run_id}', params=kwargs) + return self._session.request(METHODS.GET, f"get_tests/{run_id}", params=kwargs) -class Users(BaseCategory): +class Users(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-users""" def get_user(self, user_id: int) -> dict: """ @@ -942,7 +1016,7 @@ def get_user(self, user_id: int) -> dict: :param user_id: The ID of the user :return: response """ - return self._session.request(METHODS.GET, f'get_user/{user_id}') + return self._session.request(METHODS.GET, f"get_user/{user_id}") def get_user_by_email(self, email: str) -> dict: """ @@ -953,7 +1027,9 @@ def get_user_by_email(self, email: str) -> dict: :param email: The email address to get the user for :return: response """ - return self._session.request(METHODS.GET, f'get_user_by_email', params={'email': email}) + return self._session.request( + METHODS.GET, f"get_user_by_email", params={"email": email} + ) def get_users(self) -> List[dict]: """ @@ -963,10 +1039,11 @@ def get_users(self) -> List[dict]: :return: response """ - return self._session.request(METHODS.GET, 'get_users') + return self._session.request(METHODS.GET, "get_users") -class Attachments(BaseCategory): +class Attachments(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-attachments""" def add_attachment_to_result(self, result_id: int, path: Union[str, Path]) -> dict: """ @@ -978,25 +1055,9 @@ def add_attachment_to_result(self, result_id: int, path: Union[str, Path]) -> di :param path: The path to the file :return: response """ - return self._session.attachment_request(METHODS.POST, f'add_attachment_to_result/{result_id}', path) - - def add_attachment_to_result_for_case(self, result_id: int, case_id: int, path: Union[str, Path]): - """ - http://docs.gurock.com/testrail-api2/reference-attachments#add_attachment_to_result_for_case - - Adds attachment to a result based on a combination of result and test case IDs. - - :param result_id: The ID of the result the attachment should be added to - :param case_id: The ID of the test case - :param path: The path to the file - :return: response - """ - warnings.warn('Method removed from official documentation', DeprecationWarning, stacklevel=2) - # return self._session.attachment_request( - # METHODS.POST, - # f'add_attachment_to_result_for_case/{result_id}/{case_id}', - # path - # ) + return self._session.attachment_request( + METHODS.POST, f"add_attachment_to_result/{result_id}", path + ) def get_attachments_for_case(self, case_id: int) -> List[dict]: """ @@ -1007,7 +1068,7 @@ def get_attachments_for_case(self, case_id: int) -> List[dict]: :param case_id: The ID of the test case :return: response """ - return self._session.request(METHODS.GET, f'get_attachments_for_case/{case_id}') + return self._session.request(METHODS.GET, f"get_attachments_for_case/{case_id}") def get_attachments_for_test(self, test_id: int) -> List[dict]: """ @@ -1018,7 +1079,7 @@ def get_attachments_for_test(self, test_id: int) -> List[dict]: :param test_id: :return: response """ - return self._session.request(METHODS.GET, f'get_attachments_for_test/{test_id}') + return self._session.request(METHODS.GET, f"get_attachments_for_test/{test_id}") def get_attachment(self, attachment_id: int, path: Union[str, Path]) -> Path: """ @@ -1030,7 +1091,9 @@ def get_attachment(self, attachment_id: int, path: Union[str, Path]) -> Path: :param path: Path :return: Path """ - return self._session.get_attachment(METHODS.GET, f'get_attachment/{attachment_id}', path) + return self._session.get_attachment( + METHODS.GET, f"get_attachment/{attachment_id}", path + ) def delete_attachment(self, attachment_id: int) -> None: """ @@ -1041,10 +1104,11 @@ def delete_attachment(self, attachment_id: int) -> None: :param attachment_id: :return: None """ - return self._session.request(METHODS.POST, f'delete_attachment/{attachment_id}') + return self._session.request(METHODS.POST, f"delete_attachment/{attachment_id}") -class Reports(BaseCategory): +class Reports(_MetaCategory): + """http://docs.gurock.com/testrail-api2/reference-reports""" def get_reports(self, project_id: int) -> List[dict]: """ @@ -1055,7 +1119,7 @@ def get_reports(self, project_id: int) -> List[dict]: :param project_id: The ID of the project for which you want a list of API accessible reports :return: response """ - return self._session.request(METHODS.GET, f'get_reports/{project_id}') + return self._session.request(METHODS.GET, f"get_reports/{project_id}") def run_report(self, report_template_id: int) -> dict: """ @@ -1067,4 +1131,4 @@ def run_report(self, report_template_id: int) -> dict: :param report_template_id: :return: response """ - return self._session.request(METHODS.GET, f'run_report/{report_template_id}') + return self._session.request(METHODS.GET, f"run_report/{report_template_id}") diff --git a/testrail_api/_enums.py b/testrail_api/_enums.py index e335ec2..283e647 100644 --- a/testrail_api/_enums.py +++ b/testrail_api/_enums.py @@ -1,8 +1,10 @@ +"""Enums""" + from enum import Enum class METHODS(Enum): - GET = 'GET' - POST = 'POST' - PUT = 'PUT' - DELETE = 'DELETE' + """HTTP methods""" + + GET = "GET" + POST = "POST" diff --git a/testrail_api/_exception.py b/testrail_api/_exception.py index ea5643f..0a3f343 100644 --- a/testrail_api/_exception.py +++ b/testrail_api/_exception.py @@ -1,11 +1,13 @@ +"""Exceptions""" + + class TestRailError(Exception): """Base Exception""" - pass class TestRailAPIError(TestRailError): - pass + """Base API Exception""" class StatusCodeError(TestRailAPIError): - pass + """Status code Exception""" diff --git a/testrail_api/_session.py b/testrail_api/_session.py index 2422c87..5036f4c 100644 --- a/testrail_api/_session.py +++ b/testrail_api/_session.py @@ -1,32 +1,38 @@ +"""Base session""" + import logging import os import time from json.decoder import JSONDecodeError from pathlib import Path -from typing import Union, Optional +from typing import Optional, Union import requests -from .__version__ import __version__ +from . import __version__ from ._enums import METHODS from ._exception import StatusCodeError, TestRailError -log = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) RATE_LIMIT_TIMEOUT = 3 RATE_LIMIT_STATUS_CODE = 429 class Session: - _user_agent = f'Python TestRail API v: {__version__}' - - def __init__(self, - url: Optional[str] = None, - email: Optional[str] = None, - password: Optional[str] = None, - exc: bool = False, - rate_limit: bool = True, - **kwargs): + """Base Session""" + + _user_agent = f"Python TestRail API v: {__version__}" + + def __init__( # pylint: disable=R0913 + self, + url: Optional[str] = None, + email: Optional[str] = None, + password: Optional[str] = None, + exc: bool = False, + rate_limit: bool = True, + **kwargs, + ) -> None: """ :param url: TestRail address :param email: Email for the account on the TestRail @@ -37,40 +43,56 @@ def __init__(self, :key verify bool :key headers dict """ - _url = url or os.environ.get('TESTRAIL_URL') - _email = email or os.environ.get('TESTRAIL_EMAIL') - _password = password or os.environ.get('TESTRAIL_PASSWORD') + _url = url or os.environ.get("TESTRAIL_URL") + _email = email or os.environ.get("TESTRAIL_EMAIL") + _password = password or os.environ.get("TESTRAIL_PASSWORD") if not _url or not _email or not _password: - raise TestRailError('No url or email or password values set') - if _url.endswith('/'): + raise TestRailError("No url or email or password values set") + if _url.endswith("/"): _url = _url[:-1] - self.__base_url = f'{_url}/index.php?/api/v2/' - self.__timeout = kwargs.get('timeout', 30) + self.__base_url = f"{_url}/index.php?/api/v2/" + self.__timeout = kwargs.get("timeout", 30) self.__session = requests.Session() - self.__session.headers['User-Agent'] = self._user_agent - self.__session.headers.update(kwargs.get('headers', {})) - self.__session.verify = kwargs.get('verify', True) + self.__session.headers["User-Agent"] = self._user_agent + self.__session.headers.update(kwargs.get("headers", {})) + self.__session.verify = kwargs.get("verify", True) self.__user_email = _email self.__session.auth = (self.__user_email, _password) self.__exc = exc self._rate_limit = rate_limit - log.info( - 'Create Session{url: %s, user: %s, timeout: %s, headers: %s, verify: %s, exception: %s}', - url, self.__user_email, self.__timeout, self.__session.headers, self.__session.verify, self.__exc + LOGGER.info( + "Create Session{url: %s, user: %s, timeout: %s, headers: %s, verify: %s, exception: %s}", + url, + self.__user_email, + self.__timeout, + self.__session.headers, + self.__session.verify, + self.__exc, ) @property def user_email(self) -> str: + """get email""" return self.__user_email def __response(self, response: requests.Response): if not response.ok: - log.error('Code: %s, reason: %s url: %s, content: %s', - response.status_code, response.reason, response.url, response.content) + LOGGER.error( + "Code: %s, reason: %s url: %s, content: %s", + response.status_code, + response.reason, + response.url, + response.content, + ) if not self.__exc: - raise StatusCodeError(response.status_code, response.reason, response.url, response.content) + raise StatusCodeError( + response.status_code, + response.reason, + response.url, + response.content, + ) - log.debug('Response body: %s', response.text) + LOGGER.debug("Response body: %s", response.text) try: return response.json() except (JSONDecodeError, ValueError): @@ -78,40 +100,55 @@ def __response(self, response: requests.Response): def request(self, method: METHODS, src: str, raw: bool = False, **kwargs): """Base request method""" - url = f'{self.__base_url}{src}' - if not src.startswith('add_attachment'): - headers = kwargs.setdefault('headers', {}) - headers.update({'Content-Type': 'application/json'}) + url = f"{self.__base_url}{src}" + if not src.startswith("add_attachment"): + headers = kwargs.setdefault("headers", {}) + headers.update({"Content-Type": "application/json"}) + + if "params" in kwargs: + for key, value in kwargs["params"].items(): + if isinstance(value, list): + kwargs["params"][key] = ",".join([str(i) for i in value]) iterations = 3 for count in range(iterations): try: - response = self.__session.request(method=method.value, url=url, timeout=self.__timeout, **kwargs) + response = self.__session.request( + method=method.value, url=url, timeout=self.__timeout, **kwargs + ) except Exception as err: - log.error('%s', err, exc_info=True) + LOGGER.error("%s", err, exc_info=True) raise - if self._rate_limit and response.status_code == RATE_LIMIT_STATUS_CODE and count < iterations - 1: + if ( + self._rate_limit + and response.status_code == RATE_LIMIT_STATUS_CODE + and count < iterations - 1 + ): time.sleep(RATE_LIMIT_TIMEOUT) continue - log.debug('Response header: %s', response.headers) + LOGGER.debug("Response header: %s", response.headers) return response if raw else self.__response(response) @staticmethod def _path(path: Union[Path, str]) -> Path: return path if isinstance(path, Path) else Path(path) - def attachment_request(self, method: METHODS, src: str, file: Union[Path, str], **kwargs): - """""" + def attachment_request( + self, method: METHODS, src: str, file: Union[Path, str], **kwargs + ): + """Send attach""" file = self._path(file) - with file.open('rb') as attachment: - return self.request(method, src, files={'attachment': attachment}, **kwargs) + with file.open("rb") as attachment: + return self.request(method, src, files={"attachment": attachment}, **kwargs) - def get_attachment(self, method: METHODS, src: str, file: Union[Path, str], **kwargs) -> Path: - """""" + def get_attachment( + self, method: METHODS, src: str, file: Union[Path, str], **kwargs + ) -> Path: + """Downloads attach""" file = self._path(file) response = self.request(method, src, raw=True, **kwargs) if response.ok: - with file.open('wb') as attachment: + with file.open("wb") as attachment: attachment.write(response.content) return file return self.__response(response) diff --git a/testrail_api/_testrail_api.py b/testrail_api/_testrail_api.py index 89a6720..e01cd43 100644 --- a/testrail_api/_testrail_api.py +++ b/testrail_api/_testrail_api.py @@ -1,3 +1,4 @@ +# pylint: disable=R0401 """ TestRail API Categories """ @@ -7,6 +8,7 @@ class TestRailAPI(Session): + """Categories""" @property def attachments(self) -> _category.Attachments: diff --git a/tests/README.md b/tests/real/README.md similarity index 89% rename from tests/README.md rename to tests/real/README.md index ba99aab..1475f95 100644 --- a/tests/README.md +++ b/tests/real/README.md @@ -12,5 +12,5 @@ Set environment variables Run tests ```bash -py.test ./tests +py.test ./tests/real ``` \ No newline at end of file diff --git a/tests/attach.jpg b/tests/real/attach.jpg similarity index 100% rename from tests/attach.jpg rename to tests/real/attach.jpg diff --git a/tests/conftest.py b/tests/real/conftest.py similarity index 100% rename from tests/conftest.py rename to tests/real/conftest.py diff --git a/tests/pytest.ini b/tests/real/pytest.ini similarity index 62% rename from tests/pytest.ini rename to tests/real/pytest.ini index 0b8754d..259ecdb 100644 --- a/tests/pytest.ini +++ b/tests/real/pytest.ini @@ -1,6 +1,7 @@ [pytest] -addopts = -l - --cov=testrail_api tests/ +addopts = + -l + --cov=testrail_api tests/real console_output_style = classic log_file = pytest.log log_file_level = DEBUG \ No newline at end of file diff --git a/tests/test_cases.py b/tests/real/test_cases.py similarity index 95% rename from tests/test_cases.py rename to tests/real/test_cases.py index 9d98333..3f92520 100644 --- a/tests/test_cases.py +++ b/tests/real/test_cases.py @@ -3,7 +3,6 @@ from datetime import datetime import pytest -from requests.exceptions import ConnectTimeout from testrail_api import StatusCodeError @@ -119,7 +118,7 @@ def test_case_types(api): def test_configurations(default_project): api, project_id = default_project - config_group = api.configurations.add_config_group(project_id, 'config_group') + config_group = api.configurations.post_config(project_id, 'config_group') config = api.configurations.add_config(config_group['id'], 'config') api.configurations.update_config(config['id'], 'config2') @@ -239,20 +238,12 @@ def test_attachments(default_project_case): case_id = random.choice(case_ids) r = api.results.add_result_for_case(run_id, case_id, status_id=1, comment='pass', version='1') result_id, test_id = r['id'], r['test_id'] - r = api.attachments.add_attachment_to_result(result_id, './tests/attach.jpg') + r = api.attachments.add_attachment_to_result(result_id, './tests/real/attach.jpg') attachment_id = r['attachment_id'] api.attachments.get_attachments_for_case(case_id) api.attachments.get_attachments_for_test(test_id) - file = api.attachments.get_attachment(attachment_id, './tests/new_attach.jpg') + file = api.attachments.get_attachment(attachment_id, './tests/real/new_attach.jpg') file.unlink() api.attachments.delete_attachment(attachment_id) with pytest.raises(StatusCodeError): api.attachments.get_attachment(attachment_id, '') - - -def test_negative(time_out_api): - api = time_out_api - with pytest.raises(ConnectTimeout): - api.projects.get_projects() - with pytest.warns(DeprecationWarning): - api.attachments.add_attachment_to_result_for_case(1, 2, '.') diff --git a/tests/unit/attach.jpg b/tests/unit/attach.jpg new file mode 100644 index 0000000..f575ff8 Binary files /dev/null and b/tests/unit/attach.jpg differ diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..9d2a9c8 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path + +import pytest +import responses + +from testrail_api import TestRailAPI + + +@pytest.fixture(scope='session') +def host(): + yield 'https://example.testrail.com/' + + +@pytest.fixture(scope='session') +def base_path(): + path = Path(__file__).absolute().parent + yield str(path) + + +@pytest.fixture(scope='session') +def auth_data(host): + yield host, 'example@mail.com', 'password' + + +@pytest.fixture +def mock(): + with responses.RequestsMock() as resp: + yield resp + + +@pytest.fixture +def api(auth_data): + api = TestRailAPI(*auth_data) + yield api + + +@pytest.fixture +def environ(auth_data): + os.environ['TESTRAIL_URL'] = auth_data[0] + os.environ['TESTRAIL_EMAIL'] = auth_data[1] + os.environ['TESTRAIL_PASSWORD'] = auth_data[2] + yield + del os.environ['TESTRAIL_URL'] + del os.environ['TESTRAIL_EMAIL'] + del os.environ['TESTRAIL_PASSWORD'] diff --git a/tests/unit/pytest.ini b/tests/unit/pytest.ini new file mode 100644 index 0000000..b72f6c7 --- /dev/null +++ b/tests/unit/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = + -l + --cov=testrail_api tests/unit diff --git a/tests/unit/test_attachments.py b/tests/unit/test_attachments.py new file mode 100644 index 0000000..5666f44 --- /dev/null +++ b/tests/unit/test_attachments.py @@ -0,0 +1,108 @@ +import json +from functools import partial +from pathlib import Path + +import responses +from testrail_api import StatusCodeError +import pytest + + +def add_attachment(r): + assert 'multipart/form-data' in r.headers['Content-Type'] + assert r.headers['User-Agent'].startswith('Python TestRail API v:') + assert r.body + return 200, {}, json.dumps({'attachment_id': 433}) + + +def get_attachment(r, path): + file = Path(path, 'attach.jpg') + with file.open('rb') as f: + return 200, {}, f + + +def test_add_attachment_to_result_pathlib(api, mock, host, base_path): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_attachment_to_result/2', + add_attachment + ) + file = Path(base_path, 'attach.jpg') + resp = api.attachments.add_attachment_to_result(2, file) + assert resp['attachment_id'] == 433 + + +def test_add_attachment_to_result_str(api, mock, host, base_path): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_attachment_to_result/2', + add_attachment + ) + file = Path(base_path, 'attach.jpg') + resp = api.attachments.add_attachment_to_result(2, str(file)) + assert resp['attachment_id'] == 433 + + +def test_get_attachments_for_case(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_attachments_for_case/2', + lambda x: (200, {}, json.dumps([{'id': 1, 'filename': '444.jpg'}])) + ) + resp = api.attachments.get_attachments_for_case(2) + assert resp[0]['filename'] == '444.jpg' + + +def test_get_attachments_for_test(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_attachments_for_test/12', + lambda x: (200, {}, json.dumps([{'id': 1, 'filename': '444.jpg'}])) + ) + resp = api.attachments.get_attachments_for_test(12) + assert resp[0]['filename'] == '444.jpg' + + +def test_get_attachment(api, mock, host, base_path): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_attachment/433', + partial(get_attachment, path=base_path) + ) + file = Path(base_path, 'new_attach.jpg') + new_file = api.attachments.get_attachment(433, file) + assert new_file.exists() + new_file.unlink() + + +def test_get_attachment_str(api, mock, host, base_path): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_attachment/433', + partial(get_attachment, path=base_path) + ) + file = Path(base_path, 'new_attach_str.jpg') + new_file = api.attachments.get_attachment(433, str(file)) + assert new_file.exists() + new_file.unlink() + + +def test_get_attachment_error(api, mock, host, base_path): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_attachment/433', + lambda x: (400, {}, '') + ) + file = Path(base_path, 'new_attach_str.jpg') + with pytest.raises(StatusCodeError): + new_file = api.attachments.get_attachment(433, str(file)) + assert new_file is None + + +def test_delete_attachment(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_attachment/433', + lambda x: (200, {}, '') + ) + resp = api.attachments.delete_attachment(433) + assert resp is None diff --git a/tests/unit/test_case_fields.py b/tests/unit/test_case_fields.py new file mode 100644 index 0000000..a00c4c0 --- /dev/null +++ b/tests/unit/test_case_fields.py @@ -0,0 +1,31 @@ +import json + +import responses + + +def add_case_field(r): + data = json.loads(r.body) + return 200, {}, json.dumps( + {'id': 33, 'type_id': 12, 'label': data['label'], 'description': data['description']} + ) + + +def test_get_case_fields(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_case_fields', + lambda x: (200, {}, json.dumps([{'id': 1, 'description': 'The preconditions of this test case'}])) + ) + resp = api.case_fields.get_case_fields() + assert resp[0]['id'] == 1 + + +def test_add_case_field(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_case_field', + add_case_field + ) + resp = api.case_fields.add_case_field('Integer', 'My field', 'label', description='New field') + assert resp['label'] == 'label' + assert resp['description'] == 'New field' diff --git a/tests/unit/test_case_types.py b/tests/unit/test_case_types.py new file mode 100644 index 0000000..0fec3d9 --- /dev/null +++ b/tests/unit/test_case_types.py @@ -0,0 +1,14 @@ +import json + +import responses + + +def test_get_case_types(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_case_types', + lambda x: (200, {}, json.dumps([{'id': 1, 'name': 'Automated'}, {'id': 6, 'name': 'Other'}])) + ) + resp = api.case_types.get_case_types() + assert resp[0]['id'] == 1 + assert resp[1]['name'] == 'Other' diff --git a/tests/unit/test_cases.py b/tests/unit/test_cases.py new file mode 100644 index 0000000..2d1b037 --- /dev/null +++ b/tests/unit/test_cases.py @@ -0,0 +1,72 @@ +import json + +import responses + + +def get_cases(r): + assert r.params['suite_id'] + assert r.params['section_id'] + assert r.params['limit'] + assert r.params['offset'] + return 200, {}, json.dumps([{'id': 1, 'type_id': 1, 'title': 'My case'}]) + + +def add_case(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 1, 'title': data['title'], 'priority_id': data['priority_id']}) + + +def update_case(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 1, 'title': data['title']}) + + +def test_get_case(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_case/1', + lambda x: (200, {}, json.dumps({'id': 1, 'type_id': 1, 'title': 'My case'})), + ) + resp = api.cases.get_case(1) + assert resp['id'] == 1 + + +def test_get_cases(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + get_cases, + ) + resp = api.cases.get_cases(1, suite_id=2, section_id=3, limit=5, offset=10) + assert resp[0]['id'] == 1 + + +def test_add_case(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_case/2', + add_case, + ) + resp = api.cases.add_case(2, 'New case', priority_id=1) + assert resp['title'] == 'New case' + assert resp['priority_id'] == 1 + + +def test_update_case(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_case/1', + update_case, + ) + resp = api.cases.update_case(1, title='New case title') + assert resp['title'] == 'New case title' + + +def test_delete_case(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_case/5', + lambda x: (200, {}, ''), + ) + resp = api.cases.delete_case(5) + assert resp is None diff --git a/tests/unit/test_configurations.py b/tests/unit/test_configurations.py new file mode 100644 index 0000000..245bc59 --- /dev/null +++ b/tests/unit/test_configurations.py @@ -0,0 +1,78 @@ +import json + +import responses + + +def post_config(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 2, 'name': data['name']}) + + +def test_get_configs(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_configs/1', + lambda x: (200, {}, json.dumps([{'id': 1, 'name': 'Browsers', 'configs': []}])) + ) + resp = api.configurations.get_configs(1) + assert resp[0]['name'] == 'Browsers' + + +def test_add_config_group(api, mock, host): # no response example + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_config_group/5', + post_config + ) + resp = api.configurations.add_config_group(5, name='Python') + assert resp['name'] == 'Python' + + +def test_add_config(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_config/2', + post_config + ) + resp = api.configurations.add_config(1, 'TestRail') + assert resp['name'] == 'TestRail' + + +def test_update_config_group(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_config_group/3', + post_config + ) + resp = api.configurations.update_config_group(3, 'New Name') + assert resp['name'] == 'New Name' + + +def test_update_config(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_config/4', + post_config + ) + resp = api.configurations.update_config(4, 'New config name') + assert resp['name'] == 'New config name' + + +def test_delete_config_group(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_config_group/234', + lambda x: (200, {}, '') + ) + resp = api.configurations.delete_config_group(234) + assert resp is None + + +def test_delete_config(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_config/54', + lambda x: (200, {}, '') + ) + resp = api.configurations.delete_config(54) + assert resp is None diff --git a/tests/unit/test_milestone.py b/tests/unit/test_milestone.py new file mode 100644 index 0000000..2d78312 --- /dev/null +++ b/tests/unit/test_milestone.py @@ -0,0 +1,86 @@ +import json +from datetime import datetime + +import responses + + +def add_milestone(r): + req = json.loads(r.body) + req['id'] = 1 + return 200, {}, json.dumps(req) + + +def get_milestones(r): + req = r.params + assert req['is_started'] == '1' + return 200, {}, json.dumps([{'id': 1, 'name': 'Milestone 1', 'description': 'My new milestone'}]) + + +def update_milestone(r): + req = json.loads(r.body) + req['id'] = 1 + return 200, {}, json.dumps(req) + + +def test_get_milestone(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_milestone/1', + lambda x: (200, {}, json.dumps({'id': 1, 'name': 'Milestone 1', 'description': 'My new milestone'})), + content_type='application/json' + ) + response = api.milestones.get_milestone(1) + assert response['name'] == 'Milestone 1' + assert response['description'] == 'My new milestone' + + +def test_get_milestones(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_milestones/1', + get_milestones, + content_type='application/json' + ) + response = api.milestones.get_milestones(project_id=1, is_started=1) + assert response[0]['name'] == 'Milestone 1' + assert response[0]['description'] == 'My new milestone' + + +def test_add_milestone(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_milestone/1', + add_milestone, + content_type='application/json' + ) + response = api.milestones.add_milestone( + project_id=1, + name='New milestone', + start_on=int(datetime.now().timestamp()), + description='My new milestone' + ) + assert response['name'] == 'New milestone' + assert response['description'] == 'My new milestone' + + +def test_update_milestone(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_milestone/1', + update_milestone, + content_type='application/json' + ) + response = api.milestones.update_milestone(1, is_completed=True, parent_id=23) + assert response['is_completed'] is True + assert response['parent_id'] == 23 + + +def test_delete_milestone(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_milestone/1', + lambda x: (200, {}, ''), + content_type='application/json' + ) + response = api.milestones.delete_milestone(1) + assert response is None diff --git a/tests/unit/test_other.py b/tests/unit/test_other.py new file mode 100644 index 0000000..4862227 --- /dev/null +++ b/tests/unit/test_other.py @@ -0,0 +1,110 @@ +import json +import time + +import pytest +import responses +from requests.exceptions import ConnectionError + +from testrail_api import StatusCodeError, TestRailAPI as TRApi +from testrail_api._exception import TestRailError as TRError + + +class RateLimit: + + def __init__(self): + self.last = 0 + self.count = 0 + + def rate(self, r): + self.count += 1 + now = time.time() + if self.last == 0 or now - self.last < 3: + self.last = now + return 429, {}, '' + else: + return 200, {}, json.dumps({'count': self.count}) + + +def test_rate_limit(api, mock, host): + limit = RateLimit() + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + limit.rate, + ) + resp = api.cases.get_case(1) + assert resp['count'] == 2 + + +def test_raise_rate_limit(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + lambda x: (429, {}, ''), + ) + with pytest.raises(StatusCodeError): + api.cases.get_case(1) + + +def test_exc_raise_rate_limit(auth_data, mock, host): + api = TRApi(*auth_data, exc=True) + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + lambda x: (429, {}, ''), + ) + resp = api.cases.get_case(1) + assert resp is None + + +def test_exc_raise(auth_data, mock, host): + api = TRApi(*auth_data, exc=True) + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + lambda x: (400, {}, ''), + ) + resp = api.cases.get_case(1) + assert resp is None + + +def test_raise(auth_data, mock, host): + api = TRApi(*auth_data, exc=False) + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + lambda x: (400, {}, ''), + ) + with pytest.raises(StatusCodeError): + api.cases.get_case(1) + + +def test_no_response_raise(): + api = TRApi('http://asdadadsa.cd', 'asd@asd.com', 'asdasda', exc=False) + with pytest.raises(ConnectionError): + api.cases.get_case(1) + + +def test_get_email(): + email = 'asd@asd.com' + api = TRApi('http://asdadadsa.cd', 'asd@asd.com', 'asdasda', exc=False) + assert api.user_email == email + + +@pytest.mark.parametrize('field', ('url', 'email', 'password')) +def test_raise_no_arg(field): + data = {'url': 'http://asdadadsa.cd', 'email': 'asd@asd.com', 'password': 'asdasda'} + del data[field] + with pytest.raises(TRError): + TRApi(**data) + + +def test_environment_variables(environ, mock, host): + api = TRApi() + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_cases/1', + lambda x: (200, {}, json.dumps({'id': 1})), + ) + resp = api.cases.get_case(1) + assert resp['id'] == 1 diff --git a/tests/unit/test_plans.py b/tests/unit/test_plans.py new file mode 100644 index 0000000..d254ee9 --- /dev/null +++ b/tests/unit/test_plans.py @@ -0,0 +1,118 @@ +import json + +import responses + + +def get_plans(r): + assert r.params['is_completed'] == '1' + return 200, {}, json.dumps([{'id': 5, 'name': 'System test'}]) + + +def add_plan(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 96, 'name': data['name'], 'milestone_id': data['milestone_id']}) + + +def add_plan_entry(r): + data = json.loads(r.body) + assert data['include_all'] is True + assert data['config_ids'] == [1, 2, 3] + return 200, {}, json.dumps({'id': 5, 'name': 'System test'}) + + +def update_plan_entry(r): + data = json.loads(r.body) + assert data['case_ids'] == [2, 3] + return 200, {}, json.dumps({'id': 7, 'name': data['name']}) + + +def test_get_plan(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_plan/5', + lambda x: (200, {}, json.dumps({'id': 5, 'name': 'System test'})) + ) + resp = api.plans.get_plan(5) + assert resp['id'] == 5 + + +def test_get_plans(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_plans/7', + get_plans + ) + resp = api.plans.get_plans(7, is_completed=1) + assert resp[0]['id'] == 5 + + +def test_add_plan(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_plan/5', + add_plan + ) + resp = api.plans.add_plan(5, name='new plan', milestone_id=4) + assert resp['name'] == 'new plan' + assert resp['milestone_id'] == 4 + + +def test_add_plan_entry(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_plan_entry/7', + add_plan_entry + ) + resp = api.plans.add_plan_entry(7, 3, include_all=True, config_ids=[1, 2, 3]) + assert resp['id'] == 5 + + +def test_update_plan(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_plan/12', + add_plan + ) + resp = api.plans.update_plan(12, name='update', milestone_id=1) + assert resp['name'] == 'update' + assert resp['milestone_id'] == 1 + + +def test_update_plan_entry(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_plan_entry/7/1', + update_plan_entry + ) + resp = api.plans.update_plan_entry(7, 1, name='Update name', case_ids=[2, 3]) + assert resp['name'] == 'Update name' + + +def test_close_plan(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/close_plan/7', + lambda x: (200, {}, json.dumps({'id': 7, 'name': 'System test'})) + ) + resp = api.plans.close_plan(7) + assert resp['id'] == 7 + + +def test_delete_plan(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_plan/11', + lambda x: (200, {}, '') + ) + resp = api.plans.delete_plan(11) + assert resp is None + + +def test_delete_plan_entry(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_plan_entry/12/2', + lambda x: (200, {}, '') + ) + resp = api.plans.delete_plan_entry(12, 2) + assert resp is None diff --git a/tests/unit/test_priorities.py b/tests/unit/test_priorities.py new file mode 100644 index 0000000..4f1bb16 --- /dev/null +++ b/tests/unit/test_priorities.py @@ -0,0 +1,15 @@ +import json + +import responses + + +def test_get_priorities(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_priorities', + lambda x: (200, {}, json.dumps([{'id': 1, 'priority': 1}, {'id': 4, 'priority': 4}])) + ) + + resp = api.priorities.get_priorities() + assert resp[0]['id'] == 1 + assert resp[1]['priority'] == 4 diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py new file mode 100644 index 0000000..372411d --- /dev/null +++ b/tests/unit/test_projects.py @@ -0,0 +1,68 @@ +import json + +import responses + + +def get_projects(r): + assert r.params['is_completed'] == '0' + return 200, {}, json.dumps([{'id': 1, 'name': 'Datahub'}]) + + +def add_project(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 1, 'name': data['name']}) + + +def update_project(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 1, 'name': 'Datahub', 'is_completed': data['is_completed']}) + + +def test_get_project(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_project/1', + lambda x: (200, {}, json.dumps({'id': 1, 'name': 'Datahub'})) + ) + resp = api.projects.get_project(1) + assert resp['name'] == 'Datahub' + + +def test_get_projects(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_projects', + get_projects, + ) + resp = api.projects.get_projects(is_completed=0) + assert resp[0]['name'] == 'Datahub' + + +def test_add_project(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_project', + add_project, + ) + resp = api.projects.add_project('My project', announcement='description', show_announcement=True, suite_mode=1) + assert resp['name'] == 'My project' + + +def test_update_project(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_project/1', + update_project, + ) + resp = api.projects.update_project(1, is_completed=True) + assert resp['is_completed'] is True + + +def test_delete_project(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_project/1', + lambda x: (200, {}, ''), + ) + resp = api.projects.delete_project(1) + assert resp is None diff --git a/tests/unit/test_reports.py b/tests/unit/test_reports.py new file mode 100644 index 0000000..466a613 --- /dev/null +++ b/tests/unit/test_reports.py @@ -0,0 +1,23 @@ +import json + +import responses + + +def test_get_reports(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_reports/1', + lambda x: (200, {}, json.dumps([{'id': 1, 'name': 'Activity Summary'}])) + ) + response = api.reports.get_reports(1) + assert response[0]['name'] == 'Activity Summary' + + +def test_run_report(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/run_report/1', + lambda x: (200, {}, json.dumps({'report_url': 'https://...383'})) + ) + response = api.reports.run_report(1) + assert response['report_url'] == 'https://...383' diff --git a/tests/unit/test_result_fields.py b/tests/unit/test_result_fields.py new file mode 100644 index 0000000..eaee47c --- /dev/null +++ b/tests/unit/test_result_fields.py @@ -0,0 +1,13 @@ +import json + +import responses + + +def test_get_result_fields(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_result_fields', + lambda x: (200, {}, json.dumps([{'id': 1, 'configs': []}])) + ) + resp = api.result_fields.get_result_fields() + assert resp[0]['id'] == 1 diff --git a/tests/unit/test_results.py b/tests/unit/test_results.py new file mode 100644 index 0000000..36d4498 --- /dev/null +++ b/tests/unit/test_results.py @@ -0,0 +1,102 @@ +import json + +import pytest +import responses + + +def get_results(r): + assert r.params['limit'] == '3' + assert r.params['status_id'] == '1,2,3' + return 200, {}, json.dumps([{'id': 1, 'status_id': 2, 'test_id': 1}]) + + +def add_result(r): + data = json.loads(r.body) + return 200, {}, json.dumps( + {'id': 1, 'status_id': data['status_id'], 'test_id': 15, 'assignedto_id': data['assignedto_id'], + 'comment': data['comment']} + ) + + +def add_results(r): + data = json.loads(r.body) + return 200, {}, json.dumps(data['results']) + + +@pytest.mark.parametrize('status_id', ('1,2,3', [1, 2, 3])) +def test_get_results(api, mock, host, status_id): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_results/221', + get_results + ) + resp = api.results.get_results(221, limit=3, status_id=status_id) + assert resp[0]['status_id'] == 2 + + +@pytest.mark.parametrize('status_id', ('1,2,3', [1, 2, 3])) +def test_get_results_for_case(api, mock, host, status_id): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_results_for_case/23/2567', + get_results + ) + resp = api.results.get_results_for_case(23, 2567, limit=3, status_id=status_id) + assert resp[0]['status_id'] == 2 + + +@pytest.mark.parametrize('status_id', ('1,2,3', [1, 2, 3])) +def test_get_results_for_run(api, mock, host, status_id): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_results_for_run/12', + get_results + ) + resp = api.results.get_results_for_run(12, limit=3, status_id=status_id) + assert resp[0]['status_id'] == 2 + + +def test_add_result(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_result/15', + add_result + ) + resp = api.results.add_result(15, status_id=5, comment='Fail', assignedto_id=1) + assert resp['status_id'] == 5 + assert resp['comment'] == 'Fail' + assert resp['assignedto_id'] == 1 + + +def test_add_result_for_case(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_result_for_case/3/34', + add_result + ) + resp = api.results.add_result_for_case(3, 34, status_id=1, comment='Passed', assignedto_id=1) + assert resp['status_id'] == 1 + assert resp['comment'] == 'Passed' + assert resp['assignedto_id'] == 1 + + +def test_add_results(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_results/15', + add_results + ) + results = [{'test_id': 1, 'status_id': 5}, {'test_id': 2, 'status_id': 1}] + resp = api.results.add_results(12, results=results) + assert resp == results + + +def test_add_results_for_cases(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_results_for_cases/18', + add_results + ) + results = [{'case_id': 1, 'status_id': 5}, {'case_id': 2, 'status_id': 1}] + resp = api.results.add_results_for_cases(18, results) + assert resp == results diff --git a/tests/unit/test_runs.py b/tests/unit/test_runs.py new file mode 100644 index 0000000..85fa196 --- /dev/null +++ b/tests/unit/test_runs.py @@ -0,0 +1,74 @@ +import json + +import responses + + +def add_run(r): + data = json.loads(r.body) + return 200, {}, json.dumps( + {'id': 25, 'suite_id': data['suite_id'], 'name': data['name'], 'milestone_id': data['milestone_id']} + ) + + +def test_get_run(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_run/1', + lambda x: (200, {}, json.dumps({'id': 1, 'name': 'My run'})) + ) + resp = api.runs.get_run(1) + assert resp['id'] == 1 + + +def test_get_runs(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_runs/12', + lambda x: (200, {}, json.dumps([{'id': 1, 'name': 'My run', 'is_completed': x.params['is_completed']}])) + ) + resp = api.runs.get_runs(12, is_completed=1) + assert resp[0]['is_completed'] == '1' + + +def test_add_run(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_run/12', + add_run + ) + resp = api.runs.add_run(12, suite_id=1, name='New Run', milestone_id=1) + assert resp['suite_id'] == 1 + assert resp['name'] == 'New Run' + assert resp['milestone_id'] == 1 + + +def test_update_run(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_run/15', + add_run + ) + resp = api.runs.update_run(15, suite_id=1, name='New Run', milestone_id=1) + assert resp['suite_id'] == 1 + assert resp['name'] == 'New Run' + assert resp['milestone_id'] == 1 + + +def test_close_run(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/close_run/3', + lambda x: (200, {}, json.dumps({'id': 3, 'is_completed': True})) + ) + resp = api.runs.close_run(3) + assert resp['is_completed'] is True + + +def test_delete_run(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_run/2', + lambda x: (200, {}, '') + ) + resp = api.runs.delete_run(2) + assert resp is None diff --git a/tests/unit/test_sections.py b/tests/unit/test_sections.py new file mode 100644 index 0000000..c9fb6b4 --- /dev/null +++ b/tests/unit/test_sections.py @@ -0,0 +1,60 @@ +import json + +import responses + + +def add_section(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 2, 'name': data['name'], 'description': data['description']}) + + +def test_get_section(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_section/3', + lambda x: (200, {}, json.dumps({'depth': 1, 'description': 'My section'})) + ) + resp = api.sections.get_section(3) + assert resp['depth'] == 1 + + +def test_get_sections(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_sections/5&suite_id=2', + lambda x: (200, {}, json.dumps([{'depth': 1, 'description': 'My section'}])) + ) + resp = api.sections.get_sections(5, suite_id=2) + assert resp[0]['depth'] == 1 + + +def test_add_section(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_section/4', + add_section + ) + resp = api.sections.add_section(4, 'new section', suite_id=2, description='Description') + assert resp['name'] == 'new section' + assert resp['description'] == 'Description' + + +def test_update_section(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_section/2', + add_section + ) + resp = api.sections.update_section(2, name='new_name', description='new_description') + assert resp['name'] == 'new_name' + assert resp['description'] == 'new_description' + + +def test_delete_section(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_section/2', + lambda x: (200, {}, '') + ) + resp = api.sections.delete_section(2) + assert resp is None diff --git a/tests/unit/test_statuses.py b/tests/unit/test_statuses.py new file mode 100644 index 0000000..5fd2f4c --- /dev/null +++ b/tests/unit/test_statuses.py @@ -0,0 +1,14 @@ +import json + +import responses + + +def test_get_statuses(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_statuses', + lambda x: (200, {}, json.dumps([{'id': 1, 'label': 'Passed'}, {'id': 5, 'label': 'Failed'}])) + ) + resp = api.statuses.get_statuses() + assert resp[0]['id'] == 1 + assert resp[1]['label'] == 'Failed' diff --git a/tests/unit/test_suites.py b/tests/unit/test_suites.py new file mode 100644 index 0000000..ac31308 --- /dev/null +++ b/tests/unit/test_suites.py @@ -0,0 +1,61 @@ +import json + +import responses + + +def add_suite(r): + data = json.loads(r.body) + return 200, {}, json.dumps({'id': 1, 'name': data['name'], 'description': data['description']}) + + +def test_get_suite(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_suite/4', + lambda x: (200, {}, json.dumps({'id': 4, 'description': 'My suite'})) + ) + resp = api.suites.get_suite(4) + assert resp['id'] == 4 + + +def test_get_suites(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_suites/5', + lambda x: (200, {}, json.dumps([{'id': 1, 'description': 'Suite1'}, {'id': 2, 'description': 'Suite2'}])) + ) + resp = api.suites.get_suites(5) + assert resp[0]['id'] == 1 + assert resp[1]['description'] == 'Suite2' + + +def test_add_suite(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/add_suite/7', + add_suite + ) + resp = api.suites.add_suite(5, 'New suite', description='My new suite') + assert resp['name'] == 'New suite' + assert resp['description'] == 'My new suite' + + +def test_update_suite(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/update_suite/4', + add_suite + ) + resp = api.suites.update_suite(4, name='new name', description='new description') + assert resp['name'] == 'new name' + assert resp['description'] == 'new description' + + +def test_delete_suite(api, mock, host): + mock.add_callback( + responses.POST, + f'{host}index.php?/api/v2/delete_suite/4', + lambda x: (200, {}, '') + ) + resp = api.suites.delete_suite(4) + assert resp is None diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py new file mode 100644 index 0000000..6f89a78 --- /dev/null +++ b/tests/unit/test_template.py @@ -0,0 +1,14 @@ +import json + +import responses + + +def test_get_templates(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_templates/1', + lambda x: (200, {}, json.dumps([{'id': 1, 'name': 'Test Case (Text)'}, {'id': 2, 'name': 'Test Case (Steps)'}])) + ) + resp = api.templates.get_templates(1) + assert resp[0]['id'] == 1 + assert resp[1]['name'] == 'Test Case (Steps)' diff --git a/tests/unit/test_tests.py b/tests/unit/test_tests.py new file mode 100644 index 0000000..a8d33d3 --- /dev/null +++ b/tests/unit/test_tests.py @@ -0,0 +1,31 @@ +import json + +import pytest +import responses + + +def get_tests(r): + resp = [{'id': c, 'status_id': int(i)} for c, i in enumerate(r.params['status_id'].split(','), 1)] + return 200, {}, json.dumps(resp) + + +def test_get_test(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_test/2', + lambda x: (200, {}, json.dumps({'case_id': 1, 'id': 2, 'run_id': 2})) + ) + resp = api.tests.get_test(2) + assert resp['case_id'] == 1 + + +@pytest.mark.parametrize('status_id', ('1,5', [1, 5])) +def test_get_tests(api, mock, host, status_id): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_tests/2', + get_tests + ) + resp = api.tests.get_tests(2, status_id=status_id) + assert resp[0]['status_id'] == 1 + assert resp[1]['status_id'] == 5 diff --git a/tests/unit/test_users.py b/tests/unit/test_users.py new file mode 100644 index 0000000..2c2746a --- /dev/null +++ b/tests/unit/test_users.py @@ -0,0 +1,36 @@ +import json + +import responses + + +def test_get_user(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_user/1', + lambda x: (200, {}, json.dumps({'email': 'testrail@ff.com', 'id': 1, 'name': 'John Smith', 'is_active': True})) + ) + response = api.users.get_user(1) + assert response['name'] == 'John Smith' + + +def test_get_user_by_email(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_user_by_email', + lambda x: (200, {}, json.dumps({'email': x.params["email"], 'id': 1, 'name': 'John Smith', 'is_active': True})) + ) + email = 'testrail@cc.cc' + response = api.users.get_user_by_email(email) + assert response['email'] == email + + +def test_get_users(api, mock, host): + mock.add_callback( + responses.GET, + f'{host}index.php?/api/v2/get_users', + lambda x: ( + 200, {}, json.dumps([{'email': 'testrail@ff.com', 'id': 1, 'name': 'John Smith', 'is_active': True}]) + ), + ) + response = api.users.get_users() + assert response[0]['name'] == 'John Smith'