├── .circleci └── config.yml ├── .coveragerc ├── .gitattributes ├── .gitignore ├── .idea └── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── LICENSE ├── README.md ├── api ├── README.md ├── __init__.py ├── authentication_helper.py ├── cars_app.py └── templates │ ├── __init__.py │ └── insex.html ├── data ├── __init__.py ├── cars.py └── users.py ├── docs ├── Makefile ├── api.rst ├── api.templates.rst ├── conf.py ├── data.rst ├── index.rst ├── make.bat ├── modules.rst ├── tests.integration.rst ├── tests.rst ├── tests.system.rst ├── tests.unit.rst └── utils.rst ├── files ├── build-devops-automation-recycle_code-refresh_settings-preferences-512.png ├── iconfinder_api-code-window_532742.png ├── iconfinder_application-x-python_8974.png ├── markdown-cheatsheet-online.pdf ├── python-icon-18.jpg └── rest-api-icon-8.jpg ├── pytest.ini ├── requirements.txt ├── rocro.yml ├── tests ├── __init__.py ├── integration │ ├── __init__.py │ ├── post_cars_call_test.py │ ├── post_register_call_test.py │ └── put_cars_call_test.py ├── system │ ├── __init__.py │ ├── base_test.py │ ├── delete_car_negative_test.py │ ├── delete_car_positive_test.py │ ├── get_cars_negative_test.py │ ├── get_cars_positive_test.py │ ├── post_cars_negative_test.py │ ├── post_cars_positive_test.py │ ├── post_register_positive_test.py │ ├── put_cars_negaive_test.py │ └── put_cars_positive_test.py └── unit │ ├── __init__.py │ ├── internal_func_negative_test.py │ └── internal_func_positive_test.py └── utils ├── __init__.py └── get_args_from_cli.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # A basic unit of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | # directory where steps are run 5 | docker: # run the steps with Docker 6 | # CircleCI Python images available at: https://hub.docker.com/r/circleci/python/ 7 | - image: circleci/python:3.6.4 8 | environment: # environment variables for primary container 9 | FLASK_CONFIG: testing 10 | PIPENV_VENV_IN_PROJECT: true 11 | steps: # steps that comprise the `build` job 12 | - checkout # check out source code to working directory 13 | - run: sudo chown -R circleci:circleci /usr/local/bin 14 | - run: sudo chown -R circleci:circleci /usr/local/lib/python3.6/site-packages 15 | - run: 16 | command: | 17 | sudo pip install --upgrade pip 18 | sudo pip install pipenv 19 | sudo mkdir /home/circleci/project/test-resultse 20 | pipenv install -r requirements.txt 21 | 22 | - run: 23 | name: run tests 24 | command: | 25 | pip install flask 26 | python3 api/cars_app.py & 27 | pipenv run pytest 28 | 29 | - store_test_results: # Upload test results for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ 30 | path: test-results/ 31 | 32 | - store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 33 | path: test-results/ 34 | destination: tr1 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | # omit anything in a virtualenv directory anywhere 3 | omit = /home/travis/virtualenv/* 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # pipenv 85 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 86 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 87 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 88 | # install all needed dependencies. 89 | #Pipfile.lock 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | 121 | # Pyre type checker 122 | .pyre/ 123 | .idea/ 124 | allure-report/ 125 | allure-results/ 126 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Default ignored files 3 | /workspace.xml -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 1 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: 18 | - epub 19 | - pdf 20 | 21 | # Optionally set the version of Python and requirements required to build your docs 22 | python: 23 | version: 3.5 24 | install: 25 | - requirements: requirements.txt 26 | - method: pip 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial # required for Python >= 3.7 2 | 3 | language: python 4 | 5 | python: 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | 10 | env: 11 | global: 12 | - CC_TEST_REPORTER_ID=9cc8a024979648e0ea1d1e65619f25e61dfcf66c9748402fac9ee9a66feb6745 13 | 14 | install: 15 | - "pip install -r requirements.txt" 16 | - "pip install flask" 17 | - "pip install pytest-cov" #This plugin produces coverage reports 18 | - "pip install codecov" # install codecov 19 | - "pip install coverage" # install coverage 20 | - "npm install" # install npm 21 | 22 | before_script: # code coverage tool 23 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 24 | - chmod +x ./cc-test-reporter 25 | - ./cc-test-reporter before-build 26 | 27 | script: 28 | - 'python api/cars_app.py & pytest --cov-config=.coveragerc --cov-report term-missing --cov' # check test coverage with pytest-cov 29 | # - coverage run cars_app.py test -v 2 --exclude-tag=selenium 30 | 31 | after_script: 32 | - coverage xml 33 | - if [[ "$TRAVIS_PULL_REQUEST" == "false" && "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi 34 | 35 | after_success: 36 | # Generates coverage report, public repo using Travis: 37 | # Source: https://dev.to/j0nimost/using-codecov-with-travis-ci-pytest-cov-1dfj 38 | - codecov -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 2 | [![HitCount](http://hits.dwyl.com/ikostan/REST_API_AUTOMATION.svg)](http://hits.dwyl.com/ikostan/REST_API_AUTOMATION) 3 | [![Documentation Status](https://readthedocs.org/projects/practice-rest-api-automation-with-python-3/badge/?version=latest)](https://practice-rest-api-automation-with-python-3.readthedocs.io/en/latest/?badge=latest) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/0a3c971aa10e4e93b15944480cbf9b15)](https://www.codacy.com/manual/ikostan/REST_API_AUTOMATION?utm_source=github.com&utm_medium=referral&utm_content=ikostan/REST_API_AUTOMATION&utm_campaign=Badge_Grade) 5 | [![CircleCI](https://circleci.com/gh/ikostan/REST_API_AUTOMATION.svg?style=svg)](https://circleci.com/gh/ikostan/REST_API_AUTOMATION) 6 | [![Build Status](https://travis-ci.org/ikostan/REST_API_AUTOMATION.svg?branch=master)](https://travis-ci.org/ikostan/REST_API_AUTOMATION) 7 | [![codecov](https://codecov.io/gh/ikostan/REST_API_AUTOMATION/branch/master/graph/badge.svg)](https://codecov.io/gh/ikostan/REST_API_AUTOMATION) 8 | [![Test Coverage](https://api.codeclimate.com/v1/badges/6eefab86052a2d4be5ba/test_coverage)](https://codeclimate.com/github/ikostan/REST_API_AUTOMATION/test_coverage) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/6eefab86052a2d4be5ba/maintainability)](https://codeclimate.com/github/ikostan/REST_API_AUTOMATION/maintainability) 10 | 11 | # Practice REST API test automation with Python 3 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 | ## Project Description 21 | 22 | An API, or Application Program Interface, enables developers to integrate one app with another. To use an API, you make a request to a remote web server, and retrieve the data you need. Test Automation for APIs is needed to eliminate any possible errors, detect defects early, and ensure quality through the application development cycle. In this project, we’ll take a look at the automating API tests using Python. 23 | 24 | ### Table of Contents:
25 | 26 | 1. Main Objectives 27 | 2. Official Documentation Sources 28 | 3. Additional Tech Info Resources 29 | 4. Dev Environment 30 | 5. Nice to have tools 31 | 6. About REST API app 32 | 7. REST API app setup 33 | 8. API endpoints and examples 34 | 9. General guidelines: How to set up dev environment 35 | 10. Tech Issues and Problem Solving 36 | 37 | ### Main Objectives:
38 | 39 | 40 | - Creating REST API Automation 41 | - Cover following well known HTTP methods are commonly used in REST: GET, PUT, DELETE, POST. 42 | - Build fast and readable automation using minimal code 43 | - Industry-ready test structure 44 | - Build readable test report using Allure Framework 45 | - Test code should avoid violating principles like [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it), [SRP](https://en.wikipedia.org/wiki/Single_responsibility_principle) and [KISS](https://en.wikipedia.org/wiki/KISS_principle) 46 | - Using cloud automation platforms: [Codacy](https://www.codacy.com/), [CirclerCI](https://circleci.com), [TravisCI](https://travis-ci.org), [INSPECODE](https://inspecode.rocro.com), [Codecov](https://codecov.io) 47 | 48 | ### Official Documentation Sources
49 | 50 | 51 | - [Representational State Transfer (REST)](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) 52 | - [Designing a RESTful API with Python and Flask](https://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask) 53 | - [Allure Test Report](http://allure.qatools.ru/) 54 | - [cars-api](https://github.com/qxf2/cars-api) 55 | - [Practice API automation](http://35.167.62.251/) 56 | - [Flask](https://www.fullstackpython.com/flask.html) 57 | - [Practice REST API automation with Python 3’s documentation](https://practice-rest-api-automation-with-python-3.readthedocs.io/en/latest/?badge=latest) 58 | - [Testing Flask Applications](https://flask.palletsprojects.com/en/1.0.x/testing/) 59 | 60 | ### Additional Tech Info Resources
61 | 62 | 63 | - [REST API Tutorial](https://restfulapi.net) 64 | - [Full Pytest documentation](http://doc.pytest.org/en/latest/contents.html) 65 | - [PyCharm - Manage dependencies using requirements.txt](https://www.jetbrains.com/help/pycharm/managing-dependencies.html) 66 | - [Allure Framework](https://docs.qameta.io/allure/) 67 | - [Automated REST API Testing with Python](https://dev.to/dowenb/automated-rest-api-testing-with-python-2jm5) 68 | - [Writing Unit Tests for REST API in Python](https://hackernoon.com/writing-unit-tests-for-rest-api-in-python-web-application-2e675a601a53) 69 | - [Guide-to automating API tests using Python](https://www.grossum.com/blog/beginner-s-guide-to-automating-api-tests-using-python) 70 | - [Testing Flask application with basic authentication](https://gist.github.com/jarus/1160696) 71 | 72 | ### Dev Environment:
73 | 74 | 75 | - [Python 3.7.4](https://www.python.org/downloads/release/python-374/) 76 | - [PyTest 5.1.1](https://pypi.org/project/pytest/) 77 | - [Win 10 (64 bit)](https://www.microsoft.com/en-ca/software-download/windows10) 78 | - [PyCharm 2019.2 (Community Edition)](https://www.jetbrains.com/pycharm/download/#section=windows) 79 | - [GitHub Desktop 2.1.2](https://desktop.github.com/) 80 | - [GIT 2.22.0.windows.1](https://git-scm.com/download/win) 81 | 82 | ### Python Packages 83 | Full list of dependencies see [here.](https://github.com/ikostan/REST_API_AUTOMATION/blob/master/requirements.txt) 84 | 85 | ### Nice to have tools 86 | 87 | 88 | 1. [Fiddler: The free web debugging proxy](https://www.telerik.com/fiddler) 89 | 2. [Kite: Code Faster in Python](https://kite.com/) 90 | 91 | ### General guidelines: How to set up dev environment.
92 | 93 | 94 | 1. Install Python 95 | 2. Install PyCharm 96 | 3. Configure Python virtual environment and activate it 97 | 4. Install Python prerequisites/packages 98 | 5. Setup REST API app 99 | 100 | **NOTE:** for more detailed info please see "Tech Issues and Problem Solving" section
101 | 102 | ### About REST API app 103 | 104 | This REST application written in Python was built to help testers learn to write API automation. The application has endpoints for you to practice automating GET, POST, PUT and DELETE methods. We have also included permissioning and authentication too. This web application was developed by [Qxf2 Services](https://www.qxf2.com/?utm_source=carsapi&utm_medium=click&utm_campaign=From%20carspai). 105 | 106 | ### REST API app setup 107 | 108 | 109 | 1. In your terminal prompt, pip install flask 110 | 2. Copy the contents of [this file](https://github.com/qxf2/cars-api/blob/master/cars_app.py) and save it (anywhere) as cars_app.py 111 | 3. In your terminal prompt, cd directory_that_has_cars_app_py 112 | 4. In your terminal prompt, python cars_app.py 113 | 114 | If everything goes right, you should see an output similar to the following: 115 | ```bash 116 | * Serving Flask app "cars_app" (lazy loading) 117 | * Environment: production 118 | WARNING: This is a development server. Do not use it in a production deployment. 119 | Use a production WSGI server instead. 120 | * Debug mode: on 121 | * Restarting with stat 122 | * Debugger is active! 123 | * Debugger PIN: 105-519-712 124 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 125 | ``` 126 | 127 | ### API endpoints and examples 128 | 129 | 130 | - For more details please [click here](https://practice-rest-api-automation-with-python-3.readthedocs.io/en/latest/cars_app.html) 131 | 132 | ### Tech Issues and Problem Solving:
133 | 134 | 135 |
136 | Changing the project interpreter in the PyCharm project settings 137 | 138 | 1. In the **Settings/Preferences dialog** (Ctrl+Alt+S), select **Project | Project Interpreter**. 139 | 2. Expand the list of the available interpreters and click the **Show All** link. 140 | 3. Select the target interpreter. When PyCharm stops supporting any of the outdated Python versions, the corresponding project interpreter is marked as unsupported. 141 | 4. The Python interpreter name specified in the **Name** field, becomes visible in the list of available interpreters. Click **OK** to apply the changes. 142 | 143 | For more info please [check here](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html) 144 |
145 | 146 |
147 | PyCharm - Choosing Your Testing Framework 148 | 149 | 1. Open the Settings/Preferences dialog, and under the node Tools, click the page **Python Integrated Tools**. 150 | 2. On this page, click the **Default Test Runner** field. 151 | 3. Choose the desired test runner: 152 | 153 |
154 | 155 |
156 | 157 | For more info please see [Enable Pytest for you project](https://www.jetbrains.com/help/pycharm/pytest.html) 158 |
159 | 160 |
161 | Setting up Python3 virtual environment on Windows machine 162 | 163 | 1. open CMD
164 | 2. navigate to project directory, for example:
165 | ```bash 166 | cd C:\Users\superadmin\Desktop\Python\CodinGame 167 | ``` 168 | 3. run following command:
169 | ```bash 170 | pip install virtualenv 171 | ``` 172 | 4. run following command:
173 | ```bash 174 | virtualenv venv --python=python 175 | ``` 176 |
177 | 178 |
179 | Setting up Python3 virtual environment on Linx (Ubuntu) machine 180 | 181 | ### How to install virtualenv 182 | 183 | 1. Install **pip** first 184 | ```bash 185 | sudo apt-get install python3-pip 186 | ``` 187 | 188 | 2. Then install **virtualenv** using pip3 189 | ```bash 190 | sudo pip3 install virtualenv 191 | ``` 192 | 193 | 3. Now create a virtual environment 194 | ```bash 195 | virtualenv venv 196 | ``` 197 | >you can use any name insted of **venv** 198 | 199 | 4. You can also use a Python interpreter of your choice: 200 | 201 | ```bash 202 | virtualenv -p /usr/bin/python2.7 venv 203 | ``` 204 | 205 | 5. Active your virtual environment: 206 | 207 | ```bash 208 | source venv/bin/activate 209 | ``` 210 | 211 | 6. Using fish shell: 212 | 213 | ```bash 214 | source venv/bin/activate.fish 215 | ``` 216 | 217 | 7. To deactivate: 218 | 219 | ```bash 220 | deactivate 221 | ``` 222 | 223 | 8. Create virtualenv using Python3: 224 | 225 | ```bash 226 | virtualenv -p python3 myenv 227 | ``` 228 | 229 | 9. Instead of using virtualenv you can use this command in Python3: 230 | 231 | ```bash 232 | python3 -m venv myenv 233 | ``` 234 | 235 | [Source](https://gist.github.com/frfahim/73c0fad6350332cef7a653bcd762f08d) 236 |
237 | 238 |
239 | Activate Virtual Environment 240 | 241 | In a newly created virtualenv there will be a bin/activate shell script. For Windows systems, activation scripts are provided for CMD.exe and Powershell. 242 | 243 | 1. Open Terminal 244 | 2. Run: \path\to\env\Scripts\activate 245 | 246 | [Source](https://pypi.org/project/virtualenv/1.8.2/) 247 |
248 | 249 |
250 | Auto generate requirements.txt 251 | 252 | Any application typically has a set of dependencies that are required for that application to work. The requirements file is a way to specify and install specific set of package dependencies at once.
253 | Use pip’s freeze command to generate a requirements.txt file for your project: 254 | ```bash 255 | pip freeze > requirements.txt 256 | ``` 257 | 258 | If you save this in requirements.txt, you can follow this guide: [PyCharm - Manage dependencies using requirements.txt](https://www.jetbrains.com/help/pycharm/managing-dependencies.html), or you can:
259 | 260 | ```bash 261 | pip install -r requirements.txt 262 | ``` 263 | [Source](https://www.idiotinside.com/2015/05/10/python-auto-generate-requirements-txt/) 264 |
265 | 266 |
267 | error: RPC failed; curl 56 Recv failure: Connection was reset 268 | 269 | 1. Open Git Bash
270 | 2. Run: "git config --global http.postBuffer 157286400" 271 | 272 | [Source](https://stackoverflow.com/questions/36940425/gitlab-push-failed-error) 273 |
274 | 275 |
276 | How to fix in case .gitignore is ignored by Git 277 | 278 | Even if you haven't tracked the files so far, Git seems to be able to "know" about them even after you add them to .gitignore
279 | 280 | **NOTE:** 281 | 282 | - First commit your current changes, or you will lose them. 283 | - Then run the following commands from the top folder of your Git repository: 284 | 285 | ```bash 286 | git rm -r --cached . 287 | git add . 288 | git commit -m "fixed untracked files" 289 | ``` 290 |
291 | 292 |
293 | How to generate Allure report with history trends (Windows OS) 294 | 295 |
Step by step: 296 | 297 | 1. Run tests from pytest using following arguments: -v --alluredir=allure-results 298 | 2. Copy '.\allure-report\history\' folder into '.\allure-results\history\' 299 | 3. Run: allure generate .\allure-results\ -o .\allure-report\ --clean 300 | 4. Following output should appear: Report successfully generated to .\allure-report 301 | 5. Run: allure open .\allure-report\ 302 | 303 | [Source](https://github.com/allure-framework/allure2/issues/813) 304 |
305 | 306 |
307 | Sphinx Documentation Set Up 308 | 309 |
Step by step: 310 | 311 | 1. Create docs directory 312 | 2. Open cmd > Go to docs directory 313 | 3. cmd > Run: sphinx-quickstart 314 | **Note:** run with default answers 315 | 4. Go to docs/conf.py 316 | 5. Uncomment following lines: 317 | ```python 318 | import os 319 | import sys 320 | sys.path.insert(0, os.path.abspath('.')) 321 | ``` 322 | 6. Update extensions list as following: 323 | ```python 324 | extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] 325 | ``` 326 | 7. Update template as following: 327 | ```python 328 | html_theme = 'sphinx_rtd_theme' 329 | 330 | ``` 331 | 8. Update sys.path.insert as following: 332 | ```python 333 | sys.path.insert(0, os.path.abspath('..')) 334 | ``` 335 | 9. Go to docs/index.rst > add modules, see example below: 336 | ```bash 337 | 338 | .. toctree:: 339 | :maxdepth: 2 340 | :caption: Contents: 341 | 342 | modules 343 | ``` 344 | 10. Open cmd > run: 345 | ```python 346 | sphinx-apidoc -o . .. 347 | ``` 348 | 11. cmd > Run: make html 349 | 12. Install html template: 350 | ```python 351 | pip install sphinx_rtd_theme 352 | ``` 353 | 354 | [Video Tutorial](https://www.youtube.com/watch?v=b4iFyrLQQh4) 355 | [Sphinx Documentation](https://www.sphinx-doc.org/en/master/usage/quickstart.html) 356 | [More Info](https://stackoverflow.com/questions/13516404/sphinx-error-unknown-directive-type-automodule-or-autoclass) 357 |
358 | 359 |
360 | Auto-Generated Python Documentation with Sphinx 361 | 362 |
Step by step: 363 | 364 | 1. Open CMD 365 | 2. Go to docs directory 366 | 3. Run: make clean 367 | 4. Run: make html 368 | 369 | [Source](https://www.youtube.com/watch?v=b4iFyrLQQh4) 370 |
371 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # cars-api 2 | 3 | A sample REST application to help testers learn to write API automation 4 | 5 | This REST application written in Python was built solely to help QA learn to write API automation. 6 | 7 | The application has endpoints for you to practice automating GET, POST, PUT and DELETE methods. 8 | 9 | ## What is REST 10 | REST is acronym for REpresentational State Transfer. It is architectural style for distributed hypermedia systems and was first presented by Roy Fielding in 2000 in his famous [dissertation](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm). 11 | 12 | Like any other architectural style, REST also does have it’s own [6 guiding constraints](https://restfulapi.net/rest-architectural-constraints/) which must be satisfied if an interface needs to be referred as RESTful. These principles are listed below. 13 | 14 | ---- 15 | ### RUN 16 | ----- 17 | To run cars api on local machine: 18 | ```bash 19 | python cars_app.py` 20 | ``` 21 | 22 | ---- 23 | ### API ENDPOINTS & EXAMPLES 24 | ---- 25 | import requests 26 | 27 | GET: 28 | 29 | 1. /cars - Get the list of cars. Example: 30 | ```python 31 | response = requests.get(url='http://127.0.0.1:5000/cars',auth=(*username,*password)) 32 | ``` 33 | 34 | 2. /users - Get the list of users. Example: 35 | ```python 36 | response = requests.get(url='http://127.0.0.1:5000/users',auth=(username,password)) 37 | ``` 38 | 39 | 3. /cars/filter/ - Filter through the list of cars. Example: 40 | ```python 41 | response = requests.get(url='http://127.0.0.1:5000/cars/filter/hatchback',auth=(username,password)) 42 | ``` 43 | 44 | 4. /register - Get registered cars. Example: 45 | ```python 46 | response = requests.get(url='http://127.0.0.1:5000/register',auth=(username,password)) 47 | ``` 48 | 49 | 5. /cars/< name > - Get cars by name. Example: 50 | ```python 51 | response = requests.get(url='http://127.0.0.1:5000/cars/Swift',auth=(username,password)) 52 | ``` 53 | 54 | POST: 55 | 1. /cars/add - Add new cars 56 | Example: response = requests.post(url='http://127.0.0.1:5000/cars/add',json={'name':'figo','brand':'Ford','price_range':'2-3lacs','car_type':'hatchback'},auth=(username,password)) 57 | 58 | 2. /register/car - Register cars 59 | Example: response = requests.post(url='http://127.0.0.1:5000/register/car',params={'car_name':'figo','brand':'Ford'},json={'customer_name': 'Unai Emery','city': 'London'},auth=(username,password)) 60 | 61 | PUT: 62 | 1. /cars/update/< name > - Update car properties 63 | Example: response = requests.put(url='http://127.0.0.1:5000/cars/update/Vento',json={'name':Vento,'brand':'Ford','price_range':'2-3lacs','car_type':'sedan'},auth=(username,password)) 64 | 65 | DELETE: 66 | 1. /cars/remove/< name > - Delete cars from cars_list 67 | Example: response = requests.delete(url='http://127.0.0.1:5000/register/cars/remove/City',auth=(username,password)) 68 | 69 | 2. /car/delete/ - Delete first entry in car registration list 70 | Example: response = requests.delete(url='http://127.0.0.1:5000/register/car/delete',auth=(username,password)) 71 | 72 | * Get the username & password from user_list in the cars_app.py file -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/api/__init__.py -------------------------------------------------------------------------------- /api/authentication_helper.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """Authentication Helper Class""" 4 | 5 | from flask import jsonify 6 | 7 | 8 | class AuthenticationHelper: 9 | """ 10 | Authentication Helper Class. 11 | Contains helper methods for cars_app app 12 | """ 13 | 14 | @staticmethod 15 | def check_auth(username, password, user_list): 16 | """ 17 | check if the given user is valid 18 | :param user_list: 19 | :param username: 20 | :param password: 21 | :return: 22 | """ 23 | 24 | user = [user for user in user_list if user['name'] 25 | == username and user['password'] == password] 26 | if len(user) == 1: 27 | return True 28 | return False 29 | 30 | @staticmethod 31 | def authenticate_error(auth_flag): 32 | """ 33 | set auth message based on the 34 | authentication check result 35 | :param auth_flag: 36 | :return: 37 | """ 38 | if auth_flag is True: 39 | message = {'message': "Authenticate with proper credentials"} 40 | else: 41 | message = {'message': "Require Basic Authentication"} 42 | resp = jsonify(message) 43 | resp.status_code = 401 44 | resp.headers['WWW-Authenticate'] = 'Basic realm="Example"' 45 | return resp 46 | -------------------------------------------------------------------------------- /api/cars_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | cars api is a sample web application developed by 3 | Qxf2 Services to help testers learn API automation. 4 | This REST application written in Python was built 5 | solely to help QA learn to write API automation. 6 | The application has endpoints for you to practice 7 | automating GET, POST, PUT and DELETE methods. 8 | It includes endpoints that use URL parameters, 9 | jSON payloads, returns different response codes, etc. 10 | We have also included permissioning and authentication 11 | too to help you write role based API tests. 12 | 13 | IMPORTANT DISCLAIMER: The code here does not reflect 14 | Qxf2's coding standards and practices. 15 | 16 | Source: https://github.com/qxf2/cars-api 17 | """ 18 | 19 | import os 20 | import sys 21 | import logging 22 | import random 23 | import flask 24 | from functools import wraps 25 | from flask import Flask, request, jsonify, abort, render_template 26 | 27 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 28 | 29 | from api.authentication_helper import AuthenticationHelper 30 | from data.cars import Cars 31 | from data.users import Users 32 | 33 | 34 | app = Flask(__name__) 35 | # write logs for app filehandler of logging module 36 | # is not creating log directory if dir does not exist 37 | if not os.path.exists('log'): 38 | os.makedirs('log') 39 | file_handler = logging.FileHandler('log/app.log') 40 | app.logger.addHandler(file_handler) 41 | app.logger.setLevel(logging.INFO) 42 | 43 | CARS_LIST = Cars().get_cars() 44 | USER_LIST = Users().get_users() 45 | REGISTERED_CARS = [] 46 | 47 | 48 | def requires_auth(f): 49 | """ 50 | verify given user authentication details 51 | :param f: 52 | :return: 53 | """ 54 | 55 | @wraps(f) 56 | def decorated(*args, **kwargs): 57 | auth = request.authorization 58 | auth_flag = True 59 | 60 | if not auth: 61 | auth_flag = False 62 | return AuthenticationHelper.authenticate_error(auth_flag) 63 | elif not AuthenticationHelper.check_auth(auth.username, auth.password, USER_LIST): 64 | return AuthenticationHelper.authenticate_error(auth_flag) 65 | return f(*args, **kwargs) 66 | 67 | return decorated 68 | 69 | 70 | def requires_perm(): 71 | """ 72 | check if the authenticated user has a admin permission 73 | :return: 74 | """ 75 | auth = request.authorization 76 | perm_flag = False 77 | for user in USER_LIST: 78 | if user['name'] == auth.username and user['perm'] == 'admin': 79 | perm_flag = True 80 | return perm_flag 81 | return perm_flag 82 | 83 | 84 | @app.route("/", methods=["GET"]) 85 | def index_page(): 86 | """ 87 | this will help test GET without url params 88 | :return: 89 | """ 90 | return render_template('index.html') 91 | 92 | 93 | @app.route("/cars", methods=["GET"]) 94 | @requires_auth 95 | def get_cars(): 96 | """ 97 | this will help test GET without url params 98 | :return: 99 | """ 100 | return flask.jsonify({"cars_list": CARS_LIST, 101 | 'successful': True}) 102 | 103 | 104 | @app.route("/cars/", methods=["GET"]) 105 | @requires_auth 106 | def get_car_details(name): 107 | """ 108 | this will help test GET with url params 109 | :param name: 110 | :return: 111 | """ 112 | car = [car for car in CARS_LIST if car['name'] == name] 113 | if len(car) == 0: 114 | resp = jsonify({'message': 'No car found', 115 | 'successful': False}), 200 116 | else: 117 | resp = jsonify({'car': car[0], 118 | 'successful': True}), 200 119 | return resp 120 | 121 | 122 | @app.route("/cars/find", methods=["GET"]) 123 | @requires_auth 124 | def get_car(): 125 | """ 126 | this will help test GET with url params 127 | :return: 128 | """ 129 | car_name = request.args.get('car_name') 130 | brand = request.args.get('brand') 131 | if car_name != "" and \ 132 | car_name is not None and \ 133 | brand != "" and brand is not None: 134 | car = [car for car in CARS_LIST if car['name'] == car_name] 135 | if len(car) == 0: 136 | resp = jsonify({'message': 'No car found', 137 | 'successful': False}), 200 138 | else: 139 | resp = jsonify({'car': car[0], 'successful': True}), 200 140 | else: 141 | resp = jsonify( 142 | {'message': 'Not enough url_params', 143 | 'successful': False}) 144 | return resp 145 | 146 | 147 | @app.route("/cars/add", methods=["POST"]) 148 | @requires_auth 149 | def add_car(): 150 | """ 151 | this will help test POST without url params 152 | :return: 153 | """ 154 | if not request.json or 'name' not in request.json: 155 | resp = jsonify({'message': 'Not a json', 156 | 'successful': False, }), 400 157 | car = { 158 | 'name': request.json['name'], 159 | 'brand': request.json['brand'], 160 | 'price_range': request.json['price_range'], 161 | 'car_type': request.json['car_type'] 162 | } 163 | CARS_LIST.append(car) 164 | resp = jsonify({'car': car, 'successful': True}), 200 165 | return resp 166 | 167 | 168 | @app.route("/cars/update/", methods=["PUT"]) 169 | @requires_auth 170 | def update_car(name): 171 | """ 172 | this will help test PUT 173 | :param name: 174 | :return: 175 | """ 176 | resp = {} 177 | car = [car for car in CARS_LIST if car['name'] == name] 178 | if len(car) != 0: 179 | if not request.json or 'name' not in request.json: 180 | resp['message'], resp['successful'] = 'Not a json' 181 | status_code = 404 182 | else: 183 | car[0]['name'] = request.json['name'] 184 | car[0]['brand'] = request.json['brand'] 185 | car[0]['price_range'] = request.json['price_range'] 186 | car[0]['car_type'] = request.json['car_type'] 187 | resp['car'], resp['successful'] = car[0], True 188 | status_code = 200 189 | 190 | else: 191 | resp['message'], resp['successful'] = 'No car found', False 192 | status_code = 404 193 | return jsonify({'response': resp}), status_code 194 | 195 | 196 | @app.route("/cars/remove/", methods=["DELETE"]) 197 | @requires_auth 198 | def remove_car(name): 199 | """ 200 | this will help test DELETE 201 | :param name: 202 | :return: 203 | """ 204 | car = [car for car in CARS_LIST if car['name'] == name] 205 | if len(car) == 0: 206 | abort(404) 207 | CARS_LIST.remove(car[0]) 208 | return jsonify({'car': car[0], 'successful': True}), 200 209 | 210 | 211 | @app.route('/register/car', methods=['POST']) 212 | @requires_auth 213 | def register_car(): 214 | """ 215 | this will help test GET and POST with dynamic numbers in url 216 | :return: 217 | """ 218 | car_name = request.args.get('car_name') 219 | brand = request.args.get('brand') 220 | if car_name != "" and car_name is not None and brand != "" and brand is not None: 221 | car = [car for car in CARS_LIST if car['name'] == car_name] 222 | customer_details = { 223 | 'customer_name': request.json['customer_name'], 224 | 'city': request.json['city'] 225 | } 226 | registered_car = {'car': car[0], 'customer_details': request.json, 227 | 'registration_token': random.randrange(0, 4), 'successful': True} 228 | REGISTERED_CARS.append(registered_car) 229 | return jsonify({'registered_car': registered_car}) 230 | 231 | 232 | @app.route('/register/', methods=['GET']) 233 | @requires_auth 234 | def get_registered_cars(): 235 | """ 236 | this will help test GET without url_params 237 | :return: 238 | """ 239 | return jsonify({'registered': REGISTERED_CARS, 240 | 'successful': True}) 241 | 242 | 243 | @app.route('/register/car/delete/', methods=['DELETE']) 244 | @requires_auth 245 | def delete_registered_cars(): 246 | """ 247 | this will help test delete 248 | :return: 249 | """ 250 | del REGISTERED_CARS[0] 251 | return jsonify({'successful': True}), 200 252 | 253 | 254 | @app.route('/cars/filter/', methods=['GET']) 255 | @requires_auth 256 | def filter_cars(car_type): 257 | """ 258 | get cars of the given car type 259 | :param car_type: 260 | :return: 261 | """ 262 | filtered_list = [car for car in CARS_LIST if car['car_type'] == car_type] 263 | return jsonify({'cars': filtered_list}) 264 | 265 | 266 | @app.route('/users', methods=["GET"]) 267 | @requires_auth 268 | def get_user_list(): 269 | """ 270 | return user list if the given 271 | authenticated user has admin permission 272 | :return: 273 | """ 274 | if requires_perm() is True: 275 | return jsonify({'user_list': USER_LIST, 276 | 'successful': True}), 200 277 | return jsonify({'message': 'You are not ' 278 | 'permitted to access this resource', 279 | 'successful': False}), 403 280 | 281 | 282 | if __name__ == "__main__": 283 | app.run(host="127.0.0.1", port=5000, debug=True) 284 | -------------------------------------------------------------------------------- /api/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/api/templates/__init__.py -------------------------------------------------------------------------------- /api/templates/insex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cars API 7 | 8 | 9 | 10 | 11 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 |

Practice API automation

45 |
46 |

This REST application written in Python was built to help testers learn to write API automation. The application has endpoints for you to practice automating GET, POST, PUT and DELETE methods. We have also included permissioning and authentication too. This web application was developed by Qxf2 Services.

Note: You can look at the username/password combinations in the user_list variable in this file.

47 |
48 |
49 |

Setup

50 |

Since we don't want other people's tests interfering with your tests, we recommend you get setup with this application locally. It's easy and takes less than 15-minutes for absolute beginners. So, don't worry! 51 |

    52 |
  1. In your terminal prompt, pip install flask
  2. 53 |
  3. If you know git, git clone https://github.com/qxf2/cars-api.git
  4. 54 |
  5. If you don't know git, copy the contents of this file and save it (anywhere) as cars_app.py
  6. 55 |
  7. In your terminal prompt, cd directory_that_has_cars_app_py
  8. 56 |
  9. In your terminal prompt, python cars_app.py
  10. 57 |
58 | If everything goes right, you should see an output similar to the following image: 59 | 60 |

61 |
62 |
63 | 64 |

API endpoints and examples

65 |

This section lists the API endpoints present. It also lists the call you would make with Python's requests library.

66 | 67 |

1. GET

68 |
69 | a) /cars: Get a list of cars 70 |
 71 |                     
 72 | import requests
 73 | response = requests.get(url='http://127.0.0.1:5000/cars',auth=(username,password))
 74 |                     
 75 |                 
76 | b) /users: Get the list of users 77 |
 78 |                     
 79 | import requests
 80 | response = requests.get(url='http://127.0.0.1:5000/users',auth=(username,password))
 81 |                     
 82 |                 
83 | c) 84 | /cars/filter/<%car_type%> : Get the list of users 85 |
 86 |                     
 87 | import requests
 88 | response = requests.get(url='http://127.0.0.1:5000/cars/filter/hatchback',auth=(username,password))
 89 |                     
 90 |                 
91 | d) /register : Get registered cars 92 |
 93 |                     
 94 | import requests
 95 | response = requests.get(url='http://127.0.0.1:5000/register',auth=(username,password))
 96 |                     
 97 |                 
98 | e) /cars/<%name%> : Get cars by name 99 |
100 |                     
101 | import requests
102 | response = requests.get(url='http://127.0.0.1:5000/cars/Swift',auth=(username,password))
103 |                     
104 |                 
105 |
106 |

2. POST

107 |
108 | a) /cars/add: Add new cars 109 |
110 |                     
111 | import requests
112 | response = requests.post(url='http://127.0.0.1:5000/cars/add',json={'name':'figo','brand':'Ford','price_range':'2-3lacs','car_type':'hatchback'},auth=(username,password))
113 |                     
114 |                 
115 | b) /register/car: Register a car 116 |
117 |                     
118 | import requests
119 | response = requests.post(url='http://127.0.0.1:5000/register/car',params={'car_name':'figo','brand':'Ford'},json={'customer_name': 'Unai Emery','city': 'London'},auth=(username,password))
120 |                     
121 |                 
122 |
123 |

3. PUT

124 |
125 | a) /cars/update/<%name%>: Add new cars 126 |
127 |                     
128 | import requests
129 | esponse = requests.post(url='http://127.0.0.1:5000/cars/add',json={'name':'figo','brand':'Ford','price_range':'2-3lacs','car_type':'hatchback'},auth=(username,password))
130 |                     
131 |                 
132 |
133 |

4. DELETE

134 |
135 | a) /cars/remove/<%name%>: Add new cars 136 |
137 |                     
138 | import requests
139 | response = requests.delete(url='http://127.0.0.1:5000/register/cars/remove/City',auth=(username,password))
140 |                     
141 |                 
142 | b) /register/car/delete: Delete first entry in car registration list 143 |
144 |                     
145 | import requests
146 | response = requests.delete(url='http://127.0.0.1:5000/register/car/delete',auth=(username,password))
147 |                     
148 |                 
149 |
150 |
151 |
152 | 153 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/data/__init__.py -------------------------------------------------------------------------------- /data/cars.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """Cars Class""" 4 | 5 | 6 | class Cars: 7 | """ 8 | Contains cars list data 9 | """ 10 | 11 | CARS_LIST = [{"name": "Swift", 12 | "brand": "Maruti", 13 | "price_range": "3-5 lacs", 14 | "car_type": "hatchback"}, 15 | {"name": "Creta", 16 | "brand": "Hyundai", 17 | "price_range": "8-14 lacs", 18 | "car_type": "hatchback"}, 19 | {"name": "City", 20 | "brand": "Honda", 21 | "price_range": "3-6 lacs", 22 | "car_type": "sedan"}, 23 | {"name": "Vento", "brand": 24 | "Volkswagen", 25 | "price_range": "7-10 lacs", 26 | "car_type": "sedan"}] 27 | 28 | def get_cars(self): 29 | """ 30 | Returns cars list data 31 | :return: 32 | """ 33 | return self.CARS_LIST 34 | -------------------------------------------------------------------------------- /data/users.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """Users Class""" 4 | 5 | 6 | class Users: 7 | """ 8 | Contains Users data set 9 | """ 10 | 11 | USER_LIST = [{"name": "qxf2", 12 | "password": "qxf2", 13 | "perm": "admin"}, 14 | {"name": "eric", 15 | "password": "testqxf2", 16 | "perm": "non_admin"}, 17 | {"name": "morgan", 18 | "password": "testqxf2", 19 | "perm": "non_admin"}, 20 | {"name": "jack", 21 | "password": "qxf2", 22 | "perm": "non_admin"}] 23 | 24 | def get_users(self): 25 | """ 26 | Returns users list 27 | :return: 28 | """ 29 | return self.USER_LIST 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | api package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | api.templates 10 | 11 | Submodules 12 | ---------- 13 | 14 | api.authentication\_helper module 15 | --------------------------------- 16 | 17 | .. automodule:: api.authentication_helper 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | api.cars\_app module 23 | -------------------- 24 | 25 | .. automodule:: api.cars_app 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: api 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/api.templates.rst: -------------------------------------------------------------------------------- 1 | api.templates package 2 | ===================== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: api.templates 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Practice REST API test automation with Python 3' 21 | copyright = '2019, Egor Kostan' 22 | author = 'Egor Kostan' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] 34 | 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'sphinx_rtd_theme' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ['_static'] 56 | -------------------------------------------------------------------------------- /docs/data.rst: -------------------------------------------------------------------------------- 1 | data package 2 | ============ 3 | 4 | Submodules 5 | ---------- 6 | 7 | data.cars module 8 | ---------------- 9 | 10 | .. automodule:: data.cars 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | data.users module 16 | ----------------- 17 | 18 | .. automodule:: data.users 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: data 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Practice REST API test automation with Python 3 documentation master file, created by 2 | sphinx-quickstart on Fri Oct 4 19:56:45 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Practice REST API test automation with Python 3's documentation! 7 | =========================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | modules 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | REST_API_AUTOMATION 2 | =================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | api 8 | data 9 | tests 10 | utils 11 | -------------------------------------------------------------------------------- /docs/tests.integration.rst: -------------------------------------------------------------------------------- 1 | tests.integration package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | tests.integration.post\_cars\_call\_test module 8 | ----------------------------------------------- 9 | 10 | .. automodule:: tests.integration.post_cars_call_test 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tests.integration.post\_register\_call\_test module 16 | --------------------------------------------------- 17 | 18 | .. automodule:: tests.integration.post_register_call_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | tests.integration.put\_cars\_call\_test module 24 | ---------------------------------------------- 25 | 26 | .. automodule:: tests.integration.put_cars_call_test 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: tests.integration 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | tests package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | tests.integration 10 | tests.system 11 | tests.unit 12 | 13 | Module contents 14 | --------------- 15 | 16 | .. automodule:: tests 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | -------------------------------------------------------------------------------- /docs/tests.system.rst: -------------------------------------------------------------------------------- 1 | tests.system package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tests.system.base\_test module 8 | ------------------------------ 9 | 10 | .. automodule:: tests.system.base_test 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tests.system.delete\_car\_negative\_test module 16 | ----------------------------------------------- 17 | 18 | .. automodule:: tests.system.delete_car_negative_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | tests.system.delete\_car\_positive\_test module 24 | ----------------------------------------------- 25 | 26 | .. automodule:: tests.system.delete_car_positive_test 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | tests.system.get\_cars\_negative\_test module 32 | --------------------------------------------- 33 | 34 | .. automodule:: tests.system.get_cars_negative_test 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | tests.system.get\_cars\_positive\_test module 40 | --------------------------------------------- 41 | 42 | .. automodule:: tests.system.get_cars_positive_test 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | tests.system.post\_cars\_negative\_test module 48 | ---------------------------------------------- 49 | 50 | .. automodule:: tests.system.post_cars_negative_test 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | tests.system.post\_cars\_positive\_test module 56 | ---------------------------------------------- 57 | 58 | .. automodule:: tests.system.post_cars_positive_test 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | tests.system.post\_register\_positive\_test module 64 | -------------------------------------------------- 65 | 66 | .. automodule:: tests.system.post_register_positive_test 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | tests.system.put\_cars\_negaive\_test module 72 | -------------------------------------------- 73 | 74 | .. automodule:: tests.system.put_cars_negaive_test 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | tests.system.put\_cars\_positive\_test module 80 | --------------------------------------------- 81 | 82 | .. automodule:: tests.system.put_cars_positive_test 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | 88 | Module contents 89 | --------------- 90 | 91 | .. automodule:: tests.system 92 | :members: 93 | :undoc-members: 94 | :show-inheritance: 95 | -------------------------------------------------------------------------------- /docs/tests.unit.rst: -------------------------------------------------------------------------------- 1 | tests.unit package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tests.unit.internal\_func\_negative\_test module 8 | ------------------------------------------------ 9 | 10 | .. automodule:: tests.unit.internal_func_negative_test 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tests.unit.internal\_func\_positive\_test module 16 | ------------------------------------------------ 17 | 18 | .. automodule:: tests.unit.internal_func_positive_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: tests.unit 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | utils package 2 | ============= 3 | 4 | Submodules 5 | ---------- 6 | 7 | utils.get\_args\_from\_cli module 8 | --------------------------------- 9 | 10 | .. automodule:: utils.get_args_from_cli 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: utils 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /files/build-devops-automation-recycle_code-refresh_settings-preferences-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/build-devops-automation-recycle_code-refresh_settings-preferences-512.png -------------------------------------------------------------------------------- /files/iconfinder_api-code-window_532742.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/iconfinder_api-code-window_532742.png -------------------------------------------------------------------------------- /files/iconfinder_application-x-python_8974.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/iconfinder_application-x-python_8974.png -------------------------------------------------------------------------------- /files/markdown-cheatsheet-online.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/markdown-cheatsheet-online.pdf -------------------------------------------------------------------------------- /files/python-icon-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/python-icon-18.jpg -------------------------------------------------------------------------------- /files/rest-api-icon-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/files/rest-api-icon-8.jpg -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # Configuration of py.test 2 | [pytest] 3 | python_files=*_test.py 4 | ignore=venv 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | allure-pytest==2.8.5 3 | allure-python-commons==2.8.5 4 | atomicwrites==1.3.0 5 | attrs==19.1.0 6 | Babel==2.7.0 7 | certifi==2019.9.11 8 | chardet==3.0.4 9 | Click==7.0 10 | colorama==0.4.1 11 | docutils==0.15.2 12 | Flask==1.1.1 13 | idna==2.8 14 | imagesize==1.1.0 15 | importlib-metadata==0.23 16 | itsdangerous==1.1.0 17 | Jinja2==2.10.1 18 | MarkupSafe==1.1.1 19 | more-itertools==7.2.0 20 | packaging==19.2 21 | pluggy==0.13.0 22 | py==1.8.0 23 | Pygments==2.4.2 24 | pyparsing==2.4.2 25 | pytest==5.1.3 26 | pytz==2019.2 27 | requests==2.22.0 28 | six==1.12.0 29 | snowballstemmer==1.9.1 30 | Sphinx==2.2.0 31 | sphinx-rtd-theme==0.4.3 32 | sphinxcontrib-applehelp==1.0.1 33 | sphinxcontrib-devhelp==1.0.1 34 | sphinxcontrib-htmlhelp==1.0.2 35 | sphinxcontrib-jsmath==1.0.1 36 | sphinxcontrib-qthelp==1.0.2 37 | sphinxcontrib-serializinghtml==1.1.3 38 | urllib3==1.25.5 39 | wcwidth==0.1.7 40 | Werkzeug==0.16.0 41 | zipp==0.6.0 42 | -------------------------------------------------------------------------------- /rocro.yml: -------------------------------------------------------------------------------- 1 | # inspecode.rocro.com config file 2 | inspecode: 3 | experimental: 4 | runtime: 5 | python: 3.6.0 6 | pycodestyle: default 7 | pylint: 8 | ignore: 'docs/*' 9 | pytest: default -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/post_cars_call_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | POST Call Integration Test 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import base64 12 | import allure 13 | import unittest 14 | from flask import json 15 | from api.cars_app import app 16 | 17 | 18 | @allure.epic('Simple Flask App') 19 | @allure.parent_suite('REST API') 20 | @allure.suite("Integration Tests") 21 | @allure.sub_suite("Positive Tests") 22 | @allure.feature("POST") 23 | @allure.story('Add New Car') 24 | class PostCarsCallTestCase(unittest.TestCase): 25 | """ 26 | Testing a JSON API implemented in Flask. 27 | POST Call Integration Test. 28 | 29 | The POST method is used to request that the 30 | origin server accept the entity enclosed in the 31 | request as a new subordinate of the resource 32 | identified by the Request-URI in the Request-Line. 33 | It essentially means that POST request-URI 34 | should be of a collection URI. 35 | 36 | POST is NOT idempotent. 37 | So if you retry the request N times, 38 | you will end up having N resources with N 39 | different URIs created on server. 40 | 41 | Use POST when you want to add a child resource 42 | under resources collection. 43 | """ 44 | 45 | def setUp(self) -> None: 46 | with allure.step("Prepare test data"): 47 | self.new_car = {"name": "Punto", 48 | "brand": "Fiat", 49 | "price_range": "1-3 lacs", 50 | "car_type": "hatchback"} 51 | 52 | self.non_admin_user = {"name": "eric", 53 | "password": "testqxf2", 54 | "perm": "non_admin"} 55 | 56 | self.admin_user = {"name": "qxf2", 57 | "password": "qxf2", 58 | "perm": "admin"} 59 | 60 | def test_post_car_non_admin(self): 61 | """ 62 | Test POST call using non admin user credentials. 63 | :return: 64 | """ 65 | 66 | allure.dynamic.title("Add car using " 67 | "POST call and non " 68 | "admin credentials") 69 | allure.dynamic.severity(allure.severity_level.NORMAL) 70 | 71 | with allure.step("Send PUT request"): 72 | response = app.test_client().post( 73 | '{}'.format('/cars/add'), 74 | data=json.dumps(self.new_car), 75 | content_type='application/json', 76 | # Testing Flask application 77 | # with basic authentication 78 | # Source: https://gist.github.com/jarus/1160696 79 | headers={ 80 | 'Authorization': 'Basic ' + 81 | base64.b64encode(bytes(self.non_admin_user['name'] + 82 | ":" + 83 | self.non_admin_user['password'], 84 | 'ascii')).decode('ascii') 85 | } 86 | ) 87 | 88 | with allure.step("Verify status code"): 89 | self.assertEqual(200, response.status_code) 90 | 91 | with allure.step("Convert response into JSON data"): 92 | data = json.loads(response.get_data(as_text=True)) 93 | # print("\nDATA:\n{}\n".format(data)) # Debug only 94 | 95 | with allure.step("Verify 'successful' flag"): 96 | self.assertTrue(data['successful']) 97 | 98 | with allure.step("Verify new car data"): 99 | self.assertDictEqual(self.new_car, data['car']) 100 | 101 | def test_post_car_admin(self): 102 | """ 103 | Test POST call using admin user credentials. 104 | :return: 105 | """ 106 | 107 | allure.dynamic.title("Add car using " 108 | "POST call and admin " 109 | "credentials") 110 | allure.dynamic.severity(allure.severity_level.NORMAL) 111 | 112 | with allure.step("Send PUT request"): 113 | response = app.test_client().post( 114 | '{}'.format('/cars/add'), 115 | data=json.dumps(self.new_car), 116 | content_type='application/json', 117 | # Testing Flask application 118 | # with basic authentication 119 | # Source: https://gist.github.com/jarus/1160696 120 | headers={ 121 | 'Authorization': 'Basic ' + 122 | base64.b64encode(bytes(self.admin_user['name'] + 123 | ":" + 124 | self.admin_user['password'], 125 | 'ascii')).decode('ascii') 126 | } 127 | ) 128 | 129 | with allure.step("Verify status code"): 130 | self.assertEqual(200, response.status_code) 131 | 132 | with allure.step("Convert response into JSON data"): 133 | data = json.loads(response.get_data(as_text=True)) 134 | # print("\nDATA:\n{}\n".format(data)) # Debug only 135 | 136 | with allure.step("Verify 'successful' flag"): 137 | self.assertTrue(data['successful']) 138 | 139 | with allure.step("Verify new car data"): 140 | self.assertDictEqual(self.new_car, data['car']) 141 | -------------------------------------------------------------------------------- /tests/integration/post_register_call_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | POST Call Integration Test 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import base64 12 | import allure 13 | import unittest 14 | from flask import json 15 | from api.cars_app import app 16 | 17 | 18 | @allure.epic('Simple Flask App') 19 | @allure.parent_suite('REST API') 20 | @allure.suite("Integration Tests") 21 | @allure.sub_suite("Positive Tests") 22 | @allure.feature("POST") 23 | @allure.story('Car Registration') 24 | class PostCarRegistrationCallTestCase(unittest.TestCase): 25 | """ 26 | Testing a JSON API implemented in Flask. 27 | POST Call Integration Test. 28 | 29 | The POST method is used to request that the 30 | origin server accept the entity enclosed in the 31 | request as a new subordinate of the resource 32 | identified by the Request-URI in the Request-Line. 33 | It essentially means that POST request-URI 34 | should be of a collection URI. 35 | 36 | POST is NOT idempotent. 37 | So if you retry the request N times, 38 | you will end up having N resources with N 39 | different URIs created on server. 40 | 41 | Use POST when you want to add a child resource 42 | under resources collection. 43 | """ 44 | 45 | def setUp(self) -> None: 46 | with allure.step("Prepare test data"): 47 | 48 | self.new_car = {"car_name": "Creta", 49 | "brand": "Hyundai"} 50 | 51 | self.url = '{}?car_name={}&brand={}'.format('/register/car', 52 | self.new_car['car_name'], 53 | self.new_car['brand']) 54 | 55 | self.customer = {'customer_name': 'Unai Emery', 56 | 'city': 'London'} 57 | 58 | self.non_admin_user = {"name": "eric", 59 | "password": "testqxf2", 60 | "perm": "non_admin"} 61 | 62 | self.admin_user = {"name": "qxf2", 63 | "password": "qxf2", 64 | "perm": "admin"} 65 | 66 | def test_post_register_car_non_admin(self): 67 | """ 68 | Test POST (register car) call 69 | using non admin user credentials. 70 | :return: 71 | """ 72 | 73 | allure.dynamic.title("Add car using " 74 | "POST call and non " 75 | "admin credentials") 76 | allure.dynamic.severity(allure.severity_level.NORMAL) 77 | 78 | with allure.step("Send POST request"): 79 | 80 | response = app.test_client().post( 81 | self.url, 82 | data=json.dumps(self.customer), 83 | content_type='application/json', 84 | # Testing Flask application 85 | # with basic authentication 86 | # Source: https://gist.github.com/jarus/1160696 87 | headers={ 88 | 'Authorization': 'Basic ' + 89 | base64.b64encode(bytes(self.non_admin_user['name'] + 90 | ":" + 91 | self.non_admin_user['password'], 92 | 'ascii')).decode('ascii') 93 | } 94 | ) 95 | 96 | with allure.step("Verify status code"): 97 | self.assertEqual(200, 98 | response.status_code) 99 | 100 | def test_post_register_car_admin(self): 101 | """ 102 | Test POST (register car) call 103 | using admin user credentials. 104 | :return: 105 | """ 106 | 107 | allure.dynamic.title("Add car using " 108 | "POST call and admin " 109 | "credentials") 110 | allure.dynamic.severity(allure.severity_level.NORMAL) 111 | 112 | with allure.step("Send POST request"): 113 | 114 | response = app.test_client().post( 115 | self.url, 116 | data=json.dumps(self.customer), 117 | content_type='application/json', 118 | # Testing Flask application 119 | # with basic authentication 120 | # Source: https://gist.github.com/jarus/1160696 121 | headers={ 122 | 'Authorization': 'Basic ' + 123 | base64.b64encode(bytes(self.admin_user['name'] + 124 | ":" + 125 | self.admin_user['password'], 126 | 'ascii')).decode('ascii') 127 | } 128 | ) 129 | 130 | with allure.step("Verify status code"): 131 | self.assertEqual(200, 132 | response.status_code) 133 | -------------------------------------------------------------------------------- /tests/integration/put_cars_call_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | PUT Call Integration Test 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import base64 12 | import allure 13 | import unittest 14 | from flask import json 15 | from api.cars_app import app 16 | 17 | 18 | @allure.epic('Simple Flask App') 19 | @allure.parent_suite('REST API') 20 | @allure.suite("Integration Tests") 21 | @allure.sub_suite("Positive Tests") 22 | @allure.feature("PUT") 23 | @allure.story('Car Update') 24 | class PutCarsCallTestCase(unittest.TestCase): 25 | """ 26 | Testing a JSON API implemented in Flask. 27 | PUT Call Integration Test. 28 | 29 | PUT method requests for the enclosed entity be stored under 30 | the supplied Request-URI. If the Request-URI refers to an 31 | already existing resource – an update operation will happen, 32 | otherwise create operation should happen if Request-URI is a 33 | valid resource URI (assuming client is allowed to determine 34 | resource identifier). 35 | 36 | PUT method is idempotent. So if you send retry a request 37 | multiple times, that should be equivalent to single request 38 | modification. 39 | 40 | Use PUT when you want to modify a singular resource 41 | which is already a part of resources collection. 42 | PUT replaces the resource in its entirety. 43 | """ 44 | 45 | def setUp(self) -> None: 46 | 47 | with allure.step("Prepare test data"): 48 | 49 | self.car_original = {"name": "Creta", 50 | "brand": "Hyundai", 51 | "price_range": "8-14 lacs", 52 | "car_type": "hatchback"} 53 | 54 | self.car_updated = {"name": "Creta", 55 | "brand": "Hyundai", 56 | "price_range": "6-9 lacs", 57 | "car_type": "hatchback"} 58 | 59 | self.non_admin_user = {"name": "eric", 60 | "password": "testqxf2", 61 | "perm": "non_admin"} 62 | 63 | self.admin_user = {"name": "qxf2", 64 | "password": "qxf2", 65 | "perm": "admin"} 66 | 67 | def test_put_cars_update_non_admin(self): 68 | """ 69 | Test PUT call using non admin user credentials. 70 | :return: 71 | """ 72 | 73 | allure.dynamic.title("Update car properties using " 74 | "PUT call and non admin credentials") 75 | allure.dynamic.severity(allure.severity_level.NORMAL) 76 | 77 | with allure.step("Send PUT request"): 78 | response = app.test_client().put( 79 | '{}{}'.format('/cars/update/', 80 | self.car_original['name']), 81 | data=json.dumps(self.car_updated), 82 | content_type='application/json', 83 | 84 | # Testing Flask application 85 | # with basic authentication 86 | # Source: https://gist.github.com/jarus/1160696 87 | headers={ 88 | 'Authorization': 'Basic ' + 89 | base64.b64encode(bytes(self.non_admin_user['name'] + 90 | ":" + 91 | self.non_admin_user['password'], 92 | 'ascii')).decode('ascii') 93 | } 94 | ) 95 | 96 | with allure.step("Verify status code"): 97 | self.assertEqual(200, response.status_code) 98 | 99 | with allure.step("Convert response into JSON data"): 100 | data = json.loads(response.get_data(as_text=True)) 101 | # print("\nDATA:\n{}\n".format(data)) # Debug only 102 | 103 | with allure.step("Verify 'successful' flag"): 104 | self.assertTrue(data['response']['successful']) 105 | 106 | with allure.step("Verify updated car data"): 107 | self.assertDictEqual(self.car_updated, 108 | data['response']['car']) 109 | 110 | def test_put_cars_update_admin(self): 111 | """ 112 | Test PUT call using admin user credentials. 113 | :return: 114 | """ 115 | 116 | allure.dynamic.title("Update car properties using " 117 | "PUT call and admin credentials") 118 | allure.dynamic.severity(allure.severity_level.NORMAL) 119 | 120 | with allure.step("Send PUT request"): 121 | response = app.test_client().put( 122 | '{}{}'.format('/cars/update/', 123 | self.car_original['name']), 124 | data=json.dumps(self.car_updated), 125 | content_type='application/json', 126 | 127 | # Testing Flask application 128 | # with basic authentication 129 | # Source: https://gist.github.com/jarus/1160696 130 | headers={ 131 | 'Authorization': 'Basic ' + 132 | base64.b64encode(bytes(self.admin_user['name'] + 133 | ":" + 134 | self.admin_user['password'], 135 | 'ascii')).decode('ascii') 136 | } 137 | ) 138 | 139 | with allure.step("Verify status code"): 140 | self.assertEqual(200, response.status_code) 141 | 142 | with allure.step("Convert response into JSON data"): 143 | data = json.loads(response.get_data(as_text=True)) 144 | # print("\nDATA:\n{}\n".format(data)) # Debug only 145 | 146 | with allure.step("Verify 'successful' flag"): 147 | self.assertTrue(data['response']['successful']) 148 | 149 | with allure.step("Verify updated car data"): 150 | self.assertDictEqual(self.car_updated, 151 | data['response']['car']) 152 | -------------------------------------------------------------------------------- /tests/system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/tests/system/__init__.py -------------------------------------------------------------------------------- /tests/system/base_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Base Test Case 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import os 12 | import platform 13 | import time 14 | import unittest 15 | import allure 16 | # from utils.get_args_from_cli import get_args 17 | 18 | 19 | class BaseTestCase(unittest.TestCase): 20 | 21 | @classmethod 22 | def setUpClass(cls) -> None: 23 | """ 24 | 1. Get Args from CLI 25 | 2. Set Test URL 26 | 3. Start REST API Service 27 | :return: 28 | """ 29 | with allure.step("Get args from CLI"): 30 | # cls.args = get_args() 31 | pass 32 | 33 | with allure.step("Set test URL"): 34 | cls.URL = 'http://127.0.0.1:5000' 35 | 36 | with allure.step("Start REST API Service"): 37 | print("\nOS: {}\n".format(platform.system())) 38 | 39 | if platform.system() == 'Windows': 40 | os.system("start /B start cmd.exe @cmd /k python ../api/cars_app.py") 41 | time.sleep(5) 42 | 43 | if platform.system() == 'Linux': 44 | # os.system("python ../cars_app.py &") 45 | pass 46 | 47 | @classmethod 48 | def tearDownClass(cls) -> None: 49 | """ 50 | 1. Stop REST API Service 51 | :return: 52 | """ 53 | 54 | # with allure.step("Stop REST API Service"): 55 | # if platform.system() == 'Windows': 56 | # os.system('taskkill /F /IM python.exe') 57 | 58 | # if platform.system() == 'Linux': 59 | # os.system('pkill -f cars_app.py') 60 | pass 61 | -------------------------------------------------------------------------------- /tests/system/delete_car_negative_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: DELETE 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Negative Tests") 21 | @allure.feature("DELETE") 22 | @allure.story('Cars') 23 | class DeleteCarNegativeTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Negative Test: DELETE call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Arrange expected results (cars list)"): 35 | self.delete_car_url = '/cars/remove/' 36 | self.car = {"name": "City", 37 | "brand": "Honda", 38 | "price_range": "3-6 lacs", 39 | "car_type": "sedan"} 40 | 41 | def test_delete_car_wrong_admin_credentials(self): 42 | """ 43 | Delete car using wrong admin user credentials. 44 | :return: 45 | """ 46 | 47 | allure.dynamic.title("Delete car using " 48 | "wrong admin user credentials") 49 | allure.dynamic.severity(allure.severity_level.BLOCKER) 50 | 51 | with allure.step("Verify user permissions"): 52 | username = USER_LIST[0]['name'] 53 | password = USER_LIST[2]['password'] 54 | self.assertEqual("admin", 55 | USER_LIST[0]['perm']) 56 | 57 | with allure.step("Send DELETE request"): 58 | response = requests.delete(url='{}{}{}'.format(self.URL, 59 | self.delete_car_url, 60 | self.car['name']), 61 | auth=(username, 62 | password)) 63 | 64 | with allure.step("Verify status code"): 65 | self.assertEqual(401, 66 | response.status_code) 67 | with allure.step("Verify error message"): 68 | self.assertEqual("Authenticate with proper credentials", 69 | response.json()["message"]) 70 | 71 | def test_delete_car_wrong_non_admin_credentials(self): 72 | """ 73 | Get full list of cars using wrong 74 | non admin user credentials. 75 | :return: 76 | """ 77 | 78 | allure.dynamic.title("Get list of cars using wrong " 79 | "non admin user credentials") 80 | allure.dynamic.severity(allure.severity_level.BLOCKER) 81 | 82 | with allure.step("Verify user permissions"): 83 | username = USER_LIST[1]['name'] 84 | password = USER_LIST[3]['password'] 85 | self.assertEqual("non_admin", 86 | USER_LIST[1]['perm']) 87 | 88 | with allure.step("Send DELETE request"): 89 | response = requests.delete(url='{}{}{}'.format(self.URL, 90 | self.delete_car_url, 91 | self.car['name']), 92 | auth=(username, 93 | password)) 94 | 95 | with allure.step("Verify status code"): 96 | self.assertEqual(401, 97 | response.status_code) 98 | 99 | with allure.step("Verify error message"): 100 | self.assertEqual("Authenticate with proper credentials", 101 | response.json()["message"]) 102 | -------------------------------------------------------------------------------- /tests/system/delete_car_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: DELETE 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import CARS_LIST, USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Positive Tests") 21 | @allure.feature("DELETE") 22 | @allure.story('Cars') 23 | class DeleteCarPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Positive Test: DELETE call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Arrange expected results (cars list)"): 35 | self.post_url = '/cars/add' 36 | self.delete_car_url = '/cars/remove/' 37 | self.cars = CARS_LIST 38 | self.car = {"name": "City", 39 | "brand": "Honda", 40 | "price_range": "3-6 lacs", 41 | "car_type": "sedan"} 42 | 43 | def tearDown(self) -> None: 44 | 45 | username = USER_LIST[0]['name'] 46 | password = USER_LIST[0]['password'] 47 | 48 | with allure.step("Restore original cars list"): 49 | requests.post(url=self.URL + self.post_url, 50 | json=self.car, 51 | auth=(username, 52 | password)) 53 | 54 | def test_delete_car_admin(self): 55 | """ 56 | Delete car using admin user credentials. 57 | :return: 58 | """ 59 | 60 | allure.dynamic.title("Delete car " 61 | "using admin user credentials") 62 | allure.dynamic.severity(allure.severity_level.BLOCKER) 63 | 64 | with allure.step("Verify user permissions"): 65 | username = USER_LIST[0]['name'] 66 | password = USER_LIST[0]['password'] 67 | self.assertEqual("admin", 68 | USER_LIST[0]['perm']) 69 | 70 | with allure.step("Send DELETE request"): 71 | response = requests.delete(url='{}{}{}'.format(self.URL, 72 | self.delete_car_url, 73 | self.car['name']), 74 | auth=(username, 75 | password)) 76 | 77 | with allure.step("Verify status code"): 78 | self.assertEqual(200, 79 | response.status_code) 80 | 81 | with allure.step("Verify 'successful' flag"): 82 | self.assertTrue(response.json()['successful']) 83 | 84 | with allure.step("Verify retrieved cars list"): 85 | self.assertDictEqual(self.car, 86 | response.json()['car']) 87 | 88 | def test_delete_car_non_admin(self): 89 | """ 90 | Get full list of cars using non admin user credentials. 91 | :return: 92 | """ 93 | 94 | allure.dynamic.title("Get list of cars " 95 | "using non admin user credentials") 96 | allure.dynamic.severity(allure.severity_level.BLOCKER) 97 | 98 | with allure.step("Verify user permissions"): 99 | username = USER_LIST[1]['name'] 100 | password = USER_LIST[1]['password'] 101 | self.assertEqual("non_admin", 102 | USER_LIST[1]['perm']) 103 | 104 | with allure.step("Send DELETE request"): 105 | response = requests.delete(url='{}{}{}'.format(self.URL, 106 | self.delete_car_url, 107 | self.car['name']), 108 | auth=(username, 109 | password)) 110 | 111 | with allure.step("Verify status code"): 112 | self.assertEqual(200, 113 | response.status_code) 114 | 115 | with allure.step("Verify 'successful' flag"): 116 | self.assertTrue(response.json()['successful']) 117 | 118 | with allure.step("Verify retrieved cars list"): 119 | self.assertDictEqual(self.car, 120 | response.json()['car']) 121 | -------------------------------------------------------------------------------- /tests/system/get_cars_negative_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: GET 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Negative Tests") 21 | @allure.feature("GET") 22 | @allure.story('Cars') 23 | class GetCarsNegativeTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Negative Test: GET call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Prepare test data"): 35 | self.cars_url = '/cars' 36 | 37 | def test_get_list_of_cars_admin_wrong_credentials(self): 38 | """ 39 | Get full list of cars using wrong admin user credentials. 40 | Wrong password + Correct username. 41 | Correct password + Wrong username. 42 | :return: 43 | """ 44 | 45 | allure.dynamic.title("Get list of cars using wrong" 46 | " admin user credentials") 47 | allure.dynamic.severity(allure.severity_level.BLOCKER) 48 | 49 | with allure.step("Verify user permissions"): 50 | username = USER_LIST[0]['name'] 51 | password = USER_LIST[1]['password'] 52 | self.assertEqual("admin", 53 | USER_LIST[0]['perm']) 54 | 55 | with allure.step("Send GET request with wrong credentials"): 56 | response = requests.get(self.URL + self.cars_url, 57 | auth=(username, 58 | password)) 59 | 60 | with allure.step("Verify status code"): 61 | self.assertEqual(401, 62 | response.status_code) 63 | 64 | with allure.step("Verify error message"): 65 | self.assertEqual("Authenticate with proper credentials", 66 | response.json()["message"]) 67 | 68 | def test_get_list_of_cars_empty_credentials(self): 69 | """ 70 | Get full list of cars using empty username/password. 71 | Empty password + Empty username. 72 | :return: 73 | """ 74 | 75 | allure.dynamic.title("Get list of cars empty username/password") 76 | allure.dynamic.severity(allure.severity_level.BLOCKER) 77 | 78 | username = '' 79 | password = '' 80 | 81 | with allure.step("Send GET request with empty username/password"): 82 | response = requests.get(self.URL + self.cars_url, 83 | auth=(username, 84 | password)) 85 | 86 | with allure.step("Verify status code"): 87 | self.assertEqual(401, 88 | response.status_code) 89 | 90 | with allure.step("Verify error message"): 91 | self.assertEqual("Authenticate with proper credentials", 92 | response.json()["message"]) 93 | 94 | def test_get_list_of_cars_non_admin_wrong_credentials(self): 95 | """ 96 | Get full list of cars using wrong non admin user credentials. 97 | Wrong password + Correct username. 98 | Correct password + Wrong username. 99 | :return: 100 | """ 101 | 102 | allure.dynamic.title("Get list of cars using wrong" 103 | " non admin user credentials") 104 | allure.dynamic.severity(allure.severity_level.BLOCKER) 105 | 106 | with allure.step("Verify user permissions"): 107 | username = USER_LIST[1]['name'] 108 | password = USER_LIST[3]['password'] 109 | self.assertEqual("non_admin", 110 | USER_LIST[1]['perm']) 111 | 112 | with allure.step("Send GET request "): 113 | response = requests.get(self.URL + self.cars_url, 114 | auth=(username, 115 | password)) 116 | 117 | with allure.step("Verify status code"): 118 | self.assertEqual(401, 119 | response.status_code) 120 | 121 | with allure.step("Verify error message"): 122 | self.assertEqual("Authenticate with proper credentials", 123 | response.json()["message"]) 124 | 125 | def test_get_list_of_cars_non_admin_sedan_wrong_credentials(self): 126 | """ 127 | Get full list of cars of type = 'sedan' 128 | using wrong non admin user credentials. 129 | :return: 130 | """ 131 | 132 | allure.dynamic.title("Get list of cars of type = 'sedan' " 133 | "using wrong non admin user credentials") 134 | allure.dynamic.severity(allure.severity_level.BLOCKER) 135 | 136 | with allure.step("Verify user permissions"): 137 | username = USER_LIST[1]['name'] 138 | password = USER_LIST[3]['password'] 139 | self.assertEqual("non_admin", 140 | USER_LIST[1]['perm']) 141 | 142 | with allure.step("Send GET request with wrong credentials"): 143 | response = requests.get(self.URL + 144 | self.cars_url + 145 | '/filter/sedan', 146 | auth=(username, password)) 147 | 148 | with allure.step("Verify status code"): 149 | self.assertEqual(401, 150 | response.status_code) 151 | 152 | with allure.step("Verify error message"): 153 | self.assertEqual("Authenticate with proper credentials", 154 | response.json()["message"]) 155 | 156 | def test_get_list_of_cars_sedan_empty_credentials(self): 157 | """ 158 | Get full list of cars of type = 'sedan' 159 | using empty credentials. 160 | :return: 161 | """ 162 | 163 | allure.dynamic.title("Get list of cars of type = 'sedan' " 164 | "using empty credentials") 165 | allure.dynamic.severity(allure.severity_level.BLOCKER) 166 | 167 | username = '' 168 | password = '' 169 | 170 | with allure.step("Send GET request with wrong credentials"): 171 | response = requests.get(self.URL + 172 | self.cars_url + 173 | '/filter/sedan', 174 | auth=(username, password)) 175 | 176 | with allure.step("Verify status code"): 177 | self.assertEqual(401, 178 | response.status_code) 179 | 180 | with allure.step("Verify error message"): 181 | self.assertEqual("Authenticate with proper credentials", 182 | response.json()["message"]) 183 | 184 | def test_get_list_of_cars_admin_hatchback_wrong_credentials(self): 185 | """ 186 | Get full list of cars from type = 'hatchback' 187 | using wrong admin user credentials. 188 | :return: 189 | """ 190 | 191 | allure.dynamic.title("Get list of cars of type = 'hatchback' " 192 | "using wrong admin user credentials") 193 | allure.dynamic.severity(allure.severity_level.BLOCKER) 194 | 195 | with allure.step("Verify user permissions"): 196 | username = USER_LIST[0]['name'] 197 | password = USER_LIST[1]['password'] 198 | self.assertEqual("admin", 199 | USER_LIST[0]['perm']) 200 | 201 | with allure.step("Send GET request"): 202 | response = requests.get(self.URL + 203 | self.cars_url + 204 | '/filter/hatchback', 205 | auth=(username, password)) 206 | 207 | with allure.step("Verify status code"): 208 | self.assertEqual(401, 209 | response.status_code) 210 | 211 | with allure.step("Verify error message"): 212 | self.assertEqual("Authenticate with proper credentials", 213 | response.json()["message"]) 214 | 215 | def test_get_car_by_name_non_admin_swift_wrong_credentials(self): 216 | """ 217 | Get car data by name = 'swift' 218 | using wrong non admin user credentials. 219 | :return: 220 | """ 221 | 222 | allure.dynamic.title("Get car data by name using " 223 | "wrong non admin user credentials") 224 | allure.dynamic.severity(allure.severity_level.BLOCKER) 225 | 226 | with allure.step("Verify user permissions"): 227 | username = USER_LIST[1]['name'] 228 | password = USER_LIST[3]['password'] 229 | self.assertEqual("non_admin", 230 | USER_LIST[1]['perm']) 231 | 232 | with allure.step("Send GET request"): 233 | response = requests.get(self.URL + 234 | self.cars_url + 235 | '/Swift', 236 | auth=(username, password)) 237 | 238 | with allure.step("Verify status code"): 239 | self.assertEqual(401, 240 | response.status_code) 241 | 242 | with allure.step("Verify error message"): 243 | self.assertEqual("Authenticate with proper credentials", 244 | response.json()["message"]) 245 | 246 | def test_get_car_by_name_swift_empty_credentials(self): 247 | """ 248 | Get car data by name = 'swift' 249 | using empty user credentials. 250 | :return: 251 | """ 252 | 253 | allure.dynamic.title("Get car data by name using " 254 | "empty user credentials") 255 | allure.dynamic.severity(allure.severity_level.BLOCKER) 256 | 257 | username = '' 258 | password = '' 259 | 260 | with allure.step("Send GET request"): 261 | response = requests.get(self.URL + 262 | self.cars_url + 263 | '/Swift', 264 | auth=(username, password)) 265 | 266 | with allure.step("Verify status code"): 267 | self.assertEqual(401, 268 | response.status_code) 269 | 270 | with allure.step("Verify error message"): 271 | self.assertEqual("Authenticate with proper credentials", 272 | response.json()["message"]) 273 | -------------------------------------------------------------------------------- /tests/system/get_cars_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: GET 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import CARS_LIST, USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Positive Tests") 21 | @allure.feature("GET") 22 | @allure.story('Cars') 23 | class GetCarsPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Positive Test: GET call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Arrange expected results (cars list)"): 35 | 36 | self.cars_url = '/cars' 37 | 38 | self.CARS_HATCHBACK = [car for car in CARS_LIST 39 | if car["car_type"] == 'hatchback'] 40 | 41 | self.CARS_SEDAN = [car for car in CARS_LIST 42 | if car["car_type"] == 'sedan'] 43 | 44 | self.CARS_LIST = CARS_LIST 45 | 46 | def test_get_list_of_cars_admin(self): 47 | """ 48 | Get full list of cars using admin user credentials. 49 | :return: 50 | """ 51 | 52 | allure.dynamic.title("Get list of cars " 53 | "using admin user credentials") 54 | allure.dynamic.severity(allure.severity_level.BLOCKER) 55 | 56 | with allure.step("Verify user permissions"): 57 | username = USER_LIST[0]['name'] 58 | password = USER_LIST[0]['password'] 59 | self.assertEqual("admin", 60 | USER_LIST[0]['perm']) 61 | 62 | with allure.step("Send GET request"): 63 | response = requests.get(self.URL + self.cars_url, 64 | auth=(username, 65 | password)) 66 | 67 | with allure.step("Verify status code"): 68 | self.assertEqual(200, 69 | response.status_code) 70 | 71 | with allure.step("Verify 'successful' flag"): 72 | self.assertTrue(response.json()['successful']) 73 | 74 | with allure.step("Verify retrieved cars list"): 75 | self.assertTrue(all(True for car in self.CARS_LIST 76 | if car in response.json()['cars_list'])) 77 | 78 | def test_get_list_of_cars_non_admin(self): 79 | """ 80 | Get full list of cars using non admin user credentials. 81 | :return: 82 | """ 83 | 84 | allure.dynamic.title("Get list of cars " 85 | "using non admin user credentials") 86 | allure.dynamic.severity(allure.severity_level.BLOCKER) 87 | 88 | with allure.step("Verify user permissions"): 89 | username = USER_LIST[1]['name'] 90 | password = USER_LIST[1]['password'] 91 | self.assertEqual("non_admin", 92 | USER_LIST[1]['perm']) 93 | 94 | with allure.step("Send GET request"): 95 | response = requests.get(self.URL + self.cars_url, 96 | auth=(username, 97 | password)) 98 | 99 | with allure.step("Verify status code"): 100 | self.assertEqual(200, 101 | response.status_code) 102 | 103 | with allure.step("Verify 'successful' flag"): 104 | self.assertTrue(response.json()['successful']) 105 | 106 | with allure.step("Verify retrieved cars list"): 107 | self.assertTrue(all(True for car in self.CARS_LIST 108 | if car in response.json()['cars_list'])) 109 | 110 | def test_get_list_of_cars_non_admin_sedan(self): 111 | """ 112 | Get full list of cars of type = 'sedan' 113 | using non admin user credentials. 114 | :return: 115 | """ 116 | 117 | allure.dynamic.title("Get list of cars of type = 'sedan' " 118 | "using non admin user credentials") 119 | allure.dynamic.severity(allure.severity_level.BLOCKER) 120 | 121 | with allure.step("Verify user permissions"): 122 | username = USER_LIST[1]['name'] 123 | password = USER_LIST[1]['password'] 124 | self.assertEqual("non_admin", 125 | USER_LIST[1]['perm']) 126 | 127 | with allure.step("Send GET request"): 128 | response = requests.get(self.URL + 129 | self.cars_url + 130 | '/filter/sedan', 131 | auth=(username, password)) 132 | 133 | with allure.step("Verify status code"): 134 | self.assertEqual(200, 135 | response.status_code) 136 | 137 | with allure.step("Verify retrieved cars list of type sedan"): 138 | self.assertTrue(all(True for car in self.CARS_SEDAN 139 | if car in response.json()['cars'])) 140 | 141 | def test_get_list_of_cars_admin_hatchback(self): 142 | """ 143 | Get full list of cars from type = 'hatchback' 144 | using admin user credentials. 145 | :return: 146 | """ 147 | 148 | allure.dynamic.title("Get list of cars of type = 'hatchback' " 149 | "using admin user credentials") 150 | allure.dynamic.severity(allure.severity_level.BLOCKER) 151 | 152 | with allure.step("Verify user permissions"): 153 | username = USER_LIST[0]['name'] 154 | password = USER_LIST[0]['password'] 155 | self.assertEqual("admin", 156 | USER_LIST[0]['perm']) 157 | 158 | with allure.step("Send GET request"): 159 | response = requests.get(self.URL + 160 | self.cars_url + 161 | '/filter/hatchback', 162 | auth=(username, password)) 163 | 164 | with allure.step("Verify status code"): 165 | self.assertEqual(200, 166 | response.status_code) 167 | 168 | with allure.step("Verify retrieved cars list of type hatchback"): 169 | self.assertTrue(all(True for car in self.CARS_HATCHBACK 170 | if car in response.json()['cars'])) 171 | 172 | def test_get_car_by_name_non_admin_swift(self): 173 | """ 174 | Get car data by name = 'swift' 175 | using non admin user credentials. 176 | :return: 177 | """ 178 | 179 | allure.dynamic.title("Get car data by name using " 180 | "non admin user credentials") 181 | allure.dynamic.severity(allure.severity_level.BLOCKER) 182 | 183 | with allure.step("Verify user permissions"): 184 | username = USER_LIST[1]['name'] 185 | password = USER_LIST[1]['password'] 186 | self.assertEqual("non_admin", 187 | USER_LIST[1]['perm']) 188 | 189 | with allure.step("Prepare expected results"): 190 | car = {"brand": "Maruti", 191 | "car_type": "hatchback", 192 | "name": "Swift", 193 | "price_range": "3-5 lacs"} 194 | 195 | with allure.step("Send GET request"): 196 | response = requests.get(self.URL + 197 | self.cars_url + 198 | '/Swift', 199 | auth=(username, password)) 200 | 201 | with allure.step("Verify status code"): 202 | self.assertEqual(200, 203 | response.status_code) 204 | 205 | with allure.step("Verify 'successful' flag"): 206 | self.assertTrue(response.json()['successful']) 207 | 208 | with allure.step("Verify retrieved car"): 209 | self.assertTrue(car == response.json()['car']) 210 | -------------------------------------------------------------------------------- /tests/system/post_cars_negative_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: POST 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Negative Tests") 21 | @allure.feature("POST") 22 | @allure.story('Cars') 23 | class PostCarsPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Negative Test: POST call 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Prepare test data"): 35 | 36 | self.cars_url = '/cars' 37 | 38 | self.message = '' 39 | 40 | self.new_car = {'name': 'Figo', 41 | 'brand': 'Ford', 42 | 'price_range': '2-3 lacs', 43 | 'car_type': 'hatchback'} 44 | 45 | def test_post_car_wrong_admin_credentials(self): 46 | """ 47 | Add new car using wrong admin user credentials. 48 | :return: 49 | """ 50 | 51 | allure.dynamic.title("Add new car using wrong " 52 | "admin user credentials") 53 | allure.dynamic.severity(allure.severity_level.BLOCKER) 54 | 55 | with allure.step("Verify user permissions"): 56 | username = USER_LIST[0]['name'] 57 | password = USER_LIST[1]['password'] 58 | self.assertEqual("admin", 59 | USER_LIST[0]['perm']) 60 | 61 | with allure.step("Send POST request"): 62 | response = requests.post(self.URL + 63 | self.cars_url + 64 | '/add', 65 | json=self.new_car, 66 | auth=(username, 67 | password)) 68 | 69 | with allure.step("Verify status code"): 70 | self.assertEqual(401, 71 | response.status_code) 72 | 73 | with allure.step("Verify error message"): 74 | self.assertEqual("Authenticate with proper credentials", 75 | response.json()["message"]) 76 | 77 | def test_post_car_wrong_non_admin_credentials(self): 78 | """ 79 | Add new car using wrong non admin user credentials. 80 | :return: 81 | """ 82 | 83 | allure.dynamic.title("Add new car using wrong " 84 | "non admin user credentials") 85 | allure.dynamic.severity(allure.severity_level.BLOCKER) 86 | 87 | with allure.step("Verify user permissions"): 88 | username = USER_LIST[1]['name'] 89 | password = USER_LIST[3]['password'] 90 | self.assertEqual("non_admin", 91 | USER_LIST[1]['perm']) 92 | 93 | with allure.step("Send POST request"): 94 | response = requests.post(self.URL + 95 | self.cars_url + 96 | '/add', 97 | json=self.new_car, 98 | auth=(username, 99 | password)) 100 | 101 | with allure.step("Verify status code"): 102 | self.assertEqual(401, 103 | response.status_code) 104 | 105 | with allure.step("Verify error message"): 106 | self.assertEqual("Authenticate with proper credentials", 107 | response.json()["message"]) 108 | -------------------------------------------------------------------------------- /tests/system/post_cars_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: POST 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Positive Tests") 21 | @allure.feature("POST") 22 | @allure.story('Cars') 23 | class PostCarsPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Positive Test: POST call 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Prepare test data"): 35 | 36 | self.cars_url = '/cars' 37 | 38 | self.message = '' 39 | 40 | self.new_car = {'name': 'Figo', 41 | 'brand': 'Ford', 42 | 'price_range': '2-3 lacs', 43 | 'car_type': 'hatchback'} 44 | 45 | def tearDown(self) -> None: 46 | """ 47 | Post test procedure 48 | :return: 49 | """ 50 | 51 | with allure.step("Remove new added car from the list"): 52 | username = USER_LIST[0]['name'] 53 | password = USER_LIST[0]['password'] 54 | 55 | requests.delete(url=self.URL + 56 | self.cars_url + 57 | '/remove/' + 58 | self.new_car['name'], 59 | auth=(username, 60 | password)) 61 | 62 | def test_post_car_admin(self): 63 | """ 64 | Add new car using admin user credentials. 65 | :return: 66 | """ 67 | 68 | allure.dynamic.title("Add new car " 69 | "using admin user credentials") 70 | allure.dynamic.severity(allure.severity_level.BLOCKER) 71 | 72 | with allure.step("Verify user permissions"): 73 | username = USER_LIST[0]['name'] 74 | password = USER_LIST[0]['password'] 75 | self.assertEqual("admin", 76 | USER_LIST[0]['perm']) 77 | 78 | with allure.step("Send POST request"): 79 | response = requests.post(self.URL + 80 | self.cars_url + 81 | '/add', 82 | json=self.new_car, 83 | auth=(username, 84 | password)) 85 | 86 | with allure.step("Verify status code"): 87 | self.assertEqual(200, 88 | response.status_code) 89 | 90 | with allure.step("Verify 'successful' flag"): 91 | self.assertTrue(response.json()['successful']) 92 | 93 | with allure.step("Verify retrieved cars list"): 94 | self.assertDictEqual(self.new_car, 95 | response.json()['car']) 96 | 97 | def test_post_car_non_admin(self): 98 | """ 99 | Add new car using non admin user credentials. 100 | :return: 101 | """ 102 | 103 | allure.dynamic.title("Add new car " 104 | "using non admin user credentials") 105 | allure.dynamic.severity(allure.severity_level.BLOCKER) 106 | 107 | with allure.step("Verify user permissions"): 108 | username = USER_LIST[1]['name'] 109 | password = USER_LIST[1]['password'] 110 | self.assertEqual("non_admin", 111 | USER_LIST[1]['perm']) 112 | 113 | with allure.step("Send POST request"): 114 | response = requests.post(self.URL + 115 | self.cars_url + 116 | '/add', 117 | json=self.new_car, 118 | auth=(username, 119 | password)) 120 | 121 | with allure.step("Verify status code"): 122 | self.assertEqual(200, 123 | response.status_code) 124 | 125 | with allure.step("Verify 'successful' flag"): 126 | self.assertTrue(response.json()['successful']) 127 | 128 | with allure.step("Verify retrieved cars list"): 129 | self.assertDictEqual(self.new_car, 130 | response.json()['car']) 131 | -------------------------------------------------------------------------------- /tests/system/post_register_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: POST 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Positive Tests") 21 | @allure.feature("POST") 22 | @allure.story('Car Registration') 23 | class PostCarRegistrationPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Positive Test: POST call 26 | """ 27 | 28 | def setUp(self) -> None: 29 | """ 30 | Test data preparation 31 | :return: 32 | """ 33 | 34 | with allure.step("Prepare test data"): 35 | self.register_url = '/register/car' 36 | 37 | self.customer = {'customer_name': 'Unai Emery', 38 | 'city': 'London'} 39 | 40 | self.new_car = {"car_name": "Creta", 41 | "brand": "Hyundai"} 42 | 43 | def tearDown(self) -> None: 44 | """ 45 | Post test procedure 46 | :return: 47 | """ 48 | 49 | with allure.step("Remove new added car from the list"): 50 | pass 51 | 52 | def test_register_car_admin(self): 53 | """ 54 | Register car using admin user credentials. 55 | :return: 56 | """ 57 | 58 | allure.dynamic.title("Register car " 59 | "using admin user credentials") 60 | allure.dynamic.severity(allure.severity_level.BLOCKER) 61 | 62 | with allure.step("Verify user permissions"): 63 | username = USER_LIST[0]['name'] 64 | password = USER_LIST[0]['password'] 65 | self.assertEqual("admin", 66 | USER_LIST[0]['perm']) 67 | 68 | with allure.step("Send POST request"): 69 | response = requests.post(url=self.URL + 70 | self.register_url, 71 | params=self.new_car, 72 | json=self.customer, 73 | auth=(username, 74 | password)) 75 | 76 | with allure.step("Verify status code"): 77 | self.assertEqual(200, 78 | response.status_code) 79 | 80 | with allure.step("Verify 'successful' flag"): 81 | self.assertTrue(response.json()['registered_car']['successful']) 82 | 83 | with allure.step("Verify retrieved car details"): 84 | self.assertDictEqual({ 85 | "name": "Creta", 86 | "brand": "Hyundai", 87 | "price_range": "8-14 lacs", 88 | "car_type": "hatchback" 89 | }, response.json()['registered_car']['car']) 90 | 91 | with allure.step("Verify retrieved customer details"): 92 | self.assertDictEqual(self.customer, 93 | response.json()['registered_car']['customer_details']) 94 | 95 | def test_register_car_non_admin(self): 96 | """ 97 | Register car using non admin user credentials. 98 | :return: 99 | """ 100 | 101 | allure.dynamic.title("Register car " 102 | "using non admin user credentials") 103 | allure.dynamic.severity(allure.severity_level.BLOCKER) 104 | 105 | with allure.step("Verify user permissions"): 106 | username = USER_LIST[1]['name'] 107 | password = USER_LIST[1]['password'] 108 | self.assertEqual("non_admin", 109 | USER_LIST[1]['perm']) 110 | 111 | with allure.step("Send POST request"): 112 | response = requests.post(url=self.URL + 113 | self.register_url, 114 | params=self.new_car, 115 | json=self.customer, 116 | auth=(username, 117 | password)) 118 | 119 | with allure.step("Verify status code"): 120 | self.assertEqual(200, 121 | response.status_code) 122 | 123 | with allure.step("Verify 'successful' flag"): 124 | self.assertTrue(response.json()['registered_car']['successful']) 125 | 126 | with allure.step("Verify retrieved car details"): 127 | self.assertDictEqual({ 128 | "name": "Creta", 129 | "brand": "Hyundai", 130 | "price_range": "8-14 lacs", 131 | "car_type": "hatchback" 132 | }, response.json()['registered_car']['car']) 133 | 134 | with allure.step("Verify retrieved customer details"): 135 | self.assertDictEqual(self.customer, 136 | response.json()['registered_car']['customer_details']) 137 | -------------------------------------------------------------------------------- /tests/system/put_cars_negaive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: PUT 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Negative Tests") 21 | @allure.feature("PUT") 22 | @allure.story('Cars') 23 | class PutCarNegativeTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Negative Test: PUT call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | 30 | self.put_url = '/cars/update/Vento' 31 | 32 | self.original = {"name": "Vento", "brand": "Volkswagen", 33 | "price_range": "7-10 lacs", "car_type": "sedan"} 34 | 35 | self.updated1 = {'name': 'Vento', 'brand': 'Ford', 36 | 'price_range': '2-3 lacs', 'car_type': 'sedan'} 37 | 38 | self.updated2 = {'name': 'Vento', 'brand': 'Ford', 39 | 'price_range': '3-5 lacs', 'car_type': 'convertible'} 40 | 41 | def test_put_update_car_admin_wrong_credentials(self): 42 | """ 43 | Update car properties using wrong admin user credentials. 44 | :return: 45 | """ 46 | 47 | allure.dynamic.title("Update car using wrong admin user credentials") 48 | allure.dynamic.severity(allure.severity_level.BLOCKER) 49 | 50 | with allure.step("Verify user permissions"): 51 | username = USER_LIST[0]['name'] 52 | password = USER_LIST[1]['password'] 53 | self.assertEqual("admin", 54 | USER_LIST[0]['perm']) 55 | 56 | with allure.step("Send PUT request"): 57 | response = requests.put(self.URL + self.put_url, 58 | json=self.updated1, 59 | auth=(username, 60 | password)) 61 | 62 | with allure.step("Verify status code"): 63 | self.assertEqual(401, 64 | response.status_code) 65 | 66 | with allure.step("Verify error message"): 67 | self.assertEqual("Authenticate with proper credentials", 68 | response.json()["message"]) 69 | 70 | def test_put_update_car_non_admin_wrong_credentials(self): 71 | """ 72 | Update car properties using wrong non admin user credentials. 73 | :return: 74 | """ 75 | 76 | allure.dynamic.title("Update car using wrong non admin user credentials") 77 | allure.dynamic.severity(allure.severity_level.BLOCKER) 78 | 79 | with allure.step("Verify user permissions"): 80 | username = USER_LIST[1]['name'] 81 | password = USER_LIST[3]['password'] 82 | self.assertEqual("non_admin", 83 | USER_LIST[1]['perm']) 84 | 85 | with allure.step("Send PUT request"): 86 | response = requests.put(self.URL + self.put_url, 87 | json=self.updated2, 88 | auth=(username, 89 | password)) 90 | 91 | with allure.step("Verify status code"): 92 | self.assertEqual(401, 93 | response.status_code) 94 | 95 | with allure.step("Verify error message"): 96 | self.assertEqual("Authenticate with proper credentials", 97 | response.json()["message"]) 98 | -------------------------------------------------------------------------------- /tests/system/put_cars_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: PUT 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import requests 13 | from tests.system.base_test import BaseTestCase 14 | from api.cars_app import USER_LIST 15 | 16 | 17 | @allure.epic('Simple Flask App') 18 | @allure.parent_suite('REST API') 19 | @allure.suite("System Tests") 20 | @allure.sub_suite("Positive Tests") 21 | @allure.feature("PUT") 22 | @allure.story('Cars') 23 | class PutCarPositiveTestCase(BaseTestCase): 24 | """ 25 | Simple Flask App Positive Test: PUT call > cars 26 | """ 27 | 28 | def setUp(self) -> None: 29 | 30 | self.put_url = '/cars/update/Vento' 31 | 32 | self.original = {"name": "Vento", "brand": "Volkswagen", 33 | "price_range": "7-10 lacs", "car_type": "sedan"} 34 | 35 | self.updated1 = {'name': 'Vento', 'brand': 'Ford', 36 | 'price_range': '2-3 lacs', 'car_type': 'sedan'} 37 | 38 | self.updated2 = {'name': 'Vento', 'brand': 'Ford', 39 | 'price_range': '3-5 lacs', 'car_type': 'convertible'} 40 | 41 | def tearDown(self) -> None: 42 | 43 | username = USER_LIST[0]['name'] 44 | password = USER_LIST[0]['password'] 45 | 46 | with allure.step("Restore original cars list"): 47 | requests.put(self.URL + self.put_url, 48 | json=self.original, 49 | auth=(username, 50 | password)) 51 | 52 | def test_put_update_car_admin(self): 53 | """ 54 | Update car properties using admin user credentials. 55 | :return: 56 | """ 57 | 58 | allure.dynamic.title("Update car using admin user credentials") 59 | allure.dynamic.severity(allure.severity_level.BLOCKER) 60 | 61 | with allure.step("Verify user permissions"): 62 | username = USER_LIST[0]['name'] 63 | password = USER_LIST[0]['password'] 64 | self.assertEqual("admin", 65 | USER_LIST[0]['perm']) 66 | 67 | with allure.step("Send PUT request"): 68 | response = requests.put(self.URL + self.put_url, 69 | json=self.updated1, 70 | auth=(username, 71 | password)) 72 | 73 | with allure.step("Verify status code"): 74 | self.assertEqual(200, 75 | response.status_code) 76 | 77 | with allure.step("Verify 'successful' flag"): 78 | self.assertTrue(response.json()["response"]['successful']) 79 | 80 | with allure.step("Verify retrieved car data"): 81 | self.assertDictEqual(self.updated1, 82 | response.json()["response"]['car']) 83 | 84 | def test_put_update_car_non_admin(self): 85 | """ 86 | Update car properties using non admin user credentials. 87 | :return: 88 | """ 89 | 90 | allure.dynamic.title("Update car using non admin user credentials") 91 | allure.dynamic.severity(allure.severity_level.BLOCKER) 92 | 93 | with allure.step("Verify user permissions"): 94 | username = USER_LIST[1]['name'] 95 | password = USER_LIST[1]['password'] 96 | self.assertEqual("non_admin", 97 | USER_LIST[1]['perm']) 98 | 99 | with allure.step("Send PUT request"): 100 | response = requests.put(self.URL + self.put_url, 101 | json=self.updated2, 102 | auth=(username, 103 | password)) 104 | 105 | with allure.step("Verify status code"): 106 | self.assertEqual(200, 107 | response.status_code) 108 | 109 | with allure.step("Verify 'successful' flag"): 110 | self.assertTrue(response.json()["response"]['successful']) 111 | 112 | with allure.step("Verify retrieved car data"): 113 | self.assertDictEqual(self.updated2, 114 | response.json()["response"]['car']) 115 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/internal_func_negative_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: Unit Tests 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import unittest 13 | 14 | from api.authentication_helper import AuthenticationHelper 15 | from data.users import Users 16 | 17 | 18 | @allure.epic('Simple Flask App') 19 | @allure.parent_suite('REST API') 20 | @allure.suite("Unit Tests") 21 | @allure.sub_suite("Negative Tests") 22 | @allure.feature("Internal Functions") 23 | @allure.story('Flags and Args Validation') 24 | class InternalFuncNegativeTestCase(unittest.TestCase): 25 | 26 | @classmethod 27 | def setUpClass(cls) -> None: 28 | with allure.step("Arrange test data"): 29 | cls.admin_user = {"name": "qxf2", 30 | "password": "123", 31 | "perm": "admin"} 32 | 33 | cls.non_admin_user = {"name": "jack", 34 | "password": "123", 35 | "perm": "non_admin"} 36 | 37 | def test_check_auth_wrong_admin(self): 38 | with allure.step("Verify check_auth flag using admin user"): 39 | self.assertFalse(AuthenticationHelper.check_auth(username=self.admin_user["name"], 40 | password=self.admin_user["password"], 41 | user_list=Users().get_users())) 42 | 43 | def test_check_auth_wrong_non_admin(self): 44 | with allure.step("Verify check_auth flag using non admin user"): 45 | self.assertFalse(AuthenticationHelper.check_auth(username=self.non_admin_user["name"], 46 | password=self.non_admin_user["password"], 47 | user_list=Users().get_users())) 48 | -------------------------------------------------------------------------------- /tests/unit/internal_func_positive_test.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """ 4 | Flask App REST API testing: Unit Tests 5 | """ 6 | 7 | # Created by Egor Kostan. 8 | # GitHub: https://github.com/ikostan 9 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 10 | 11 | import allure 12 | import unittest 13 | 14 | from api.authentication_helper import AuthenticationHelper 15 | from api.cars_app import app 16 | from data.users import Users 17 | 18 | 19 | @allure.epic('Simple Flask App') 20 | @allure.parent_suite('REST API') 21 | @allure.suite("Unit Tests") 22 | @allure.sub_suite("Positive Tests") 23 | @allure.feature("Internal Functions") 24 | @allure.story('Flags and Args Validation') 25 | class InternalFuncPositiveTestCase(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls) -> None: 29 | with allure.step("Arrange test data"): 30 | cls.admin_user = {"name": "qxf2", 31 | "password": "qxf2", 32 | "perm": "admin"} 33 | 34 | cls.non_admin_user = {"name": "jack", 35 | "password": "qxf2", 36 | "perm": "non_admin"} 37 | 38 | def test_debug_mode(self): 39 | """ 40 | Whether debug mode is enabled. 41 | 42 | When using flask run to start the development server, 43 | an interactive debugger will be shown for unhandled exceptions, 44 | and the server will be reloaded when code changes. 45 | The debug attribute maps to this config key. This is enabled 46 | when ENV is 'development' and is overridden by the FLASK_DEBUG 47 | environment variable. It may not behave as expected if set in code. 48 | 49 | Do not enable debug mode when deploying in production. 50 | 51 | Default: True if ENV is 'development', or False otherwise. 52 | :return: 53 | """ 54 | 55 | allure.dynamic.title("API flags validation") 56 | allure.dynamic.severity(allure.severity_level.CRITICAL) 57 | 58 | with allure.step("Verify DEBUG flag"): 59 | self.assertFalse(app.config['DEBUG']) 60 | 61 | def test_check_auth_admin(self): 62 | with allure.step("Verify check_auth flag using admin user"): 63 | self.assertTrue(AuthenticationHelper.check_auth(username=self.admin_user["name"], 64 | password=self.admin_user["password"], 65 | user_list=Users().get_users())) 66 | 67 | def test_check_auth_non_admin(self): 68 | with allure.step("Verify check_auth flag using non admin user"): 69 | self.assertTrue(AuthenticationHelper.check_auth(username=self.non_admin_user["name"], 70 | password=self.non_admin_user["password"], 71 | user_list=Users().get_users())) 72 | 73 | def test_authenticate_error_flag_true(self): 74 | with app.app_context(): 75 | with allure.step("Arrange authenticate_error response"): 76 | response = AuthenticationHelper.authenticate_error(auth_flag=True) 77 | 78 | with allure.step("Verify status code"): 79 | self.assertEqual(401, 80 | response.status_code) 81 | 82 | with allure.step("Verify header"): 83 | self.assertEqual('Basic realm="Example"', 84 | response.headers['WWW-Authenticate']) 85 | 86 | def test_authenticate_error_flag_false(self): 87 | with app.app_context(): 88 | with allure.step("Arrange authenticate_error response"): 89 | response = AuthenticationHelper.authenticate_error(auth_flag=False) 90 | 91 | with allure.step("Verify status code"): 92 | self.assertEqual(401, 93 | response.status_code) 94 | 95 | with allure.step("Verify header"): 96 | self.assertEqual('Basic realm="Example"', 97 | response.headers['WWW-Authenticate']) 98 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikostan/REST_API_AUTOMATION/cdb4d30fbc7457b2a403b4dad6fe1efa2e754681/utils/__init__.py -------------------------------------------------------------------------------- /utils/get_args_from_cli.py: -------------------------------------------------------------------------------- 1 | #!/path/to/interpreter 2 | 3 | """Extract arguments from CLI""" 4 | 5 | # Created by Egor Kostan. 6 | # GitHub: https://github.com/ikostan 7 | # LinkedIn: https://www.linkedin.com/in/egor-kostan/ 8 | 9 | import sys 10 | 11 | 12 | def get_args(): 13 | """ 14 | Reads arguments from CLI and returns them as dictionary. 15 | Helps running Python unittest with command-line arguments. 16 | Raises Exception if one of the required arguments is missing. 17 | :return: 18 | """ 19 | 20 | env = None 21 | 22 | for param in sys.argv: 23 | 24 | # Test environment (mainly for web testing) 25 | if '--env=' in param: 26 | env = str(param).replace('--env=', '') 27 | print("\n--env={}".format(env)) 28 | 29 | return {'env': env} 30 | --------------------------------------------------------------------------------