├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.rst ├── data ├── HTTP_GET.json ├── HTTP_GET.yaml ├── HTTP_GET_SAVE_RESPONSE.json └── MULTIPLE_HTTP_REQUESTS.json ├── pyhttptest ├── __init__.py ├── cli.py ├── constants.py ├── core.py ├── decorators.py ├── exceptions.py ├── http.py ├── http_schemas │ ├── __init__.py │ ├── base_schema.py │ ├── delete_schema.py │ ├── get_schema.py │ ├── post_schema.py │ └── put_schema.py ├── logger.py ├── printer.py ├── utils.py └── wrappers.py ├── setup.py └── tests ├── __init__.py ├── http_schemas ├── __init__.py ├── test_base_schema.py ├── test_delete_schema.py ├── test_get_schema.py ├── test_post_schema.py └── test_put_schema.py ├── test_cli.py ├── test_core.py ├── test_decorators.py ├── test_exceptions.py ├── test_http.py ├── test_printer.py ├── test_utils.py └── test_wrappers.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | __pycache__/ 4 | .idea/ 5 | .DS_Store 6 | env/ 7 | venv/ 8 | *.egg-info/ 9 | *.eggs/ 10 | build/ 11 | dist/ 12 | docs/_build/ 13 | .pytest_cache/ 14 | .tox/ 15 | .coverage 16 | .coveragerc 17 | .coverage.* 18 | htmlcov/ 19 | .flake8 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | - 3.6 5 | - 3.7 6 | - 3.8 7 | 8 | install: "make" 9 | 10 | script: 11 | - make test 12 | 13 | cache: pip 14 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | This document records all notable changes to *pyhttptest*. 2 | 3 | 0.5 (19.03.2020) 4 | --------------------- 5 | 6 | * Modified the format of result printed on the screen 7 | 8 | 0.4 (23.01.2020) 9 | --------------------- 10 | 11 | * Fixed improperly-working functionality that validates data against JSON Schema 12 | 13 | 0.3 (24.12.2019) 14 | --------------------- 15 | 16 | * Improved JSON parsing speed 17 | * Fixed YAJL import error on OS X 18 | 19 | 0.2 (10.12.2019) 20 | --------------------- 21 | 22 | * Added handling and processing multiple test cases define in a .json file 23 | * Fixed handling and processing an HTTP DELETE Request 24 | * Fixed handling and processing an HTTP PUT Request 25 | 26 | 0.1 (19.11.2019) 27 | --------------------- 28 | 29 | * Added handling and processing an HTTP DELETE Request 30 | * Added JSON Schema to validate an HTTP DELETE Request 31 | * Added handling and processing an HTTP PUT Request 32 | * Added JSON Schema to validate an HTTP PUT Request 33 | * Improved code coverage percentage 34 | * Official public release 35 | 36 | 0.1b (28.10.2019) 37 | --------------------- 38 | 39 | * Added handling and processing an HTTP POST Request 40 | * Added JSON Schema to validate an HTTP POST Request 41 | * Fixed sending an HTTP Request Headers 42 | * Fixed sending query parameters in URLs 43 | 44 | 0.1a (22.10.2019) 45 | --------------------- 46 | 47 | * Initial public pre-release 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at slavov.iliyan96@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Contributing to pyhttptest 3 | ============================= 4 | 5 | As an open source project, pyhttptest welcomes contributions of many forms. 6 | 7 | Examples of contributions include: 8 | 9 | * Features 10 | * Test cases 11 | * Code comments improvements 12 | * Bug reports and features reviews 13 | 14 | 15 | Conventions to follow 16 | ============================= 17 | 18 | * Test Driven Development approach 19 | * Comment the source code 20 | * `Style Guide for Python Code`_ (PEP8) 21 | 22 | 23 | How to 24 | ============================= 25 | 26 | Follow the `simple guide`_ to do your first contribution. 27 | 28 | 29 | .. _Style Guide for Python Code: https://python.org/dev/peps/pep-0008/ 30 | .. _simple guide: https://github.com/firstcontributions/first-contributions 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Iliyan Slavov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install pipenv 3 | pipenv install --dev 4 | 5 | flake8: 6 | pipenv run flake8 7 | 8 | test: 9 | pipenv run pytest tests 10 | 11 | coverage: 12 | pipenv run pytest --cov 13 | 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | flake8 = "*" 8 | pytest-cov = "*" 9 | 10 | [packages] 11 | pytest = "*" 12 | ijson = "*" 13 | jsonschema = "*" 14 | requests = "*" 15 | tabulate = "*" 16 | click = "*" 17 | zipp = "==0.6" 18 | 19 | [requires] 20 | python_version = "3.7" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b119ee460fcec1e12d04b3df8c9df17bcaf78164be92ddc94826fe1bb0e3a7ed" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "attrs": { 20 | "hashes": [ 21 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 22 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 23 | ], 24 | "version": "==19.3.0" 25 | }, 26 | "certifi": { 27 | "hashes": [ 28 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 29 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 30 | ], 31 | "version": "==2020.4.5.1" 32 | }, 33 | "chardet": { 34 | "hashes": [ 35 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 36 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 37 | ], 38 | "version": "==3.0.4" 39 | }, 40 | "click": { 41 | "hashes": [ 42 | "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", 43 | "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" 44 | ], 45 | "index": "pypi", 46 | "version": "==7.1.1" 47 | }, 48 | "idna": { 49 | "hashes": [ 50 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 51 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 52 | ], 53 | "version": "==2.9" 54 | }, 55 | "ijson": { 56 | "hashes": [ 57 | "sha256:097daa03bc94426fc1186e695e9caaf4bb072cf88f4d14a69fb47b7594be5d8f", 58 | "sha256:39183885e098db9fff55ce861ddd33424863c3721412d624002c9fb3acba5408", 59 | "sha256:3ea29d61983f941b325ab42e38d8cfac2d87d52a72427906de0e5b45584e94f9", 60 | "sha256:46011b6c0ccfd20f2e6c69a223df614bdc32722aed7001826d5d72c2cdf66370", 61 | "sha256:4a68beb8aeb229585fc2871930969574bcd9337da40c8550c2df0b470d8456d0", 62 | "sha256:51fad287625060b73a359bc36a1a90b300ce6b19daf1eb6ac1fb6fbfff78c6b6", 63 | "sha256:56cdc2d8ba055b2c4f72c402b445416ce5c19521ebb73f58c5c536b1601241a8", 64 | "sha256:6a8457f7730d98421fa8852eae14238d9310a06f2e42033bcdc0de3690fd3a99", 65 | "sha256:934265fa0f6769b47d2fef8c36123503a27c320fa6933054bf332719af5c9808", 66 | "sha256:943ed9c261144fb1a07ad5119768be2cd0480f0d29c6513fa08b02000e7b1911", 67 | "sha256:9e47584e45919ff582ccc2c5c5d1d84c70aa4c3c5387cc102aa9aeef1cf20a15", 68 | "sha256:a1e6e074bb3fc1d7ad32d9dad2af2d3977b655b525c7b955c0b13dbf14dce49c", 69 | "sha256:aa86385fe83380a738cb56fceb704be9dce225e3729aff51083e36e2493d4fe8", 70 | "sha256:c229a68d1fc8d389430d1a12eb94b2843809092ac5644bf09d0a545542e9dac7", 71 | "sha256:c35b19ff2bf4528a94b674e451c305482a74ad13b6ad2e4f0813c0ddc001a1b6", 72 | "sha256:d2308baca46fddf7672d10b77da440663314f82534f404518d4c13f90d3b878c", 73 | "sha256:d9bdebbb61c72a04dc7272a8ca2fc99a43eaec6683956ded8e78e07525391004", 74 | "sha256:de3cb8296bac4ce4a48f4a55af37d9a003714c05a88c1b4fcecdd0d4145404e8", 75 | "sha256:e1c1a7f9d7d402b76a7e4860b19517543e24b660cb7a58008e76fb0eec35f4ee", 76 | "sha256:f5cb0529cae6a9ab21f8d6f27e1599c148dde003d0fe7d5cb1f607c611bf093f" 77 | ], 78 | "index": "pypi", 79 | "version": "==3.0.3" 80 | }, 81 | "importlib-metadata": { 82 | "hashes": [ 83 | "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", 84 | "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" 85 | ], 86 | "markers": "python_version < '3.8'", 87 | "version": "==1.6.0" 88 | }, 89 | "jsonschema": { 90 | "hashes": [ 91 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 92 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 93 | ], 94 | "index": "pypi", 95 | "version": "==3.2.0" 96 | }, 97 | "more-itertools": { 98 | "hashes": [ 99 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 100 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 101 | ], 102 | "version": "==8.2.0" 103 | }, 104 | "packaging": { 105 | "hashes": [ 106 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 107 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 108 | ], 109 | "version": "==20.3" 110 | }, 111 | "pluggy": { 112 | "hashes": [ 113 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 114 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 115 | ], 116 | "version": "==0.13.1" 117 | }, 118 | "py": { 119 | "hashes": [ 120 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 121 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 122 | ], 123 | "version": "==1.8.1" 124 | }, 125 | "pyparsing": { 126 | "hashes": [ 127 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 128 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 129 | ], 130 | "version": "==2.4.7" 131 | }, 132 | "pyrsistent": { 133 | "hashes": [ 134 | "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" 135 | ], 136 | "version": "==0.16.0" 137 | }, 138 | "pytest": { 139 | "hashes": [ 140 | "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", 141 | "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" 142 | ], 143 | "index": "pypi", 144 | "version": "==5.4.1" 145 | }, 146 | "requests": { 147 | "hashes": [ 148 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 149 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 150 | ], 151 | "index": "pypi", 152 | "version": "==2.23.0" 153 | }, 154 | "six": { 155 | "hashes": [ 156 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 157 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 158 | ], 159 | "version": "==1.14.0" 160 | }, 161 | "tabulate": { 162 | "hashes": [ 163 | "sha256:ac64cb76d53b1231d364babcd72abbb16855adac7de6665122f97b593f1eb2ba", 164 | "sha256:db2723a20d04bcda8522165c73eea7c300eda74e0ce852d9022e0159d7895007" 165 | ], 166 | "index": "pypi", 167 | "version": "==0.8.7" 168 | }, 169 | "urllib3": { 170 | "hashes": [ 171 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 172 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 173 | ], 174 | "version": "==1.25.9" 175 | }, 176 | "wcwidth": { 177 | "hashes": [ 178 | "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", 179 | "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" 180 | ], 181 | "version": "==0.1.9" 182 | }, 183 | "zipp": { 184 | "hashes": [ 185 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 186 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 187 | ], 188 | "index": "pypi", 189 | "version": "==0.6.0" 190 | } 191 | }, 192 | "develop": { 193 | "attrs": { 194 | "hashes": [ 195 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 196 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 197 | ], 198 | "version": "==19.3.0" 199 | }, 200 | "coverage": { 201 | "hashes": [ 202 | "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", 203 | "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", 204 | "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", 205 | "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", 206 | "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", 207 | "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", 208 | "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", 209 | "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", 210 | "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", 211 | "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", 212 | "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", 213 | "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", 214 | "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", 215 | "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", 216 | "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", 217 | "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", 218 | "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", 219 | "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", 220 | "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", 221 | "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", 222 | "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", 223 | "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", 224 | "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", 225 | "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", 226 | "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", 227 | "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", 228 | "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", 229 | "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", 230 | "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", 231 | "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", 232 | "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" 233 | ], 234 | "version": "==5.1" 235 | }, 236 | "entrypoints": { 237 | "hashes": [ 238 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 239 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 240 | ], 241 | "version": "==0.3" 242 | }, 243 | "flake8": { 244 | "hashes": [ 245 | "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", 246 | "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" 247 | ], 248 | "index": "pypi", 249 | "version": "==3.7.9" 250 | }, 251 | "importlib-metadata": { 252 | "hashes": [ 253 | "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", 254 | "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" 255 | ], 256 | "markers": "python_version < '3.8'", 257 | "version": "==1.6.0" 258 | }, 259 | "mccabe": { 260 | "hashes": [ 261 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 262 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 263 | ], 264 | "version": "==0.6.1" 265 | }, 266 | "more-itertools": { 267 | "hashes": [ 268 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 269 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 270 | ], 271 | "version": "==8.2.0" 272 | }, 273 | "packaging": { 274 | "hashes": [ 275 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 276 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 277 | ], 278 | "version": "==20.3" 279 | }, 280 | "pluggy": { 281 | "hashes": [ 282 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 283 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 284 | ], 285 | "version": "==0.13.1" 286 | }, 287 | "py": { 288 | "hashes": [ 289 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 290 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 291 | ], 292 | "version": "==1.8.1" 293 | }, 294 | "pycodestyle": { 295 | "hashes": [ 296 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 297 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 298 | ], 299 | "version": "==2.5.0" 300 | }, 301 | "pyflakes": { 302 | "hashes": [ 303 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 304 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 305 | ], 306 | "version": "==2.1.1" 307 | }, 308 | "pyparsing": { 309 | "hashes": [ 310 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 311 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 312 | ], 313 | "version": "==2.4.7" 314 | }, 315 | "pytest": { 316 | "hashes": [ 317 | "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", 318 | "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" 319 | ], 320 | "index": "pypi", 321 | "version": "==5.4.1" 322 | }, 323 | "pytest-cov": { 324 | "hashes": [ 325 | "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", 326 | "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" 327 | ], 328 | "index": "pypi", 329 | "version": "==2.8.1" 330 | }, 331 | "six": { 332 | "hashes": [ 333 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 334 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 335 | ], 336 | "version": "==1.14.0" 337 | }, 338 | "wcwidth": { 339 | "hashes": [ 340 | "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", 341 | "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" 342 | ], 343 | "version": "==0.1.9" 344 | }, 345 | "zipp": { 346 | "hashes": [ 347 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 348 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 349 | ], 350 | "index": "pypi", 351 | "version": "==0.6.0" 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyhttptest: HTTP tests over RESTful APIs✨ 2 | ########################################## 3 | 4 | .. image:: https://travis-ci.org/slaily/pyhttptest.svg?branch=master 5 | :target: https://travis-ci.org/slaily/pyhttptest 6 | 7 | Pissed about writing test scripts against your RESTFul APIs anytime? 8 | 9 | Describe an HTTP Requests test cases in a simplest and widely used format JSON within a file. 10 | 11 | Run one command and gain a summary report. 12 | 13 | **WARNING⚠️** 14 | The project is in search of any kind of contributor. Due to my commitment to managing many other projects, still **pyhttptest** lacking introducing features requested from users. The project has a lot of potentials to be useful and used on a daily basis. Glad to receive any help and discuss the future of **pyhttptest**. Contact me by email: **iliyan@cosense.ai**. 15 | 16 | 17 | 📣 **Coverage measuring on Test Cases coming soon** 18 | 19 | 20 | .. image:: https://www.dropbox.com/s/cd0g07dop4j1riq/pyhttptest-cli-table-of-results.png?raw=1 21 | :alt: pyhttptest in the command line 22 | :width: 100% 23 | :align: center 24 | 25 | 26 | Installation 27 | ****************************************** 28 | 29 | Recommended installation method is to use ``pip``: 30 | 31 | .. code-block:: bash 32 | 33 | $ pip install pyhttptest 34 | 35 | Python version **3+** is required. 36 | 37 | 38 | Usage 39 | ****************************************** 40 | 41 | .. code-block:: bash 42 | 43 | $ pyhttptest execute FILE 44 | 45 | See also ``pyhttptest --help``. 46 | 47 | 48 | Examples 49 | ****************************************** 50 | 51 | Single test case 52 | ------------------------------------------ 53 | 54 | Create a .json file and define a test case like an example: 55 | 56 | ``FILE: HTTP_GET.json`` 57 | 58 | .. code-block:: json 59 | 60 | { 61 | "name": "TEST: List all users", 62 | "verb": "GET", 63 | "endpoint": "users", 64 | "host": "https://github.com", 65 | "headers": { 66 | "Accept-Language": "en-US" 67 | }, 68 | "query_string": { 69 | "limit": 5 70 | } 71 | } 72 | 73 | Execute a test case: 74 | 75 | .. code-block:: bash 76 | 77 | $ pyhttptest execute FILE_PATH/HTTP_GET.json 78 | 79 | Result: 80 | 81 | .. image:: https://www.dropbox.com/s/0h56p3c4jm4sriy/pyhttptest-cli.png?raw=1 82 | :alt: pyhttptest in the command line 83 | :width: 100% 84 | :align: center 85 | 86 | Мultiple test cases 87 | ------------------------------------------ 88 | 89 | Create a .json file and define a test cases like an example: 90 | 91 | ``FILE: requests.json`` 92 | 93 | .. code-block:: json 94 | 95 | [ 96 | { 97 | "name":"TEST: List all users", 98 | "verb":"GET", 99 | "endpoint":"api/v1/users", 100 | "host":"http://localhost:8085/", 101 | "headers":{ 102 | "Accept-Language":"en-US" 103 | }, 104 | "query_string":{ 105 | "limit":1 106 | } 107 | }, 108 | { 109 | "name":"TEST: Add a new user", 110 | "verb":"POST", 111 | "endpoint":"api/v1/users", 112 | "host":"http://localhost:8085/", 113 | "payload":{ 114 | "username":"pyhttptest", 115 | "email":"admin@pyhttptest.com" 116 | } 117 | }, 118 | { 119 | "name":"TEST: Modify an existing user", 120 | "verb":"PUT", 121 | "endpoint":"api/v1/users/XeEsscGqweEttXsgY", 122 | "host":"http://localhost:8085/", 123 | "payload":{ 124 | "username":"pyhttptest" 125 | } 126 | }, 127 | { 128 | "name":"TEST: Delete an existing user", 129 | "verb":"DELETE", 130 | "endpoint":"api/v1/users/XeEsscGqweEttXsgY", 131 | "host":"http://localhost:8085/" 132 | } 133 | ] 134 | 135 | Execute a test case: 136 | 137 | .. code-block:: bash 138 | 139 | $ pyhttptest execute FILE_PATH/requests.json 140 | 141 | Result: 142 | 143 | .. image:: https://www.dropbox.com/s/cd0g07dop4j1riq/pyhttptest-cli-table-of-results.png?raw=1 144 | :alt: pyhttptest in the command line 145 | :width: 100% 146 | :align: center 147 | 148 | Dependencies 149 | ****************************************** 150 | 151 | Under the hood, pyhttptest uses these amazing libraries: 152 | 153 | * `ijson `_ 154 | — Iterative JSON parser with a standard Python iterator interface 155 | * `jsonschema `_ 156 | — An implementation of JSON Schema validation for Python 157 | * `Requests `_ 158 | — Python HTTP library for humans 159 | * `tabulate `_ 160 | — Pretty-print tabular data 161 | * `click `_ 162 | — Composable command line interface toolkit 163 | 164 | 165 | Contributing 166 | ****************************************** 167 | 168 | See `CONTRIBUTING `_. 169 | 170 | 171 | Changelog 172 | ****************************************** 173 | 174 | See `CHANGELOG `_. 175 | 176 | 177 | Licence 178 | ****************************************** 179 | 180 | BSD-3-Clause: `LICENSE `_. 181 | 182 | 183 | Authors 184 | ****************************************** 185 | 186 | `Iliyan Slavov`_ 187 | 188 | .. _Iliyan Slavov: https://www.linkedin.com/in/iliyan-slavov-03478a157/ 189 | -------------------------------------------------------------------------------- /data/HTTP_GET.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TEST: List all users", 3 | "verb": "GET", 4 | "endpoint": "users", 5 | "host": "http://localhost:8080", 6 | "headers": { 7 | "Accept-Language": "en-US" 8 | }, 9 | "query_string": { 10 | "limit": 5 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /data/HTTP_GET.yaml: -------------------------------------------------------------------------------- 1 | name: 'TEST: List all users' 2 | verb: GET 3 | endpoint: users 4 | host: http://localhost:8080 5 | headers: 6 | Accept-Language: en-US 7 | query_string: 8 | limit: 5 9 | -------------------------------------------------------------------------------- /data/HTTP_GET_SAVE_RESPONSE.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TEST: List all users", 3 | "verb": "GET", 4 | "endpoint": "users", 5 | "host": "http://localhost:8080", 6 | "headers": { 7 | "Accept-Language": "en-US" 8 | }, 9 | "query_string": { 10 | "limit": 5 11 | }, 12 | "response": { 13 | "index": "users", 14 | "save": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/MULTIPLE_HTTP_REQUESTS.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "TEST: List all users", 4 | "verb": "GET", 5 | "endpoint": "users", 6 | "host": "https://github.com", 7 | "headers": { 8 | "Accept-Language": "en-US" 9 | }, 10 | "query_string": { 11 | "limit": 5 12 | } 13 | }, 14 | { 15 | "name": "TEST: Create an HTML bin", 16 | "verb": "POST", 17 | "endpoint": "post", 18 | "host": "https://httpbin.org", 19 | "payload": { 20 | "hello": "world!" 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /pyhttptest/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.7' 2 | -------------------------------------------------------------------------------- /pyhttptest/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from pyhttptest.wrappers import execute_test_scenarios 4 | 5 | 6 | @click.group() 7 | def main(): 8 | pass 9 | 10 | 11 | @main.command(short_help='Executes a test scenario from a given .json file.') 12 | @click.argument('file') 13 | def execute(file): 14 | """Executes a test scenario from a given .json file format. 15 | 16 | The command executes an HTTP Request with a composed content from 17 | a given .json file, described in a JSON format and prints out 18 | the result of an HTTP Response in a tabular string to the stdout. 19 | """ 20 | output = execute_test_scenarios(file) 21 | click.echo(output) 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /pyhttptest/constants.py: -------------------------------------------------------------------------------- 1 | JSON_FILE_EXTENSION = '.json' 2 | 3 | REQUIRED_SCHEMA_KEYS = ( 4 | 'name', 5 | 'verb', 6 | 'endpoint', 7 | 'host', 8 | ) 9 | 10 | OPTIONAL_SCHEMA_KEYS = ( 11 | 'headers', 12 | 'query_string', 13 | 'response', 14 | 'payload', 15 | ) 16 | 17 | HTTP_METHOD_NAMES = ('get', 'post', 'put', 'delete',) 18 | 19 | SLICE_TO_INDEX = 100 20 | 21 | PRINTER_HEADERS = ( 22 | 'Test name', 23 | 'HTTP Response Status Code', 24 | 'Cover', 25 | ) 26 | 27 | PRINTER_HEADERS_DATA_KEYS = ( 28 | 'name', 29 | 'status_code', 30 | 'cover' 31 | ) 32 | -------------------------------------------------------------------------------- /pyhttptest/core.py: -------------------------------------------------------------------------------- 1 | import ijson.backends.yajl2_c as ijson 2 | 3 | from requests import Response 4 | 5 | from pyhttptest import utils 6 | from pyhttptest import constants 7 | from pyhttptest.http import method_dispatcher 8 | from pyhttptest.printer import prepare_data_for_print 9 | from pyhttptest.decorators import ( 10 | check_file_extension, 11 | validate_data_against_json_schema 12 | ) 13 | 14 | 15 | @check_file_extension 16 | def load_content_from_json_file(file_path): 17 | """Loads content from the file. 18 | 19 | By passing ``file_path`` parameter, the file is opened 20 | and the content from the file is extracted. 21 | 22 | :param str file_path: Optional file path. 23 | 24 | :returns: A content in a list 25 | :rtype: `list` 26 | """ 27 | with open(file_path, 'rb') as file: 28 | items_generator = ijson.items(file, '') 29 | list_of_items = [item for item in items_generator] 30 | 31 | return list_of_items 32 | 33 | 34 | @validate_data_against_json_schema 35 | def extract_json_data(data): 36 | """Wrapper function that extracts JSON data. 37 | 38 | By passing ``data`` parameter, the JSON content 39 | from parameter is extracted under the required 40 | and optional keys. 41 | 42 | :param dict data: An arbitrary data. 43 | 44 | :returns: Splitted data into required and optional. 45 | :rtype: `tuple` 46 | """ 47 | required_args = utils.extract_properties_values_from_json( 48 | data, 49 | constants.REQUIRED_SCHEMA_KEYS 50 | ) 51 | optional_kwargs = utils.extract_properties_values_of_type_dict_from_json( 52 | data, 53 | constants.OPTIONAL_SCHEMA_KEYS 54 | ) 55 | 56 | return (required_args, optional_kwargs) 57 | 58 | 59 | def prepare_request_args(*args): 60 | """Prepares the required arguments that will be used 61 | to send an HTTP Request. 62 | 63 | By passing ``args`` parameter, the arguments within 64 | are transformed in a way to cover sending an HTTP Request 65 | gracefully. 66 | 67 | :param args: Expect arguments in format (name, verb, endpoint, host). 68 | 69 | :returns: Transformed arguments for HTTP Request. 70 | :rtype: `tuple` 71 | """ 72 | if not args or len(args) != 4: 73 | return None 74 | 75 | _, http_method, endpoint, host = args 76 | url = utils.prepare_url(host, endpoint) 77 | 78 | return (http_method.lower(), url) 79 | 80 | 81 | def send_http_request(*args, **kwargs): 82 | """Wrapper function responsible for sending an HTTP Request 83 | and receiving an HTTP Response. 84 | 85 | :param args: An HTTP Request arguments. 86 | :param kwargs: Optional arguments like HTTP headers, cookies and etc. 87 | 88 | :returns: :class:`Response` object or `None`. 89 | :rtype: :class:`requests.Response` or `None` 90 | """ 91 | return method_dispatcher(*args, **kwargs) 92 | 93 | 94 | def extract_http_response_content(response): 95 | """Extracts given :class:`requests.Response` instance 96 | attributes. 97 | 98 | Аttributes that are extracted from the instance are following: 99 | 100 | - HTTP Status Code 101 | 102 | :param requests.Response response: Instance. 103 | 104 | :returns: Content of HTTP Response. 105 | :rtype: `dict` 106 | """ 107 | if not isinstance(response, Response): 108 | return None 109 | 110 | content = { 111 | 'status_code': str(response.status_code), 112 | } 113 | 114 | if response.content: 115 | try: 116 | content['data'] = response.json() 117 | except Exception: 118 | pass 119 | 120 | return content 121 | 122 | 123 | def transform_data_in_tabular_str(data): 124 | """Transforms the data into tabular string. 125 | 126 | param list|dict data: An extract of HTTP Response data. 127 | 128 | :returns: A tabular string. 129 | :rtype: `str` 130 | """ 131 | if not any(isinstance(data, _type) for _type in [list, dict]): 132 | return 'The data is not correctly structured.' 133 | 134 | if isinstance(data, list) and not isinstance(data[0], dict): 135 | return 'The list of content is not correctly formatted.' 136 | 137 | if isinstance(data, dict): 138 | # Put the `dict` data into a list 139 | data = [data] 140 | 141 | return prepare_data_for_print(data) 142 | -------------------------------------------------------------------------------- /pyhttptest/decorators.py: -------------------------------------------------------------------------------- 1 | from sys import modules 2 | from functools import wraps 3 | 4 | from jsonschema import validate 5 | 6 | from pyhttptest.constants import ( 7 | HTTP_METHOD_NAMES, 8 | JSON_FILE_EXTENSION, 9 | ) 10 | from pyhttptest.exceptions import ( 11 | FileExtensionError, 12 | HTTPMethodNotSupportedError 13 | ) 14 | from pyhttptest.http_schemas import ( # noqa 15 | get_schema, 16 | post_schema, 17 | put_schema, 18 | delete_schema 19 | ) 20 | 21 | 22 | def check_file_extension(func): 23 | """A decorator responsible for checking whether 24 | the file extension is supported. 25 | 26 | An inner :func:`_decorator` slices the last five 27 | characters of the passed ``file_path`` parameter and 28 | checking whether they are equal to JSON file extension(.json). 29 | If there is equality, decorated function business logic is 30 | performed otherwise, the exception for not supported file extension 31 | is raised. 32 | 33 | Usage: 34 | 35 | .. code-block:: python 36 | 37 | @check_file_extension 38 | def load_content_from_json_file(file_path): 39 | ... 40 | 41 | :raises FileExtensionError: If the file extension is not '.json'. 42 | """ 43 | @wraps(func) 44 | def _decorator(file_path): 45 | file_extension = file_path[-5:] 46 | if file_extension != JSON_FILE_EXTENSION: 47 | raise FileExtensionError(file_extension) 48 | return func(file_path) 49 | return _decorator 50 | 51 | 52 | def validate_extract_json_properties_func_args(func): 53 | """A validation decorator, ensuring that arguments 54 | passed to the decorated function are with proper types. 55 | 56 | An inner :func:`_decorator` does checking of arguments 57 | types. If the types of the arguments are different than allowing 58 | ones, the exception is raised, otherwise decorated function 59 | is processed. 60 | 61 | Usage: 62 | 63 | .. code-block:: python 64 | 65 | @validate_extract_json_properties_func_args 66 | def extract_properties_values_from_json(data, keys): 67 | ... 68 | 69 | :raises TypeError: If the data is not a `dict`. 70 | :raises TypeError: If the keys is not a type of (`tuple`, `list`, `set`). 71 | """ 72 | @wraps(func) 73 | def _decorator(data, keys): 74 | if not isinstance(data, dict): 75 | raise TypeError( 76 | ( 77 | "Passed 'data' param argument, must be of " 78 | "data type 'dict'. Not a type of {type}.".format( 79 | type=type(data) 80 | ) 81 | ) 82 | ) 83 | 84 | if not isinstance(keys, (tuple, list, set)): 85 | raise TypeError( 86 | ( 87 | "Passed 'keys' param argument, must be one of: " 88 | "(tuple, list, set) data types. Not a type of {type}.".format( 89 | type=type(keys) 90 | ) 91 | ) 92 | ) 93 | return func(data, keys) 94 | return _decorator 95 | 96 | 97 | def validate_data_against_json_schema(func): 98 | """A validation decorator, ensuring that data is 99 | covering JSON Schema requirements. 100 | 101 | An inner :func:`_decorator` does checking of data 102 | type, HTTP Method support along with appropriate JSON Schema, 103 | that can validate passed data. If one of the checks doesn't match, 104 | the exception is raised, otherwise, data validation is run against 105 | JSON Schema and decorated function is processed. 106 | 107 | Usage: 108 | 109 | .. code-block:: python 110 | 111 | @validate_data_against_json_schema 112 | def extract_json_data(data): 113 | ... 114 | 115 | :raises TypeError: If the data is not a `dict`. 116 | :raises HTTPMethodNotSupportedError: If an HTTP Method is not supported. 117 | :raises TypeError: If lack of appropriate JSON Schema to validate data. 118 | """ 119 | @wraps(func) 120 | def _decorator(data): 121 | if not isinstance(data, dict): 122 | raise TypeError( 123 | ( 124 | "Passed 'data' param argument, must be of " 125 | "data type 'dict'. Not a type of {type}.".format( 126 | type=type(data) 127 | ) 128 | ) 129 | ) 130 | 131 | if 'verb' not in data or data['verb'].lower() not in HTTP_METHOD_NAMES: 132 | raise HTTPMethodNotSupportedError(data.get('verb', 'None')) 133 | 134 | http_schema_name = '_'.join([data['verb'].lower(), 'schema']) 135 | # The key is used to extract module loaded in sys.modules 136 | http_schema_module_key = '.'.join( 137 | ['pyhttptest.http_schemas', http_schema_name] 138 | ) 139 | # Extract the module instance 140 | http_schema_module = modules[http_schema_module_key] 141 | 142 | if not hasattr(http_schema_module, http_schema_name): 143 | raise ValueError( 144 | ( 145 | 'There is no appropriate JSON Schema to ' 146 | 'validate data against it.' 147 | ) 148 | ) 149 | 150 | http_schema_instance = getattr(http_schema_module, http_schema_name) 151 | validate(instance=data, schema=http_schema_instance) 152 | 153 | return func(data) 154 | return _decorator 155 | -------------------------------------------------------------------------------- /pyhttptest/exceptions.py: -------------------------------------------------------------------------------- 1 | class FileExtensionError(Exception): 2 | """The exception raised for trying to load unsupported file extensions""" 3 | 4 | def __init__(self, file_extension): 5 | """Instantiate an object with a file extension isn't supported for 6 | loading and message that gives information to the user. 7 | 8 | :param str file_extension: A File extension e.g. '.yaml'. 9 | """ 10 | self.file_extension = file_extension 11 | self.message = ( 12 | "A file extension '{file_extension}' is not supported. " 13 | "Only a file with '.json' extension is supported".format( 14 | file_extension=self.file_extension 15 | ) 16 | ) 17 | super().__init__(self.message) 18 | 19 | 20 | class HTTPMethodNotSupportedError(Exception): 21 | """The exception raised when HTTP method isn't supported 22 | on the application level or typo found. 23 | """ 24 | 25 | def __init__(self, http_method): 26 | """Instantiate an object with a HTTP method and message that gives information to the user. 27 | 28 | :param str http_method: An HTTP method e.g. 'HEAD'. 29 | """ 30 | self.http_method = http_method.upper() 31 | self.message = ( 32 | "An HTTP method ('{method}') is not supported by the application.".format( 33 | method=self.http_method 34 | ) 35 | ) 36 | super().__init__(self.message) 37 | -------------------------------------------------------------------------------- /pyhttptest/http.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import requests 4 | 5 | from pyhttptest import constants 6 | from pyhttptest.exceptions import HTTPMethodNotSupportedError 7 | 8 | 9 | def method_dispatcher(*args, **kwargs): 10 | """Try to dispatch to the right HTTP method handler. 11 | If an HTTP method isn't on the approved list, defer 12 | to the error handler. Otherwise, the HTTP Method is 13 | processed by the appropriate handler. 14 | 15 | :param args: Expect arguments in format (http_method, url). 16 | :param kwargs: Optional arguments like HTTP headers, cookies and etc. 17 | 18 | :returns: Result from the handler. 19 | :rtype: func 20 | """ 21 | http_method, url = args 22 | 23 | if http_method not in constants.HTTP_METHOD_NAMES: 24 | raise HTTPMethodNotSupportedError(http_method) 25 | 26 | handler = getattr(sys.modules[__name__], http_method) 27 | 28 | return handler(*args, **kwargs) 29 | 30 | 31 | def get(*args, **kwargs): 32 | """Sends an HTTP GET Request. 33 | 34 | :param args: URL argument on the first index(args[1]). 35 | :param kwargs: Optional arguments that ``requests.get`` takes. 36 | 37 | :returns: :class:`Response` object or `None` if an error occurred. 38 | :rtype: :class:`requests.Response` or `None` 39 | """ 40 | url = args[1] 41 | query_string = kwargs.get('query_string', None) 42 | headers = kwargs.get('headers', None) 43 | 44 | return requests.get(url, params=query_string, headers=headers) 45 | 46 | 47 | def post(*args, **kwargs): 48 | """Sends an HTTP POST Request. 49 | 50 | :param args: URL argument on the first index(args[1]). 51 | :param kwargs: Optional arguments that ``requests.post`` takes. 52 | 53 | :returns: :class:`Response` object or `None` if an error occurred. 54 | :rtype: :class:`requests.Response` or `None` 55 | """ 56 | url = args[1] 57 | query_string = kwargs.get('query_string', None) 58 | payload = kwargs.get('payload', None) 59 | headers = kwargs.get('headers', None) 60 | 61 | return requests.post( 62 | url, 63 | params=query_string, 64 | json=payload, 65 | headers=headers 66 | ) 67 | 68 | 69 | def put(*args, **kwargs): 70 | """Sends an HTTP PUT Request. 71 | 72 | :param args: URL argument on the first index(args[1]). 73 | :param kwargs: Optional arguments that ``requests.put`` takes. 74 | 75 | :returns: :class:`Response` object or `None` if an error occurred. 76 | :rtype: :class:`requests.Response` or `None` 77 | """ 78 | url = args[1] 79 | query_string = kwargs.get('query_string', None) 80 | payload = kwargs.get('payload', None) 81 | headers = kwargs.get('headers', None) 82 | 83 | return requests.put( 84 | url, 85 | params=query_string, 86 | data=payload, 87 | headers=headers 88 | ) 89 | 90 | 91 | def delete(*args, **kwargs): 92 | """Sends an HTTP DELETE Request. 93 | 94 | :param args: URL argument on the first index(args[1]). 95 | :param kwargs: Optional arguments that ``requests.delete`` takes. 96 | 97 | :returns: :class:`Response` object or `None` if an error occurred. 98 | :rtype: :class:`requests.Response` or `None` 99 | """ 100 | url = args[1] 101 | query_string = kwargs.get('query_string', None) 102 | headers = kwargs.get('headers', None) 103 | 104 | return requests.delete(url, params=query_string, headers=headers) 105 | -------------------------------------------------------------------------------- /pyhttptest/http_schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slaily/pyhttptest/e9218acd8bbbd2ec8560fdc402ea9e3c04c9f010/pyhttptest/http_schemas/__init__.py -------------------------------------------------------------------------------- /pyhttptest/http_schemas/base_schema.py: -------------------------------------------------------------------------------- 1 | # A ``dict`` schema that defines a structure specification for sending 2 | # an HTTP Request, along with required and non-required parameters 3 | # like Request Header and Response Headers that might be sent by request. 4 | # This is the base schema that will be extended from the HTTP Methods 5 | # schemas:: 6 | # 7 | # ... GET - :file:`pyhttptest/http_schemas/get_schema.py` 8 | # 9 | # The purpose is to validate input JSON file content 10 | # under the schema. 11 | base_schema = { 12 | 'type': 'object', 13 | 'properties': { 14 | 'name': {'type': 'string'}, 15 | 'verb': { 16 | 'type': 'string', 17 | 'enum': ['GET', 'POST', 'PUT', 'DELETE'] 18 | }, 19 | 'endpoint': {'type': 'string'}, 20 | 'host': {'type': 'string'}, 21 | 'headers': { 22 | 'type': 'object', 23 | 'properties': { 24 | 'Content-Type': {'type': 'string'}, 25 | 'Content-Encoding': {'type': 'string'}, 26 | 'Content-Language': {'type': 'string'}, 27 | 'Content-Location': {'type': 'string'}, 28 | 'Content-Length': {'type': 'number'}, 29 | 'Content-Range': {'type': 'string'}, 30 | 'Cache-Control': {'type': 'string'}, 31 | 'Expect': {'type': 'string'}, 32 | 'Host': {'type': 'string'}, 33 | 'Max-Forwards': {'type': 'number'}, 34 | 'Pragma': {'type': 'string'}, 35 | 'If-Match': {'type': 'string'}, 36 | 'If-None-Match': {'type': 'string'}, 37 | 'If-Modified-Since': { 38 | 'type': 'string', 39 | 'format': 'date' # , :: GMT 40 | }, 41 | 'If-Unmodified-Since': { 42 | 'type': 'string', 43 | 'format': 'date' # , :: GMT 44 | }, 45 | 'Accept': {'type': 'string'}, 46 | 'Accept-Charset': {'type': 'string'}, 47 | 'Accept-Encoding': {'type': 'string'}, 48 | 'Accept-Language': {'type': 'string'}, 49 | 'Authorization': {'type': 'string'}, 50 | 'Proxy-Authorization': {'type': 'string'}, 51 | 'From': {'type': 'string'}, 52 | 'Referer': {'type': 'string'}, 53 | 'User-Agent': {'type': 'string'}, 54 | 'Trailer': {'type': 'string'}, 55 | 'Transfer-Encoding': {'type': 'string'} 56 | } 57 | }, 58 | 'response': { 59 | 'type': 'object', 60 | 'properties': { 61 | 'index': {'type': 'string'}, 62 | 'save': {'type': 'boolean'} 63 | } 64 | } 65 | }, 66 | 'required': [ 67 | 'name', 68 | 'verb', 69 | 'endpoint', 70 | 'host' 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /pyhttptest/http_schemas/delete_schema.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from pyhttptest.http_schemas.base_schema import base_schema 4 | 5 | 6 | # A ``dict`` schema that copies :file:`pyhttptest/http_schemas/base_schema.py` 7 | # schema and extends it with properties related for a structure specification 8 | # sending an HTTP DELETE Request. 9 | delete_schema = deepcopy(base_schema) 10 | delete_schema['properties'].update( 11 | { 12 | 'query_string': { 13 | 'type': 'object' 14 | } 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /pyhttptest/http_schemas/get_schema.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from pyhttptest.http_schemas.base_schema import base_schema 4 | 5 | 6 | # A ``dict`` schema that copies :file:`pyhttptest/http_schemas/base_schema.py` 7 | # schema and extends it with properties related for a structure specification 8 | # sending an HTTP GET Request. 9 | get_schema = deepcopy(base_schema) 10 | get_schema['properties'].update( 11 | { 12 | 'query_string': { 13 | 'type': 'object' 14 | } 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /pyhttptest/http_schemas/post_schema.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from pyhttptest.http_schemas.base_schema import base_schema 4 | 5 | 6 | # A ``dict`` schema that copies :file:`pyhttptest/http_schemas/base_schema.py` 7 | # schema and extends it with properties related for a structure specification 8 | # sending an HTTP POST Request. 9 | post_schema = deepcopy(base_schema) 10 | post_schema['properties'].update( 11 | { 12 | 'payload': { 13 | 'type': 'object' 14 | }, 15 | 'query_string': { 16 | 'type': 'object' 17 | } 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /pyhttptest/http_schemas/put_schema.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from pyhttptest.http_schemas.base_schema import base_schema 4 | 5 | 6 | # A ``dict`` schema that copies :file:`pyhttptest/http_schemas/base_schema.py` 7 | # schema and extends it with properties related for a structure specification 8 | # sending an HTTP PUT Request. 9 | put_schema = deepcopy(base_schema) 10 | put_schema['properties'].update( 11 | { 12 | 'payload': { 13 | 'type': 'object' 14 | }, 15 | 'query_string': { 16 | 'type': 'object' 17 | } 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /pyhttptest/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | formatter = logging.Formatter('%(levelname)s - %(message)s') 5 | stream_handler = logging.StreamHandler() 6 | stream_handler.setFormatter(formatter) 7 | logger = logging.getLogger('pyhttptest') 8 | logger.addHandler(stream_handler) 9 | -------------------------------------------------------------------------------- /pyhttptest/printer.py: -------------------------------------------------------------------------------- 1 | from tabulate import tabulate 2 | 3 | from pyhttptest.constants import ( 4 | SLICE_TO_INDEX, 5 | PRINTER_HEADERS, 6 | PRINTER_HEADERS_DATA_KEYS 7 | ) 8 | from pyhttptest.utils import extract_properties_values_from_json 9 | 10 | 11 | def _slice_str_args(*args, slice_to=SLICE_TO_INDEX): 12 | """Given `str` arguments are sliced to the specified length. 13 | 14 | :param args: Arguments in `str` type. 15 | :param int slice_to(optional): Index to slice. 16 | 17 | :returns: Sliced arguments. 18 | :rtype: `tuple` 19 | """ 20 | return tuple( 21 | str_value[:slice_to] for str_value in args 22 | ) 23 | 24 | 25 | def _format_data_as_tabular(list_data, headers=PRINTER_HEADERS): 26 | """Formats the data and put it into a table with headers 27 | on the top оf it. 28 | 29 | :param list|tuple|set list_data: Iterable of iterables. 30 | :param headers: Iterable of `str`. 31 | 32 | Example table output: 33 | 34 | ╒══════════════════════════╤═════════════════════════════╕ 35 | │ Test name │ HTTP Response Status Code │ 36 | ╞══════════════════════════╪═════════════════════════════╡ 37 | │ TEST: List all users │ 200 │ 38 | ├──────────────────────────┼─────────────────────────────┤ 39 | │ TEST: Create an HTML bin │ 200 │ 40 | ╘══════════════════════════╧═════════════════════════════╛ 41 | 42 | 43 | :returns: Data in tabular format. 44 | :rtype: `str` 45 | """ 46 | return tabulate( 47 | list_data, 48 | headers, 49 | tablefmt='fancy_grid', 50 | ) 51 | 52 | 53 | def prepare_data_for_print(list_of_dicts): 54 | """Wrapper function responsible to take the data, push it 55 | through several processes to prepare it for print. 56 | 57 | :param list list_of_dicts: List of `dict` data. 58 | 59 | :returns: Data for print. 60 | :rtype: `str` 61 | """ 62 | list_of_strs = [] 63 | 64 | for _dict in list_of_dicts: 65 | args = extract_properties_values_from_json( 66 | _dict, 67 | PRINTER_HEADERS_DATA_KEYS 68 | ) 69 | sliced_str_args = _slice_str_args(*args) 70 | list_of_strs.append(sliced_str_args) 71 | 72 | return _format_data_as_tabular(list_of_strs) 73 | -------------------------------------------------------------------------------- /pyhttptest/utils.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from http.client import InvalidURL 3 | 4 | from pyhttptest.decorators import validate_extract_json_properties_func_args 5 | 6 | 7 | @validate_extract_json_properties_func_args 8 | def extract_properties_values_from_json(data, keys): 9 | """Extracts properties values from the JSON data. 10 | 11 | .. note:: 12 | 13 | Each of key/value pairs into JSON conventionally referred 14 | to as a "property". More information about this convention follow 15 | `JSON Schema documentation `_. 16 | 17 | Passing ``data`` argument for an example: 18 | 19 | .. code-block:: python 20 | 21 | data = { 22 | 'verb': 'GET', 23 | 'endpoint': 'users', 24 | 'host': 'http://localhost:8080' 25 | ... 26 | } 27 | 28 | along with ``keys`` argument for an example: 29 | 30 | .. code-block:: python 31 | 32 | keys = ('verb', 'endpoint', 'host') 33 | 34 | Iterating over ``keys`` parameter values and 35 | extracts the property value of ``data`` parameter by key with the 36 | exact same value. 37 | 38 | Result: 39 | 40 | .. code-block:: python 41 | 42 | ('GET', 'users, 'http://localhost:8080') 43 | 44 | :param dict data: An arbitrary data. 45 | :param tuple|list|set keys: Iterable with values of type `str`. 46 | 47 | :returns: Packaged values. 48 | :rtype: `tuple` 49 | """ 50 | return tuple(data[key] for key in keys if key in data) 51 | 52 | 53 | @validate_extract_json_properties_func_args 54 | def extract_properties_values_of_type_dict_from_json(data, keys): 55 | """Extracts properties values of type `dict` from the JSON data. 56 | 57 | .. note:: 58 | 59 | Each of key/value pairs into JSON conventionally referred 60 | to as a "property". More information about this convention follow 61 | `JSON Schema documentation `_. 62 | 63 | Passing ``data`` argument for an example: 64 | 65 | .. code-block:: python 66 | 67 | data = { 68 | 'verb': 'GET', 69 | 'endpoint': 'users', 70 | 'host': 'http://localhost:8080' 71 | 'headers': { 72 | 'Accept-Language': 'en-US' 73 | } 74 | ... 75 | } 76 | 77 | along with ``keys`` argument for an example: 78 | 79 | .. code-block:: python 80 | 81 | keys = ('headers',) 82 | 83 | Iterating over ``keys`` parameter values and 84 | extracts the property value of type `dict` from ``data`` 85 | parameter by key with the exact same value. 86 | 87 | Result: 88 | 89 | .. code-block:: python 90 | 91 | { 92 | 'headers': { 93 | 'Accept-Language': 'en-US' 94 | } 95 | } 96 | 97 | :param dict data: An arbitrary data. 98 | :param tuple|list|set keys: Iterable with values of type `str`. 99 | 100 | :returns: Packaged key/value pairs. 101 | :rtype: `dict` 102 | """ 103 | return { 104 | key: data[key] for key in keys 105 | if key in data and isinstance(data[key], dict) 106 | } 107 | 108 | 109 | def prepare_url(host, endpoint): 110 | """Glues the ``host`` and ``endpoint`` parameters to 111 | form an URL. 112 | 113 | :param str host: Value e.g. **http://localhost.com**. 114 | :param str endpoint: An API resourse e.g. **/users**. 115 | 116 | :raises InvalidURL: If the URL parts are in invalid format. 117 | :raises InvalidURL: If the URL schema is not supported. 118 | 119 | :returns: URL. 120 | :rtype: `str` 121 | """ 122 | if not host or not endpoint: 123 | return None 124 | 125 | if not host[-1] == '/' and not endpoint[0] == '/': 126 | url = '/'.join([host, endpoint]) 127 | 128 | if host[-1] == '/' and not endpoint[0] == '/': 129 | url = ''.join([host, endpoint]) 130 | 131 | if not host[-1] == '/' and endpoint[0] == '/': 132 | url = ''.join([host, endpoint]) 133 | 134 | if host[-1] == '/' and endpoint[0] == '/': 135 | url = ''.join([host, endpoint[1:]]) 136 | 137 | parsed_url = urlparse(url) 138 | 139 | if not parsed_url.scheme or not parsed_url.netloc: 140 | raise InvalidURL('Invalid URL {url}'.format(url=url)) 141 | 142 | if parsed_url.scheme not in ['http', 'https']: 143 | raise InvalidURL( 144 | 'Invalid URL scheme {scheme}. ' 145 | 'Supported schemes are http or https.'.format( 146 | scheme=parsed_url.scheme 147 | ) 148 | ) 149 | 150 | return url 151 | -------------------------------------------------------------------------------- /pyhttptest/wrappers.py: -------------------------------------------------------------------------------- 1 | from pyhttptest import core 2 | 3 | 4 | def execute_single_test_scenario(json_data): 5 | """Wrapper function that comprises functionalities 6 | to execute a single test scenario. 7 | 8 | :param dict json_data: An arbitrary data. 9 | 10 | :returns: Result of the test scenario. 11 | :rtype: `dict' 12 | """ 13 | required_args, optional_kwargs = core.extract_json_data(json_data) 14 | http_method, url = core.prepare_request_args(*required_args) 15 | response = core.send_http_request(http_method, url, **optional_kwargs) 16 | response_content = core.extract_http_response_content(response) 17 | # Add a test case name as JSON property 18 | response_content['name'] = json_data['name'] 19 | response_content['cover'] = ' % - COMING SOON' 20 | 21 | return response_content 22 | 23 | 24 | def execute_multiple_test_scenarios(list_of_dicts): 25 | """Wrapper function that comprises functionality 26 | to execute multiple tests scenarios. 27 | 28 | :param list list_of_dicts: List with values of type `dict`. 29 | 30 | :returns: Results of the tests scenarios. 31 | :rtype: `list' 32 | """ 33 | results = [ 34 | execute_single_test_scenario(json_data) for json_data in list_of_dicts 35 | ] 36 | 37 | return results 38 | 39 | 40 | def execute_test_scenarios(file): 41 | """Wrapper function that executes single or multiple 42 | test scenarios according to the content from file. 43 | 44 | :param str file_path: Optional file path. 45 | 46 | :returns: Text output with the result. 47 | :rtype: `str' 48 | """ 49 | try: 50 | list_of_content = core.load_content_from_json_file(file) 51 | # Actually, the zeroth index contains the content 52 | content = list_of_content[0] 53 | 54 | # Depends on a type of the content loaded from the file, 55 | # the business logic for single or multiple test scenarios 56 | # is executed. 57 | if isinstance(content, dict): 58 | result = execute_single_test_scenario(content) 59 | elif isinstance(content, list): 60 | result = execute_multiple_test_scenarios(content) 61 | except Exception as exc: 62 | return str(exc) 63 | 64 | return core.transform_data_in_tabular_str(result) 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from re import search 2 | from setuptools import ( 3 | setup, 4 | find_namespace_packages 5 | ) 6 | 7 | 8 | with open('README.rst', 'r', encoding='utf8') as file: 9 | readme = file.read() 10 | 11 | with open('pyhttptest/__init__.py', 'rt', encoding='utf8') as file: 12 | version = search( 13 | r"__version__ = ['\"]([^'\"]+)['\"]", 14 | file.read() 15 | ).group(1) 16 | 17 | setup( 18 | name='pyhttptest', 19 | version=version, 20 | author='Iliyan Slavov', 21 | author_email='slavov.iliyan96@gmail.com', 22 | description='A command-line tool for HTTP tests over RESTful APIs', 23 | long_description=readme, 24 | keywords='HTTP test RESTFul API JSON', 25 | license='BSD 3-Clause License', 26 | url='https://github.com/slaily/pyhttptest', 27 | project_urls={ 28 | 'Issues': 'https://github.com/slaily/pyhttptest/issues', 29 | }, 30 | packages=find_namespace_packages(include=['pyhttptest', 'pyhttptest.*']), 31 | install_requires=[ 32 | 'click==7.0', 33 | 'ijson==3.0.3', 34 | 'jsonschema==3.1.1', 35 | 'requests==2.22.0', 36 | 'tabulate==0.8.5' 37 | ], 38 | tests_require='pytest', 39 | python_requires='>=3', 40 | entry_points={ 41 | 'console_scripts': [ 42 | 'pyhttptest = pyhttptest.cli:main', 43 | ] 44 | }, 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Environment :: Console', 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: BSD License', 50 | 'Natural Language :: English', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python :: 3', 53 | 'Topic :: Software Development :: Testing' 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slaily/pyhttptest/e9218acd8bbbd2ec8560fdc402ea9e3c04c9f010/tests/__init__.py -------------------------------------------------------------------------------- /tests/http_schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slaily/pyhttptest/e9218acd8bbbd2ec8560fdc402ea9e3c04c9f010/tests/http_schemas/__init__.py -------------------------------------------------------------------------------- /tests/http_schemas/test_base_schema.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from jsonschema import validate 4 | from jsonschema.exceptions import ValidationError 5 | 6 | from pyhttptest.http_schemas.base_schema import base_schema 7 | 8 | 9 | def test_schema_with_valid_data(): 10 | data = { 11 | 'name': 'Test', 12 | 'verb': 'GET', 13 | 'endpoint': 'users', 14 | 'host': 'http://test.com', 15 | } 16 | 17 | result = validate(instance=data, schema=base_schema) 18 | 19 | assert result is None 20 | 21 | 22 | def test_schema_with_invalid_data(): 23 | with pytest.raises(ValidationError) as exc: 24 | # Not including a required property 'endpoint' 25 | # from the schema into the ``dict`` below 26 | data = { 27 | 'name': 'Test', 28 | 'verb': 'GET', 29 | 'host': 'http://test.com', 30 | } 31 | 32 | validate(instance=data, schema=base_schema) 33 | 34 | assert 'required property' in str(exc.value) 35 | -------------------------------------------------------------------------------- /tests/http_schemas/test_delete_schema.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | 3 | from pyhttptest.http_schemas.delete_schema import delete_schema 4 | 5 | 6 | def test_schema_with_valid_data(): 7 | data = { 8 | 'name': 'Test', 9 | 'verb': 'DELETE', 10 | 'endpoint': 'users/1', 11 | 'host': 'http://test.com', 12 | } 13 | 14 | result = validate(instance=data, schema=delete_schema) 15 | 16 | assert result is None 17 | -------------------------------------------------------------------------------- /tests/http_schemas/test_get_schema.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | 3 | from pyhttptest.http_schemas.get_schema import get_schema 4 | 5 | 6 | def test_schema_with_valid_data(): 7 | data = { 8 | 'name': 'Test', 9 | 'verb': 'GET', 10 | 'endpoint': 'users', 11 | 'host': 'http://test.com', 12 | 'query_string': { 13 | 'page': '1', 14 | 'limit': '5' 15 | } 16 | } 17 | 18 | result = validate(instance=data, schema=get_schema) 19 | 20 | assert result is None 21 | -------------------------------------------------------------------------------- /tests/http_schemas/test_post_schema.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | 3 | from pyhttptest.http_schemas.post_schema import post_schema 4 | 5 | 6 | def test_schema_with_valid_data(): 7 | data = { 8 | 'name': 'Test', 9 | 'verb': 'POST', 10 | 'endpoint': 'users', 11 | 'host': 'http://test.com', 12 | 'payload': { 13 | 'user': { 14 | 'id': 1 15 | } 16 | } 17 | } 18 | result = validate(instance=data, schema=post_schema) 19 | 20 | assert result is None 21 | -------------------------------------------------------------------------------- /tests/http_schemas/test_put_schema.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | 3 | from pyhttptest.http_schemas.put_schema import put_schema 4 | 5 | 6 | def test_schema_with_valid_data(): 7 | data = { 8 | 'name': 'Test', 9 | 'verb': 'PUT', 10 | 'endpoint': '/users/1', 11 | 'host': 'http://test.com', 12 | 'payload': { 13 | 'user': { 14 | 'name': 'HTTP_PUT', 15 | 'password': '00193b3fd0cf9ef573f0df2f6f8a0940' 16 | } 17 | } 18 | } 19 | result = validate(instance=data, schema=put_schema) 20 | 21 | assert result is None 22 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from pyhttptest import cli 4 | 5 | 6 | def test_execute(): 7 | runner = CliRunner() 8 | result = runner.invoke(cli.execute, ['data/HTTP_GET.json']) 9 | 10 | assert result.exit_code == 0 11 | 12 | 13 | def test_execute_with_not_supported_file_extension_as_arg(): 14 | runner = CliRunner() 15 | result = runner.invoke(cli.execute, ['data/HTTP_GET.yaml']) 16 | 17 | assert result.exit_code == 0 18 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from requests import Response 8 | 9 | from pyhttptest import core 10 | from pyhttptest.exceptions import FileExtensionError 11 | 12 | 13 | def test_load_content_from_json_file(): 14 | content = core.load_content_from_json_file('data/HTTP_GET.json') 15 | 16 | assert isinstance(content[0], dict) 17 | 18 | 19 | def test_load_content_from_json_file_with_not_supported_file_extension(): 20 | with pytest.raises(FileExtensionError) as exc: 21 | core.load_content_from_json_file('data/HTTP_GET.yaml') 22 | 23 | assert 'is not supported' in str(exc.value) 24 | 25 | 26 | def test_extract_json_data(): 27 | content = core.load_content_from_json_file('data/HTTP_GET.json') 28 | required_args, optional_kwargs = core.extract_json_data(content[0]) 29 | 30 | assert isinstance(required_args, tuple) and isinstance(optional_kwargs, dict) 31 | 32 | 33 | def test_extract_json_data_with_key_name_response(): 34 | content = core.load_content_from_json_file('data/HTTP_GET_SAVE_RESPONSE.json') 35 | required_args, optional_kwargs = core.extract_json_data(content[0]) 36 | 37 | assert 'response' in optional_kwargs and optional_kwargs['response'] 38 | 39 | 40 | def test_prepare_request_args(): 41 | args = ( 42 | 'TEST: List all users', 43 | 'GET', 44 | 'users', 45 | 'http://localhost:8080' 46 | ) 47 | request_args = core.prepare_request_args(*args) 48 | expected_args = ('get', 'http://localhost:8080/users') 49 | 50 | assert sorted(request_args) == sorted(expected_args) 51 | 52 | 53 | def test_prepare_request_args_with_invalid_arguments(): 54 | args = ( 55 | 'users', 56 | 'http://localhost:8080' 57 | ) 58 | request_args = core.prepare_request_args(*args) 59 | 60 | assert request_args is None 61 | 62 | 63 | @patch('pyhttptest.core.method_dispatcher', return_value=Response) 64 | def test_send_http_request(mock): 65 | mock.return_value.status_code = 200 66 | args = ('get', 'http://localhost:8080/users') 67 | response = core.send_http_request(*args) 68 | 69 | assert response.status_code == 200 70 | 71 | 72 | def test_extract_http_response_content(): 73 | response = Response() 74 | response.status_code = 200 75 | response.headers = {'Content-Type': 'application/json'} 76 | response_content = core.extract_http_response_content(response) 77 | 78 | assert all( 79 | key in response_content for key in ('status_code',) 80 | ) 81 | 82 | 83 | def test_extract_http_response_content_check_for_json_key(): 84 | response = Response() 85 | response.status_code = 200 86 | response.encoding = 'UTF-8' 87 | response._content = dumps({'content': True}).encode() 88 | response_content = core.extract_http_response_content(response) 89 | 90 | assert 'data' in response_content 91 | 92 | 93 | def test_extract_http_response_content_with_not_supported_argument_type(): 94 | response = { 95 | 'status_code': 200, 96 | 'headers': { 97 | 'Content-Type': 'application/json' 98 | }, 99 | 'body': '

