├── .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 |
--------------------------------------------------------------------------------