Hello, pyhttptest!

' 100 | } 101 | response_content = core.extract_http_response_content(response) 102 | 103 | assert response_content is None 104 | 105 | 106 | def test_load_content_from_json_file_with_multiple_http_requests_scenarios(): 107 | content = core.load_content_from_json_file( 108 | 'data/MULTIPLE_HTTP_REQUESTS.json' 109 | ) 110 | 111 | assert isinstance(content[0], list) 112 | 113 | 114 | def test_transform_data_in_tabular_str_with_dict_data(): 115 | data = { 116 | 'name': 'Test: process data for print', 117 | 'status_code': '200', 118 | 'headers': '{Content-Type: application/json}', 119 | 'body': 'Lorem Ipsum' 120 | } 121 | tabular_str = core.transform_data_in_tabular_str(data) 122 | 123 | assert isinstance(tabular_str, str) 124 | 125 | 126 | def test_transform_data_in_tabular_str_with_list_of_dict_data(): 127 | data = [{ 128 | 'name': 'Test: process data for print', 129 | 'status_code': '200', 130 | 'headers': '{Content-Type: application/json}', 131 | 'body': 'Lorem Ipsum' 132 | }] 133 | tabular_str = core.transform_data_in_tabular_str(data) 134 | 135 | assert isinstance(tabular_str, str) 136 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from jsonschema.exceptions import ValidationError 4 | 5 | from pyhttptest import decorators 6 | from pyhttptest.exceptions import ( 7 | FileExtensionError, 8 | HTTPMethodNotSupportedError, 9 | ) 10 | 11 | 12 | def test_check_file_extension(): 13 | func = decorators.check_file_extension( 14 | lambda file_extension: file_extension 15 | ) 16 | func_result = func('.json') 17 | 18 | assert func_result == '.json' 19 | 20 | 21 | def test_check_file_extension_with_not_supported_file_extension(): 22 | with pytest.raises(FileExtensionError) as exc: 23 | func = decorators.check_file_extension( 24 | lambda file_extension: file_extension 25 | ) 26 | func('test.yaml') 27 | 28 | assert 'is not supported' in str(exc.value) 29 | 30 | 31 | def test_validate_extract_json_properties_func_args(): 32 | func = decorators.validate_extract_json_properties_func_args( 33 | lambda data, keys: True 34 | ) 35 | json_data = {'name': 'TEST'} 36 | json_keys = ('name',) 37 | has_func_result = func(json_data, json_keys) 38 | 39 | assert has_func_result 40 | 41 | 42 | def test_validate_extract_json_properties_func_args_with_wrong_data_format(): 43 | with pytest.raises(TypeError) as exc: 44 | func = decorators.validate_extract_json_properties_func_args( 45 | lambda data, keys: None 46 | ) 47 | json_data = 'key: value' 48 | func(json_data, ()) 49 | 50 | part_of_exc_msg = 'Not a type of {type}'.format(type=type(json_data)) 51 | 52 | assert part_of_exc_msg in str(exc.value) 53 | 54 | 55 | def test_extract_properties_values_from_json_with_wrong_keys_format(): 56 | with pytest.raises(TypeError) as exc: 57 | func = decorators.validate_extract_json_properties_func_args( 58 | lambda data, keys: None 59 | ) 60 | json_data = {'name': 'test', 'verb': 'GET'} 61 | json_keys = 'name, verb' 62 | func(json_data, json_keys) 63 | 64 | part_of_exc_msg = 'Not a type of {type}'.format(type=type(json_keys)) 65 | 66 | assert part_of_exc_msg in str(exc.value) 67 | 68 | 69 | def test_validate_data_against_json_schema(): 70 | func = decorators.validate_data_against_json_schema( 71 | lambda data: data 72 | ) 73 | data = { 74 | 'name': 'TEST: List all users', 75 | 'verb': 'GET', 76 | 'endpoint': 'users', 77 | 'host': 'https://localhost.com', 78 | } 79 | func_result = func(data) 80 | 81 | assert id(func_result) == id(data) 82 | 83 | 84 | def test_validate_data_against_json_schema_with_not_supported_argument_type(): 85 | with pytest.raises(TypeError) as exc: 86 | func = decorators.validate_data_against_json_schema( 87 | lambda data: data 88 | ) 89 | data = { 90 | 'TEST: List all users', 91 | 'GET', 92 | 'users', 93 | 'https://localhost.com', 94 | } 95 | func(data) 96 | 97 | part_of_exc_msg = 'Not a type of {type}'.format(type=type(data)) 98 | 99 | assert part_of_exc_msg in str(exc.value) 100 | 101 | 102 | def test_validate_data_against_json_schema_with_not_supported_http_method(): 103 | with pytest.raises(HTTPMethodNotSupportedError) as exc: 104 | func = decorators.validate_data_against_json_schema( 105 | lambda data: data 106 | ) 107 | data = { 108 | 'name': 'TEST: List all users', 109 | 'verb': 'HEAD', 110 | 'endpoint': 'users', 111 | 'host': 'https://localhost.com', 112 | } 113 | func(data) 114 | 115 | part_of_exc_msg = "An HTTP method ('{http_method}') is not".format( 116 | http_method=data['verb'] 117 | ) 118 | 119 | assert part_of_exc_msg in str(exc.value) 120 | 121 | 122 | def test_validate_data_against_json_schema_with_invalid_value_type_in_data(): 123 | with pytest.raises(ValidationError) as exc: 124 | func = decorators.validate_data_against_json_schema( 125 | lambda data: data 126 | ) 127 | data = { 128 | 'name': 'TEST: List all users', 129 | 'verb': 'GET', 130 | 'endpoint': 'users', 131 | 'host': 'https://localhost.com', 132 | 'response': True, 133 | } 134 | func(data) 135 | 136 | part_of_exc_msg = ( 137 | "{value} is not of type 'object'".format(value=data['response']) 138 | ) 139 | 140 | assert part_of_exc_msg in str(exc.value) 141 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from pyhttptest import exceptions 2 | 3 | 4 | def test_file_extension_error_exception(): 5 | file_extension_error = exceptions.FileExtensionError('.yaml') 6 | exception_message = ( 7 | "A file extension '.yaml' is not supported. " 8 | "Only a file with '.json' extension is supported" 9 | ) 10 | 11 | assert file_extension_error.message == exception_message 12 | 13 | 14 | def test_http_method_not_supported_error_exception(): 15 | http_method_not_supported_error = exceptions.HTTPMethodNotSupportedError('HEAD') 16 | exception_message = "An HTTP method ('HEAD') is not supported by the application." 17 | 18 | assert http_method_not_supported_error.message == exception_message 19 | -------------------------------------------------------------------------------- /tests/test_http.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | from requests import Response 6 | 7 | from pyhttptest import http 8 | from pyhttptest.exceptions import HTTPMethodNotSupportedError 9 | 10 | 11 | def test_method_dispatcher(): 12 | with pytest.raises(HTTPMethodNotSupportedError) as exc: 13 | args = ('head', 'http://localhost:8080/users') 14 | http.method_dispatcher(*args) 15 | 16 | exception_message = "An HTTP method ('HEAD') is not supported by the application." 17 | 18 | assert exception_message == str(exc.value) 19 | 20 | 21 | @patch('pyhttptest.http.requests.get', return_value=Response) 22 | def test_get(mock): 23 | mock.return_value.status_code = 200 24 | args = ('get', 'http://localhost:8080/users') 25 | response = http.get(*args) 26 | 27 | assert response.status_code == 200 28 | 29 | 30 | @patch('pyhttptest.http.requests.post', return_value=Response) 31 | def test_post(mock): 32 | mock.return_value.status_code = 200 33 | args = ('post', 'http://localhost:8080/users') 34 | response = http.post(*args) 35 | 36 | assert response.status_code == 200 37 | 38 | 39 | @patch('pyhttptest.http.requests.put', return_value=Response) 40 | def test_put(mock): 41 | mock.return_value.status_code = 204 42 | args = ('put', 'http://localhost:8080/users/1') 43 | response = http.put(*args) 44 | 45 | assert response.status_code == 204 46 | 47 | 48 | @patch('pyhttptest.http.requests.delete', return_value=Response) 49 | def test_delete(mock): 50 | mock.return_value.status_code = 204 51 | args = ('delete', 'http://localhost:8080/users/1') 52 | response = http.delete(*args) 53 | 54 | assert response.status_code == 204 55 | -------------------------------------------------------------------------------- /tests/test_printer.py: -------------------------------------------------------------------------------- 1 | from pyhttptest import printer 2 | 3 | 4 | def test_slice_str_args(): 5 | dummy_str = ''' 6 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 7 | Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, 8 | when an unknown printer took a galley of type and scrambled it to make a type 9 | specimen book. 10 | ''' 11 | sliced_dummy_str, = printer._slice_str_args(dummy_str) 12 | 13 | assert len(sliced_dummy_str) == 100 14 | 15 | 16 | def test_format_data_as_tabular(): 17 | data = ( 18 | 'Test: Extract all users', 19 | '200', 20 | ) 21 | tabular_data = printer._format_data_as_tabular(data) 22 | 23 | assert 'Test name' in tabular_data 24 | 25 | 26 | def test_process_data_for_print(): 27 | test_kwargs = [ 28 | { 29 | 'name': 'Test: process data for print', 30 | 'status_code': '200', 31 | }, 32 | { 33 | 'name': 'Test: process data for print', 34 | 'status_code': '200', 35 | } 36 | ] 37 | data_for_print = printer.prepare_data_for_print(test_kwargs) 38 | 39 | assert 'HTTP Response Status Code' in data_for_print 40 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from http.client import InvalidURL 4 | 5 | from pyhttptest import utils 6 | 7 | 8 | def test_extract_properties_values_from_json(): 9 | json_data = { 10 | 'name': 'TEST: List all users', 11 | 'verb': 'GET', 12 | 'endpoint': 'users', 13 | 'host': 'http://localhost:8080', 14 | 'headers': { 15 | 'Accept-Language': 'en-US' 16 | } 17 | } 18 | json_keys = ('verb', 'endpoint', 'host') 19 | extracted_keys_values = utils.extract_properties_values_from_json( 20 | json_data, 21 | json_keys 22 | ) 23 | expected_keys_values = ( 24 | json_data['verb'], 25 | json_data['endpoint'], 26 | json_data['host'] 27 | ) 28 | 29 | assert sorted(expected_keys_values) == sorted(extracted_keys_values) 30 | 31 | 32 | def test_extract_properties_values_of_type_dict_from_json(): 33 | json_data = { 34 | 'host': 'http://localhost:8080', 35 | 'headers': { 36 | 'Accept-Language': 'en-US' 37 | } 38 | } 39 | json_keys = ('headers',) 40 | extracted_keys_values = utils.extract_properties_values_of_type_dict_from_json( 41 | json_data, 42 | json_keys 43 | ) 44 | 45 | assert 'headers' in extracted_keys_values 46 | 47 | 48 | def test_prepare_url(): 49 | host = 'http://localhost:8080' 50 | endpoint = 'users' 51 | url = utils.prepare_url(host, endpoint) 52 | 53 | assert url == 'http://localhost:8080/users' 54 | 55 | 56 | def test_prepare_url_with_none_type_arg(): 57 | url = utils.prepare_url('http://localhost:8080', None) 58 | 59 | assert url is None 60 | 61 | 62 | def test_prepare_url_with_host_arg_ends_with_backslash(): 63 | host = 'http://localhost:8080/' 64 | endpoint = 'users' 65 | url = utils.prepare_url(host, endpoint) 66 | 67 | assert url == 'http://localhost:8080/users' 68 | 69 | 70 | def test_prepare_url_with_endpoint_arg_starts_with_backslash(): 71 | host = 'http://localhost:8080' 72 | endpoint = '/users' 73 | url = utils.prepare_url(host, endpoint) 74 | 75 | assert url == 'http://localhost:8080/users' 76 | 77 | 78 | def test_prepare_url_with_both_host_and_endpoint_args_contains_backslash(): 79 | host = 'http://localhost:8080/' 80 | endpoint = '/users' 81 | url = utils.prepare_url(host, endpoint) 82 | 83 | assert url == 'http://localhost:8080/users' 84 | 85 | 86 | def test_prepare_url_with_invalid_host_arg_format(): 87 | with pytest.raises(InvalidURL) as exc: 88 | host = 'localhost.com' 89 | endpoint = 'users' 90 | utils.prepare_url(host, endpoint) 91 | 92 | assert 'Invalid URL' in str(exc.value) 93 | 94 | 95 | def test_prepare_url_with_not_supported_url_scheme(): 96 | with pytest.raises(InvalidURL) as exc: 97 | host = 'ftp://localhost.com' 98 | endpoint = 'users' 99 | utils.prepare_url(host, endpoint) 100 | 101 | assert 'Invalid URL scheme' in str(exc.value) 102 | -------------------------------------------------------------------------------- /tests/test_wrappers.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from pyhttptest import wrappers 4 | 5 | 6 | @patch('pyhttptest.wrappers.core.extract_http_response_content', return_value=dict) 7 | @patch('pyhttptest.wrappers.core.send_http_request') 8 | def test_execute_single_test_scenario(_, mock): 9 | mock.return_value = { 10 | 'status_code': 200, 11 | 'headers': {'Content-Type': 'application/json'}, 12 | 'body': {'username': 'pyhttptest'} 13 | } 14 | json_data = { 15 | 'name': 'TEST: List all users', 16 | 'verb': 'GET', 17 | 'endpoint': 'users', 18 | 'host': 'http://localhost:8080', 19 | } 20 | dict_data = wrappers.execute_single_test_scenario(json_data) 21 | 22 | assert 'body' in dict_data 23 | 24 | 25 | @patch('pyhttptest.wrappers.core.extract_http_response_content', return_value=dict) 26 | @patch('pyhttptest.wrappers.core.send_http_request') 27 | def test_execute_multiple_test_scenarios(_, mock): 28 | mock.return_value = {'status_code': 200} 29 | list_of_dicts = [ 30 | { 31 | 'name': 'TEST: List all users', 32 | 'verb': 'GET', 33 | 'endpoint': 'users', 34 | 'host': 'http://localhost:8080', 35 | }, 36 | { 37 | 'name': 'TEST: Delete a user', 38 | 'verb': 'DELETE', 39 | 'endpoint': 'users/1', 40 | 'host': 'http://localhost:8080', 41 | } 42 | ] 43 | results = wrappers.execute_multiple_test_scenarios(list_of_dicts) 44 | 45 | assert len(results) == 2 46 | 47 | 48 | @patch('pyhttptest.wrappers.core.extract_http_response_content', return_value=dict) 49 | @patch('pyhttptest.wrappers.core.send_http_request') 50 | def test_cli_execute(_, mock): 51 | mock.return_value = { 52 | 'name': 'TEST: List all users', 53 | 'status_code': '200', 54 | 'headers': '{"Content-Type": "application/json"}', 55 | 'body': '{"username": "pyhttptest"}' 56 | } 57 | output = wrappers.execute_test_scenarios('data/HTTP_GET.json') 58 | 59 | return isinstance(output, str) 60 | --------------------------------------------------------------------------------