├── UI ├── __init__.py ├── requirements.txt ├── .gitignore ├── README.md ├── pytest.ini ├── pages │ ├── RegistrationPage.py │ ├── LoginPage.py │ ├── ProfilePage.py │ ├── FavoritesPage.py │ ├── CartPage.py │ └── EditProfilePage.py ├── test_change_email.py ├── test_dropdawn_not_present.py ├── test_change_last_name.py ├── test_change_first_name.py ├── test_change_email_negative.py ├── conftest.py ├── test_change_last_name_negative.py ├── test_change_first_name_negative.py ├── set_of_steps.py └── test_review_negative.py ├── data ├── __init__.py ├── images │ └── filelate.jpeg ├── incorrect_format_email.json ├── error_message_token.py ├── data_for_cart.py ├── data_format_token.py └── text_reviews_for_product.py ├── tests ├── __init__.py ├── cart │ ├── __init__.py │ ├── test_get_shopping_cart.py │ ├── test_add_item_to_cart_negative.py │ ├── test_delete_items_from_cart.py │ ├── test_add_item_to_cart_positive.py │ ├── test_update_item_in_shopping_cart.py │ └── test_update_item_in_cart_negative.py ├── user │ ├── __init__.py │ ├── test_delete_user.py │ └── test_change_password.py ├── favorite │ ├── __init__.py │ ├── test_delete_product_from_favorite.py │ ├── test_add_product_to_favorite.py │ ├── test_add_product_to_favorite_negative.py │ └── test_get_favorite_products_list.py ├── product │ ├── __init__.py │ ├── test_product.py │ └── test_all_products.py ├── review │ ├── __init__.py │ ├── conftest.py │ └── test_delete_review.py ├── authentication │ ├── __init__.py │ ├── test_logout.py │ ├── test_authentication.py │ ├── test_authentication_blank_field.py │ ├── test_authentication_invalid_credential.py │ ├── test_authentication_limit.py │ ├── test_refresh_token.py │ └── test_fogot_password.py └── registration │ ├── __init__.py │ ├── test_confirm_email_negative.py │ ├── test_registration_with_email.py │ └── test_registration_negative.py ├── framework ├── steps │ ├── __init__.py │ └── registration_steps.py ├── tools │ ├── __init__.py │ ├── matcher.py │ ├── favorite_methods.py │ ├── load_file.py │ ├── logging_allure.py │ ├── class_email.py │ ├── methods_to_cart.py │ └── generators.py ├── asserts │ ├── __init__.py │ ├── product_asserts.py │ ├── registration_asserts.py │ ├── assert_cart.py │ ├── user_asserts.py │ ├── assert_favorite.py │ └── common.py ├── clients │ ├── __init__.py │ ├── db_client.py │ └── db_client_ssh.py ├── endpoints │ ├── __init__.py │ ├── product_api.py │ ├── favorite_api.py │ ├── cart_api.py │ ├── users_api.py │ ├── authenticate_api.py │ └── review_api.py └── queries │ ├── __init__.py │ └── postgres_db.py ├── db_navigator.png ├── db_navigator_setup.png ├── .vscode └── settings.json ├── pytest.ini ├── .pre-commit-config.yaml ├── start_be.sh ├── LICENSE ├── configs.py ├── requirements.txt ├── .gitignore ├── docker-compose.local.yml └── README.md /UI/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cart/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/steps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/favorite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/product/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/review/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/asserts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /framework/queries/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/registration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db_navigator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/Iced-Latte-QA/HEAD/db_navigator.png -------------------------------------------------------------------------------- /UI/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/Iced-Latte-QA/HEAD/UI/requirements.txt -------------------------------------------------------------------------------- /db_navigator_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/Iced-Latte-QA/HEAD/db_navigator_setup.png -------------------------------------------------------------------------------- /data/images/filelate.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/Iced-Latte-QA/HEAD/data/images/filelate.jpeg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.autoImportCompletions": true, 3 | "python.analysis.typeCheckingMode": "basic" 4 | } -------------------------------------------------------------------------------- /UI/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | # !!! 3 | venv_activate.bat 4 | .idea/ 5 | 6 | # Allure 7 | allure_report/ 8 | 9 | # Config 10 | # !!! 11 | # configs.py 12 | 13 | __pycache__/ 14 | tests/__pycache__/ 15 | pages/__pycache__/ 16 | data_for_auth.py -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --alluredir=allure_report 4 | --allure-no-capture 5 | --clean-alluredir 6 | --reruns 2 7 | --reruns-delay 5 8 | 9 | markers = 10 | critical: 11 | high: 12 | medium: 13 | low: 14 | -------------------------------------------------------------------------------- /UI/README.md: -------------------------------------------------------------------------------- 1 | # Iced-Latte-Test-UI 2 | 3 | ## Description 4 | 5 | UI automated tests 6 | 7 | ## Requirements 8 | 9 | requirements.txt 10 | 11 | ## Run 12 | 13 | `pytest` 14 | 15 | ## Run options 16 | 17 | pytest.ini 18 | 19 | ## Allure Report 20 | 21 | `allure serve allure_report` -------------------------------------------------------------------------------- /UI/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | ; markers = 3 | ; login_guest: marker for guest login 4 | ; need_review: marker for need review tests 5 | addopts = 6 | -v 7 | -rx 8 | ; --tb=line 9 | --alluredir=allure_report 10 | --allure-no-capture 11 | --clean-alluredir 12 | -s -------------------------------------------------------------------------------- /data/incorrect_format_email.json: -------------------------------------------------------------------------------- 1 | [ 2 | "example.domain.com", 3 | "usernamedomain.com", 4 | "username@", 5 | "john doe@example.com", 6 | "user!name@example.com", 7 | "@domain.com", 8 | "username@example", 9 | "user@name@example.com", 10 | ".username@example.com", 11 | "username@example_domain.com" 12 | ] 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.10.0 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /data/error_message_token.py: -------------------------------------------------------------------------------- 1 | # error message for token 2 | 3 | token_is_absent = "Bearer authentication header is absent" 4 | token_is_invalid = "Invalid token" 5 | token_is_blacklisted = "JWT Token is blacklisted" 6 | token_is_expired = "Jwt token is expired" 7 | token_containing_not_correct_user_email = ( 8 | "Jwt token does not contain correct user email" 9 | ) 10 | token_not_containing_email = "Jwt token does not contain email" 11 | -------------------------------------------------------------------------------- /data/data_for_cart.py: -------------------------------------------------------------------------------- 1 | data_for_not_exist_shopping_cart_item_id = { 2 | "shoppingCartItemId": "ec39b8c6-ba83-4f0a-8332-0769be35d5f9", 3 | "productQuantityChange": 1, 4 | } 5 | data_for_adding_product_to_cart = [ 6 | {"productId": "ad0ef2b7-816b-4a11-b361-dfcbe705fc96", "productQuantity": 2}, 7 | {"productId": "3ea8e601-24c9-49b1-8c65-8db8b3a5c7a3", "productQuantity": 3}, 8 | ] 9 | 10 | id_product_not_exist_in_BD_for_adding_to_cart = [ 11 | {"productId": "fc98cd99-5049-4b00-8d08-df1d994a5ce1", "productQuantity": 1} 12 | ] 13 | -------------------------------------------------------------------------------- /framework/tools/matcher.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def is_timestamp_valid(timestamp: str, pattern: str) -> bool: 5 | """ 6 | Function check if a given timestamp string matches a specified regular expression pattern. 7 | 8 | Args: 9 | timestamp: The timestamp string to be validated. This should be a string representing a date and/or time. 10 | pattern: The regular expression pattern against which the timestamp is to be validated. The pattern should 11 | be provided as a string. 12 | """ 13 | return re.fullmatch(pattern, timestamp) is not None 14 | -------------------------------------------------------------------------------- /data/data_format_token.py: -------------------------------------------------------------------------------- 1 | # data for format token = code, expected_status_code, expected_message_part 2 | incorrect_format_token = [ 3 | (" ", 400, "ErrorMessage: Token cannot be empty"), 4 | ( 5 | "testtest", 6 | 400, 7 | "Incorrect token format, token must be #########", 8 | ), 9 | ("1234567890", 400, "Incorrect token format, token must be #########"), 10 | ("1234567", 400, "Incorrect token format, token must be #########"), 11 | ("12345678", 400, "Incorrect token format, token must be #########"), 12 | ("$*@!_+_*&%", 400, "Incorrect token format, token must be #########"), 13 | ("356-234-123", 400, "Incorrect token format, token must be #########"), 14 | ] 15 | -------------------------------------------------------------------------------- /framework/asserts/product_asserts.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_ 2 | 3 | 4 | def check_mapping_db_to_api(reference: dict, compared: dict) -> None: 5 | """Checking the mapping of data from database in the request API 6 | 7 | Args: 8 | reference: reference data, usually data from a database; 9 | compared: compared data, data from other source. 10 | """ 11 | assert_that( 12 | float(reference["price"]), 13 | is_(float(compared["price"])), 14 | reason=f'"price" not equal expected', 15 | ) 16 | for key in ["id", "name", "description", "quantity"]: 17 | assert_that( 18 | reference[key], is_(compared[key]), reason=f'"{key}" not equal expected' 19 | ) 20 | -------------------------------------------------------------------------------- /framework/steps/registration_steps.py: -------------------------------------------------------------------------------- 1 | class RegistrationSteps: 2 | @staticmethod 3 | def data_for_sent( 4 | email: str = None, 5 | first_name: str = None, 6 | last_name: str = None, 7 | password: str = None, 8 | ) -> dict: 9 | """Registration data to be sent via the REST API 10 | 11 | Args: 12 | email: electronic mail; 13 | first_name: name of the user; 14 | last_name: surname of the user; 15 | password: password for electronic mail. 16 | """ 17 | data = {} 18 | if email: 19 | data["email"] = email 20 | if first_name: 21 | data["firstName"] = first_name 22 | if last_name: 23 | data["lastName"] = last_name 24 | if password: 25 | data["password"] = password 26 | 27 | return data 28 | -------------------------------------------------------------------------------- /start_be.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | be_hash=$1 4 | if [[ -z "$be_hash" ]] 5 | then 6 | github_tag=development-$(git ls-remote https://github.com/Sunagatov/Iced-Latte.git development | head -c7) 7 | dockerhub_tag=$(curl -s \ 8 | 'https://hub.docker.com/v2/repositories/zufarexplainedit/iced-latte-backend/tags?page_size=1' | \ 9 | python3 -c "import sys, json; print(json.load(sys.stdin)['results'][0]['name'])") 10 | if [[ "$github_tag" != "$dockerhub_tag" ]]; then 11 | echo 'WARN: difference between GitHub and DockerHub commits. Most likely BE deploy job failed.' 12 | fi 13 | echo "Using the latest DockerHub tag $dockerhub_tag" 14 | tag=$dockerhub_tag 15 | else 16 | echo "Using specified DockerHub tag $be_hash" 17 | tag=$be_hash 18 | fi 19 | 20 | export DOCKER_IMAGE_TAG=${tag} 21 | # remove all QA containers and all volumes 22 | docker-compose -f docker-compose.local.yml down -v 23 | # start QA containers 24 | docker-compose -f docker-compose.local.yml up -d --build 25 | -------------------------------------------------------------------------------- /framework/tools/favorite_methods.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from requests import Response 4 | 5 | 6 | def extract_random_product_ids(response: Response, product_quantity: int) -> list: 7 | """Extract random product IDs from the API response to add to favorites. 8 | 9 | Selects a random sample of products from the response based on the given quantity. 10 | Returns a list of the ID values for those randomly selected products. 11 | 12 | Args: 13 | response: Response data from API request containing products 14 | product_quantity: Number of random products to select 15 | 16 | Raises: 17 | ValueError: If requested quantity exceeds products available 18 | """ 19 | products = response.json()["products"] 20 | if product_quantity > len(products): 21 | raise ValueError("Requested amount exceeds the number of available products") 22 | 23 | selected_products = random.sample(products, product_quantity) 24 | 25 | return [product["id"] for product in selected_products] 26 | -------------------------------------------------------------------------------- /data/text_reviews_for_product.py: -------------------------------------------------------------------------------- 1 | from data.text_review import ( 2 | text_review_750_char, 3 | text_review_1500_char, 4 | text_review_1499_char, 5 | text_review_with_digits, 6 | text_review_with_extended_latin_letters, 7 | text_review_with_allowed_symbols, 8 | text_review_with_emojy, 9 | text_review_1_char, 10 | text_review_2_char, 11 | ) 12 | 13 | reviews = [ 14 | {"text_review": text_review_750_char, "rating": 5}, 15 | {"text_review": text_review_1500_char, "rating": 4}, 16 | {"text_review": text_review_1499_char, "rating": 3}, 17 | {"text_review": text_review_2_char, "rating": 2}, 18 | {"text_review": text_review_1_char, "rating": 1}, 19 | {"text_review": text_review_with_emojy, "rating": 2}, 20 | {"text_review": text_review_with_allowed_symbols, "rating": 1}, 21 | {"text_review": text_review_with_extended_latin_letters, "rating": 5}, 22 | {"text_review": text_review_with_digits, "rating": 5}, 23 | { 24 | "text_review": "So happy with this purchase, it was perfect in every way.", 25 | "rating": 5, 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /UI/pages/RegistrationPage.py: -------------------------------------------------------------------------------- 1 | from .BasePage import BasePage 2 | from .locators import BasePageLocators, RegistrationPageLocators 3 | 4 | 5 | class RegistrationPage(BasePage): 6 | # check that sort drop-down is not present on the page 7 | def is_dropdown_present(self): 8 | self.is_element_present(*BasePageLocators.SORT_DROPDOWN) 9 | 10 | def register_new_user(self, first_name, last_name, email, password): 11 | first_name_field = self.browser.find_element(*RegistrationPageLocators.FIRST_NAME_FIELD) 12 | first_name_field.send_keys(first_name) 13 | last_name_field = self.browser.find_element(*RegistrationPageLocators.LAST_NAME_FIELD) 14 | last_name_field.send_keys(last_name) 15 | email_field = self.browser.find_element(*RegistrationPageLocators.EMAIL_FIELD) 16 | email_field.send_keys(email) 17 | password_field = self.browser.find_element(*RegistrationPageLocators.PASSWORD_FIELD) 18 | password_field.send_keys(password) 19 | register_button = self.browser.find_element(*RegistrationPageLocators.REGISTER_BUTTON) 20 | register_button.click() 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zufar Sunagatov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /configs.py: -------------------------------------------------------------------------------- 1 | from data.data_for_auth import ( 2 | JWT_SECRET, 3 | DEFAULT_PASSWORD, 4 | password2, 5 | gmail_password, 6 | gmail_password2, 7 | password, 8 | ) 9 | 10 | HOST = "https://iced-latte.uk/backend" 11 | 12 | # data for creating user#1 13 | email = "icedlate.test@gmail.com" 14 | password = password 15 | firstName = "TestUser" 16 | lastName = "TestUser" 17 | EMAIL_DOMAIN = "gmail.com" 18 | EMAIL_LOCAL_PART = "icedlate.test+" 19 | 20 | # DATA to connect to the qmail on imap.gmail.com server for user#1 21 | gmail_password = gmail_password 22 | imap_server = "imap.gmail.com" 23 | email_address_to_connect = "icedlate.test@gmail.com" 24 | 25 | # data for creating user#2 26 | email2 = "icedlate.test@gmail.com" 27 | password2 = password2 28 | firstName2 = "TestUser" 29 | lastName2 = "TestUser" 30 | EMAIL_DOMAIN2 = "gmail.com" 31 | EMAIL_LOCAL_PART2 = "icedlate2.test+" 32 | 33 | # DATA to connect to the qmail on imap.gmail.com server for user#2 34 | gmail_password2 = gmail_password2 35 | imap_server2 = "imap.gmail.com" 36 | email_address_to_connect2 = "icedlate2.test@gmail.com" 37 | 38 | # iced-late mail 39 | email_iced_late = "youricedlatteshop@gmail.com" 40 | DEFAULT_PASSWORD = DEFAULT_PASSWORD 41 | 42 | JWT_SECRET = JWT_SECRET 43 | -------------------------------------------------------------------------------- /framework/tools/load_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | import os 5 | import json 6 | 7 | 8 | def load_file_json(directory, file_name, file_type="json"): 9 | """ 10 | Load content from a file located in the specified directory. 11 | 12 | Args: 13 | directory (str): The directory where the file is located. 14 | file_name (str): The name of the file to load. 15 | file_type (str): The type of file, which dictates how the content is loaded. 16 | 17 | Returns: 18 | The content of the file, parsed according to the file_type argument. 19 | """ 20 | # Construct the absolute file path 21 | file_path = os.path.join( 22 | os.path.dirname(os.path.abspath(__file__)), directory, file_name 23 | ) 24 | print(f"Loading file from: {file_path}") # Debugging print 25 | # Load and return the file content based on its type 26 | if file_type == "json": 27 | try: 28 | with open(file_path, "r") as file: 29 | return json.load(file) 30 | except Exception as e: 31 | print(f"Failed to load or parse the JSON file {file_path}. Error: {e}") 32 | return [] 33 | else: 34 | print(f"Unsupported file type: {file_type}") 35 | return [] 36 | -------------------------------------------------------------------------------- /UI/test_change_email.py: -------------------------------------------------------------------------------- 1 | from .pages.ProfilePage import ProfilePage 2 | from .pages.EditProfilePage import EditProfilePage 3 | from .set_of_steps import go_to_edit_profile_page 4 | from .configs import link, new_email_positive 5 | 6 | from allure import step, title, severity, story 7 | import pytest 8 | 9 | 10 | @title("Test Change Email") 11 | @story("Personal Account") 12 | # @allure.description("") 13 | # @allure.tag("") 14 | @severity(severity_level="MAJOR") 15 | @pytest.mark.parametrize('new_email', new_email_positive) 16 | def test_user_can_change_email(browser, new_email): 17 | with step('Go to Edit Profile Page'): 18 | go_to_edit_profile_page(browser, link) 19 | with step('Enter new email'): 20 | page = EditProfilePage(browser, browser.current_url) 21 | page.change_email(new_email) 22 | with step('Click "Save Changes" button'): 23 | page.save_change() 24 | # with step('Assert Success massage is present'): 25 | # assert page.is_success_message_present('Your First Name was changed'), 'Success message is not present' 26 | with step('Assert New Email is present in profile'): 27 | page = ProfilePage(browser, browser.current_url) 28 | assert page.is_new_email_present(new_email), 'New Email is not present in profile' 29 | -------------------------------------------------------------------------------- /UI/test_dropdawn_not_present.py: -------------------------------------------------------------------------------- 1 | from .pages.BasePage import BasePage 2 | from .pages.LoginPage import LoginPage 3 | from .pages.RegistrationPage import RegistrationPage 4 | from .configs import link 5 | 6 | from allure import step, title, severity, story, severity_level 7 | 8 | 9 | @title("Test Fixed Bug: Sort Drop-down is present on Login and Registration page") 10 | @story("Registration/Authorization") 11 | # @allure.description("") 12 | # @allure.tag("") 13 | @severity(severity_level.CRITICAL) 14 | def test_dropdown_not_present(browser): 15 | with step('Go to Login Page'): 16 | page = BasePage(browser, link) 17 | page.open() 18 | page.go_to_login_page() 19 | with step('Assert Sort Drop-down is not present on Login Page'): 20 | page = LoginPage(browser, browser.current_url) 21 | page.open() 22 | assert not page.is_dropdown_present(), 'Sort drop-down is present on Login Page' 23 | with step('Go to Registration Page'): 24 | page.go_to_registration_page() 25 | with step('Assert Sort Drop-down is not present on Registration Page'): 26 | page = RegistrationPage(browser, browser.current_url) 27 | assert not page.is_dropdown_present(), 'Sort drop-down is present on Registration Page' 28 | -------------------------------------------------------------------------------- /framework/endpoints/product_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests import Response 3 | 4 | from configs import HOST 5 | 6 | 7 | class ProductAPI: 8 | def __init__(self): 9 | self.url = HOST + "/api/v1/products" 10 | self.headers = {"Content-Type": "application/json"} 11 | 12 | def get_by_id(self, _id: str, token: str = None) -> Response: 13 | """Getting product info by id 14 | 15 | Args: 16 | token: JWT token for authorization of request; 17 | _id: product ID. 18 | """ 19 | headers = self.headers 20 | if token: 21 | headers["Authorization"] = f"Bearer {token}" 22 | url = self.url + f"/{_id}" 23 | response = requests.get(headers=headers, url=url) 24 | return response 25 | 26 | def get_all(self, token: str = None, params: dict = None) -> Response: 27 | """Getting info about all products 28 | 29 | Args: 30 | token: JWT token for authorization of request; 31 | params: URL-parameters request. 32 | """ 33 | headers = self.headers 34 | if token: 35 | headers["Authorization"] = f"Bearer {token}" 36 | response = requests.get(headers=headers, url=self.url, params=params) 37 | return response 38 | -------------------------------------------------------------------------------- /UI/test_change_last_name.py: -------------------------------------------------------------------------------- 1 | from .pages.ProfilePage import ProfilePage 2 | from .pages.EditProfilePage import EditProfilePage 3 | from .set_of_steps import go_to_edit_profile_page 4 | from .configs import link, new_last_name_positive 5 | 6 | from allure import step, title, severity, story 7 | import pytest 8 | 9 | 10 | @title("Test Change Last Name") 11 | @story("Personal Account") 12 | # @allure.description("") 13 | # @allure.tag("") 14 | @severity(severity_level="MAJOR") 15 | @pytest.mark.parametrize('new_last_name', new_last_name_positive) 16 | def test_user_can_change_last_name(browser, new_last_name): 17 | with step('Go to Edit Profile Page'): 18 | go_to_edit_profile_page(browser, link) 19 | with step('Enter new Last Name'): 20 | page = EditProfilePage(browser, browser.current_url) 21 | page.change_last_name(new_last_name) 22 | with step('Click "Save Changes" button'): 23 | page.save_change() 24 | # with step('Assert Success massage is present'): 25 | # assert page.is_success_message_present('Your Last Name was changed'), 'Success message is not present' 26 | with step('Assert New Last Name is present in profile'): 27 | page = ProfilePage(browser, browser.current_url) 28 | assert page.is_new_last_name_present(new_last_name), 'New Last Name is not present in profile' 29 | -------------------------------------------------------------------------------- /UI/test_change_first_name.py: -------------------------------------------------------------------------------- 1 | from .pages.ProfilePage import ProfilePage 2 | from .pages.EditProfilePage import EditProfilePage 3 | from .set_of_steps import go_to_edit_profile_page 4 | from .configs import link, new_first_name_positive 5 | 6 | from allure import step, title, severity, story 7 | import pytest 8 | 9 | 10 | @title("Test Change First Name") 11 | @story("Personal Account") 12 | # @allure.description("") 13 | # @allure.tag("") 14 | @severity(severity_level="MAJOR") 15 | @pytest.mark.parametrize('new_first_name', new_first_name_positive) 16 | def test_user_can_change_first_name(browser, new_first_name): 17 | with step('Go to Edit Profile Page'): 18 | go_to_edit_profile_page(browser, link) 19 | with step('Enter new First Name'): 20 | page = EditProfilePage(browser, browser.current_url) 21 | page.change_first_name(new_first_name) 22 | with step('Click "Save Changes" button'): 23 | page.save_change() 24 | # with step('Assert Success massage is present'): 25 | # assert page.is_success_message_present('Your First Name was changed'), 'Success message is not present' 26 | with step('Assert New First Name is present in profile'): 27 | page = ProfilePage(browser, browser.current_url) 28 | assert page.is_new_first_name_present(new_first_name), 'New First Name is not present in profile' 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | allure-pytest==2.13.3 2 | allure-python-commons==2.13.3 3 | assertpy==1.1 4 | atomicwrites==1.4.1 5 | attrs==23.1.0 6 | bcrypt==4.0.1 7 | build==1.1.1 8 | certifi==2023.11.17 9 | cffi==1.16.0 10 | cfgv==3.4.0 11 | charset-normalizer==3.3.2 12 | click==8.1.7 13 | colorama==0.4.6 14 | cryptography==42.0.5 15 | curlify==2.2.1 16 | distlib==0.3.7 17 | exceptiongroup==1.2.0 18 | execnet==2.0.2 19 | Faker==15.1.5 20 | filelock==3.13.1 21 | h11==0.14.0 22 | identify==2.5.32 23 | idna==3.6 24 | imap-tools==1.5.0 25 | iniconfig==2.0.0 26 | nodeenv==1.8.0 27 | outcome==1.3.0.post0 28 | packaging==23.2 29 | paramiko==3.4.0 30 | pip-tools==7.4.0 31 | pip-upgrade==0.0.6 32 | platformdirs==4.0.0 33 | pluggy==1.3.0 34 | postgres==4.0 35 | pre-commit==3.5.0 36 | psycopg2-binary==2.9.6 37 | psycopg2-pool==1.1 38 | py==1.11.0 39 | pycparser==2.21 40 | PyHamcrest==2.0.4 41 | PyJWT==2.8.0 42 | PyNaCl==1.5.0 43 | pyproject_hooks==1.0.0 44 | PySocks==1.7.1 45 | pytest==7.1.2 46 | pytest-rerunfailures==13.0 47 | pytest-xdist==3.5.0 48 | python-dateutil==2.8.2 49 | PyYAML==6.0.1 50 | requests==2.31.0 51 | selenium==4.16.0 52 | six==1.16.0 53 | sniffio==1.3.0 54 | sortedcontainers==2.4.0 55 | SQLAlchemy==2.0.29 56 | sshtunnel==0.4.0 57 | tomli==2.0.1 58 | trio==0.23.2 59 | trio-websocket==0.11.1 60 | typing_extensions==4.11.0 61 | urllib3==2.1.0 62 | virtualenv==20.24.7 63 | wsproto==1.2.0 64 | -------------------------------------------------------------------------------- /framework/asserts/registration_asserts.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_ 2 | import bcrypt 3 | 4 | 5 | def check_mapping_api_to_db(api_request: dict, database_data: tuple) -> None: 6 | """Checking the mapping of data from the request API to the database 7 | 8 | Args: 9 | api_request: reference data, data from an API request; 10 | database_data: compared data, data from the database (a dictionary). 11 | """ 12 | fields_api_to_db = { 13 | "email": database_data[6], 14 | "firstName": database_data[1], 15 | "lastName": database_data[2], 16 | "password": database_data[7], 17 | } 18 | 19 | hashed_password = bcrypt.hashpw( 20 | api_request["password"].encode("utf-8"), 21 | fields_api_to_db["password"].encode("utf-8"), 22 | ).decode("utf-8") 23 | 24 | for key_api, value_db in fields_api_to_db.items(): 25 | if key_api == "password": 26 | assert_that( 27 | hashed_password, 28 | is_(value_db), 29 | reason=f'"{key_api}" not equal to the expected value in the database', 30 | ) 31 | else: 32 | assert_that( 33 | api_request[key_api], 34 | is_(value_db), 35 | reason=f'"{key_api}" not equal to the expected value in the database', 36 | ) 37 | -------------------------------------------------------------------------------- /UI/pages/LoginPage.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.ui import WebDriverWait 2 | from selenium.webdriver.support import expected_conditions as ec 3 | from selenium.common.exceptions import TimeoutException 4 | 5 | from .BasePage import BasePage 6 | from .locators import BasePageLocators, LoginPageLocators 7 | 8 | 9 | class LoginPage(BasePage): 10 | def go_to_registration_page(self): 11 | link = self.browser.find_element(*LoginPageLocators.REGISTER_BUTTON) 12 | link.click() 13 | 14 | # check that sort drop-down is not present on the page 15 | def is_dropdown_present(self): 16 | self.is_element_present(*BasePageLocators.SORT_DROPDOWN) 17 | 18 | def is_login_page(self): 19 | is_true = True 20 | try: 21 | WebDriverWait(self.browser, 4).until( 22 | ec.text_to_be_present_in_element(LoginPageLocators.WELCOME_BACK, 'Welcome back') 23 | ) 24 | except TimeoutException: 25 | is_true = False 26 | return is_true 27 | 28 | def login_existing_user(self, email, password): 29 | email_field = self.browser.find_element(*LoginPageLocators.EMAIL_FIELD) 30 | password_field = self.browser.find_element(*LoginPageLocators.PASSWORD_FIELD) 31 | login_button = self.browser.find_element(*LoginPageLocators.LOGIN_BUTTON) 32 | email_field.send_keys(email) 33 | password_field.send_keys(password) 34 | login_button.click() 35 | -------------------------------------------------------------------------------- /UI/pages/ProfilePage.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.ui import WebDriverWait 2 | from selenium.webdriver.support import expected_conditions as ec 3 | from .BasePage import BasePage 4 | from .locators import ProfilePageLocators 5 | 6 | 7 | class ProfilePage(BasePage): 8 | def go_to_edit_page(self): 9 | button = self.browser.find_element(*ProfilePageLocators.EDIT_BUTTON) 10 | button.click() 11 | 12 | def is_new_email_present(self, new_email): 13 | email_field = WebDriverWait(self.browser, 3).until( 14 | ec.presence_of_element_located(ProfilePageLocators.EMAIL_FIELD) 15 | ) 16 | if email_field.text == f'Email:\n{new_email}': 17 | return True 18 | else: 19 | return False 20 | 21 | def is_new_first_name_present(self, new_first_name): 22 | first_name_field = self.browser.find_element(*ProfilePageLocators.FIRST_NAME_FIELD) 23 | if first_name_field.text == f'First name:\n{new_first_name}': 24 | return True 25 | else: 26 | return False 27 | 28 | def is_new_last_name_present(self, new_last_name): 29 | last_name_field = self.browser.find_element(*ProfilePageLocators.LAST_NAME_FIELD) 30 | if last_name_field.text == f'Last name:\n{new_last_name}': 31 | return True 32 | else: 33 | return False 34 | 35 | def log_out(self): 36 | log_out_button = self.browser.find_element(*ProfilePageLocators.LOG_OUT_BUTTON) 37 | log_out_button.click() 38 | -------------------------------------------------------------------------------- /UI/test_change_email_negative.py: -------------------------------------------------------------------------------- 1 | from .pages.BasePage import BasePage 2 | from .pages.ProfilePage import ProfilePage 3 | from .pages.EditProfilePage import EditProfilePage 4 | from .set_of_steps import go_to_edit_profile_page 5 | from .configs import link, new_email_negative 6 | 7 | from allure import step, title, severity, story, severity_level 8 | import pytest 9 | from time import sleep 10 | 11 | 12 | @title("Test Change Email Negative") 13 | @story("Personal Account") 14 | # @allure.description("") 15 | # @allure.tag("") 16 | @severity(severity_level.NORMAL) 17 | @pytest.mark.parametrize('new_email', new_email_negative) 18 | def test_user_cant_change_email(browser, new_email): 19 | with step('Go to Edit Profile Page'): 20 | go_to_edit_profile_page(browser, link) 21 | with step('Enter new Negative Email'): 22 | page = EditProfilePage(browser, browser.current_url) 23 | page.change_email(new_email) 24 | with step('Click "Save Changes" Button'): 25 | page.save_change() 26 | with step('Go to Main Page'): 27 | page.go_to_main_page() 28 | sleep(2) # waiting is mandatory (do not remove) 29 | with step('Go to Profile Page'): 30 | page = BasePage(browser, link) 31 | page.go_to_profile_page() 32 | with step('Assert New Negative Email is not Present in Profile'): 33 | page = ProfilePage(browser, browser.current_url) 34 | assert not page.is_new_email_present(new_email), 'New Negative Email is Present in Profile' 35 | -------------------------------------------------------------------------------- /tests/user/test_delete_user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from allure import severity 4 | from hamcrest import assert_that, equal_to 5 | 6 | from framework.asserts.common import assert_response_message 7 | from framework.endpoints.users_api import UsersAPI 8 | from framework.queries.postgres_db import PostgresDB 9 | 10 | 11 | @feature("Delete user") 12 | class TestDeleteUser: 13 | @pytest.mark.critical 14 | @severity(severity_level="MAJOR") 15 | @title("Test delete user") 16 | @description( 17 | "GIVEN user is registered" 18 | "WHEN user sends a request to delete user" 19 | "THEN status HTTP CODE = 200" 20 | ) 21 | def test_delete_user(self, create_authorized_user): 22 | with step("Registration of user"): 23 | user, token = ( 24 | create_authorized_user["user"], 25 | create_authorized_user["token"], 26 | ) 27 | 28 | with step("Delete user"): 29 | UsersAPI().delete_user(token=token, expected_status_code=200) 30 | 31 | with step("Verify that user is deleted through API."): 32 | response = UsersAPI().get_user(token=token, expected_status_code=404) 33 | assert_message = "User with the provided email does not exist" 34 | assert_response_message(response, assert_message) 35 | 36 | with step("Verify that user is not in BD"): 37 | result = PostgresDB().select_user_by_email(email=user["email"]) 38 | count = result[0]["count"] 39 | assert_that(count, equal_to(0)) 40 | -------------------------------------------------------------------------------- /tests/favorite/test_delete_product_from_favorite.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pytest 3 | from allure import description, step, title, feature 4 | 5 | from framework.endpoints.favorite_api import FavoriteAPI 6 | 7 | 8 | @feature("Delete product from favorite list ") 9 | class TestFavorite: 10 | @pytest.mark.critical 11 | @pytest.mark.parametrize("product_quantity", [2], indirect=True) 12 | @title("Test delete product from favorite list") 13 | @description( 14 | "GIVEN user is registered has favorite products list" 15 | "WHEN user delete product from favorite list" 16 | "THEN status HTTP CODE = 200" 17 | ) 18 | def test_deleting_product_from_favorite_list(self, add_product_to_favorite_list): 19 | with step("Registration of user"): 20 | token, product_list_added_to_favorite = add_product_to_favorite_list 21 | 22 | with step("Get info about user's favorite list"): 23 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 24 | 25 | with step("Selecting random product from favorite list for deleting"): 26 | favorite_product_list_info_from_response = ( 27 | response_get_favorites.json().get("products") 28 | ) 29 | selected_product = random.choice(favorite_product_list_info_from_response) 30 | selected_id_product_to_delete = selected_product.get("id") 31 | 32 | with step("Deleting selected product from favorite list"): 33 | FavoriteAPI().delete_favorites( 34 | token=token, id_product=selected_id_product_to_delete 35 | ) 36 | -------------------------------------------------------------------------------- /framework/tools/logging_allure.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import curlify 4 | import allure 5 | from allure_commons.types import AttachmentType 6 | from requests import Response 7 | 8 | 9 | def log_request(response: Response) -> None: 10 | """Logging request and response data to Allure report""" 11 | 12 | method, url = response.request.method, response.request.url 13 | 14 | with allure.step(f"{method} {url}"): 15 | message = curlify.to_curl(response.request) 16 | allure.attach( 17 | body=message.encode("utf8"), 18 | name=f"Request {method} {response.status_code}", 19 | attachment_type=allure.attachment_type.TEXT, 20 | extension="txt", 21 | ) 22 | 23 | if ( 24 | "application/json" in response.headers.get("Content-Type", "") 25 | and response.text 26 | ): 27 | try: 28 | body = response.json() 29 | allure.attach( 30 | body=json.dumps(body, indent=2), 31 | name="Response body", 32 | attachment_type=AttachmentType.JSON, 33 | ) 34 | except json.decoder.JSONDecodeError: 35 | allure.attach( 36 | body=response.text, 37 | name="Non-JSON Response body", 38 | attachment_type=allure.attachment_type.TEXT, 39 | ) 40 | else: 41 | allure.attach( 42 | body=response.text, 43 | name="Non-JSON Response body", 44 | attachment_type=allure.attachment_type.TEXT, 45 | ) 46 | -------------------------------------------------------------------------------- /UI/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from selenium.webdriver.firefox.options import Options 4 | 5 | 6 | def pytest_addoption(parser): 7 | parser.addoption('--browser_name', action='store', default='chrome', 8 | help="Choose browser: chrome or firefox") 9 | parser.addoption('--language', action='store', default='en', 10 | help="Choose language: es, ru or en") 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def browser(request): 15 | options = webdriver.ChromeOptions() 16 | # turn off Chrome service message 17 | options.add_experimental_option('excludeSwitches', ['enable-logging']) 18 | # get language from cmd 19 | user_language = request.config.getoption("language") 20 | # turn on Chrome language support 21 | options.add_experimental_option('prefs', {'intl.accept_languages': user_language}) 22 | # turn on FF language support 23 | options_ff = Options() 24 | options_ff.set_preference("intl.accept_languages", user_language) 25 | # get browser from cmd 26 | browser_name = request.config.getoption("browser_name") 27 | browser = None 28 | if browser_name == "chrome": 29 | print("\nstart chrome browser for test..") 30 | browser = webdriver.Chrome(options=options) 31 | elif browser_name == "firefox": 32 | print("\nstart firefox browser for test..") 33 | browser = webdriver.Firefox(options=options_ff) 34 | else: 35 | raise pytest.UsageError("--browser_name should be chrome or firefox") 36 | yield browser 37 | print("\nquit browser..") 38 | browser.quit() 39 | -------------------------------------------------------------------------------- /tests/authentication/test_logout.py: -------------------------------------------------------------------------------- 1 | from allure import feature, description, link, step, title 2 | from hamcrest import assert_that, is_ 3 | 4 | from framework.endpoints.authenticate_api import AuthenticateAPI 5 | from framework.endpoints.users_api import UsersAPI 6 | 7 | 8 | @feature("Logout of a user") 9 | @link( 10 | url="https://github.com/Sunagatov/Online-Store/wiki/", 11 | name="(!) WAIT LINK. Description of the tested functionality", 12 | ) 13 | class TestLogout: 14 | @title("Checking log out") 15 | @description( 16 | "GIVEN the user is logged in, WHEN the user logout, THEN the JWT token is not valid" 17 | ) 18 | def test_logout(self, create_authorized_user): 19 | user, token = create_authorized_user["user"], create_authorized_user["token"] 20 | 21 | with step("Log out of user"): 22 | logging_out_response = AuthenticateAPI().logout(token=token) 23 | assert_that( 24 | logging_out_response.status_code, 25 | is_(200), 26 | reason='Failed request "logout"', 27 | ) 28 | 29 | with step("Re-getting data user by ID via API"): 30 | getting_user_response = UsersAPI().get_user( 31 | token=token, expected_status_code=400 32 | ) 33 | 34 | with step("Checking the response from the API"): 35 | assert_that( 36 | getting_user_response.status_code, 37 | is_(400), 38 | reason="Log out not executed", 39 | ) 40 | assert_that( 41 | getting_user_response.json()["message"], 42 | is_("JWT Token is blacklisted"), 43 | reason="Description does not match", 44 | ) 45 | -------------------------------------------------------------------------------- /UI/test_change_last_name_negative.py: -------------------------------------------------------------------------------- 1 | from .pages.BasePage import BasePage 2 | from .pages.ProfilePage import ProfilePage 3 | from .pages.EditProfilePage import EditProfilePage 4 | from .set_of_steps import go_to_edit_profile_page 5 | from .configs import link, new_last_name_negative 6 | 7 | from allure import step, title, severity, story, severity_level 8 | import pytest 9 | 10 | 11 | @title("Test Change Last Name Negative") 12 | @story("Personal Account") 13 | # @allure.description("") 14 | # @allure.tag("") 15 | @severity(severity_level.NORMAL) 16 | @pytest.mark.parametrize('new_last_name', new_last_name_negative) 17 | def test_user_cant_change_last_name(browser, new_last_name): 18 | with step('Go to Edit Profile Page'): 19 | go_to_edit_profile_page(browser, link) 20 | with step('Enter new Negative Last Name'): 21 | page = EditProfilePage(browser, browser.current_url) 22 | page.change_last_name(new_last_name) 23 | with step('Click "Save Changes" button'): 24 | page.save_change() 25 | with step('Assert Error Message is Present'): 26 | assert page.is_error_message_last_name_present(('Invalid Last name format. Use extended Latin letters', 27 | 'Last name is required', 28 | 'Server Error: Internal server error') 29 | ), 'Error message is not present' 30 | with step('Go to Main Page'): 31 | page.go_to_main_page() 32 | with step('Go to Profile Page'): 33 | page = BasePage(browser, browser.current_url) 34 | page.go_to_profile_page() 35 | with step('Assert New Negative Last Name is not Present in Profile'): 36 | page = ProfilePage(browser, browser.current_url) 37 | assert not page.is_new_last_name_present(new_last_name), 'New Negative Last Name is Present in Profile' 38 | -------------------------------------------------------------------------------- /framework/asserts/assert_cart.py: -------------------------------------------------------------------------------- 1 | from assertpy import assert_that as assertpy_assert_that 2 | from requests import Response 3 | 4 | 5 | def assert_deleted_item_ids_in_response( 6 | response: Response, ids_to_check: list[dict] 7 | ) -> None: 8 | """Checking deleted items in the request API 9 | 10 | Args: 11 | response: The API response object, expected to contain a JSON body with product information. 12 | ids_to_check: dictionary with list deleted product ids . 13 | """ 14 | try: 15 | response_data = response.json() 16 | except ValueError as e: 17 | raise AssertionError(f"Response is not in valid JSON format: {e}") from e 18 | 19 | item_ids = [item["id"] for item in response_data.get("items", [])] 20 | for id_to_check in ids_to_check: 21 | assertpy_assert_that(item_ids).does_not_contain(id_to_check) 22 | 23 | 24 | def assert_added_product_not_in_api_response( 25 | response: Response, added_products: list[dict] 26 | ) -> None: 27 | """ 28 | Asserts that none of the products specified in added_products are present in the API response. 29 | 30 | Args: 31 | added_products: A list of dictionaries, each containing 'productId' and 'productQuantity' keys 32 | for products added to the user's shopping cart. 33 | response: The API response object, expected to contain a JSON body with product information. 34 | """ 35 | 36 | try: 37 | response_data = response.json() 38 | except ValueError as e: 39 | raise AssertionError(f"Response is not in valid JSON format: {e}") from e 40 | 41 | response_product_ids = [ 42 | item["productInfo"]["id"] for item in response_data["items"] 43 | ] 44 | 45 | added_product_ids = [product["productId"] for product in added_products] 46 | 47 | for added_product_id in added_product_ids: 48 | assertpy_assert_that(response_product_ids).does_not_contain(added_product_id) 49 | -------------------------------------------------------------------------------- /tests/product/test_product.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from allure import description, feature, link, step, title 4 | from hamcrest import assert_that, is_ 5 | 6 | from framework.asserts.common import assert_status_code, assert_response_message 7 | from framework.asserts.product_asserts import check_mapping_db_to_api 8 | from framework.endpoints.product_api import ProductAPI 9 | 10 | 11 | @feature("Getting product info by ID") 12 | class TestProduct: 13 | @title("Getting product info by ID not authorized") 14 | @description( 15 | "WHEN not authorized requesting to get product info by ID, THEN product info is returned" 16 | ) 17 | def test_get_all_products(self, postgres): 18 | with step("Getting info about the random product in DB"): 19 | db_data = postgres.get_random_products()[0] 20 | 21 | with step("Getting product info by ID via API"): 22 | data = ProductAPI().get_by_id(_id=db_data["id"]) 23 | assert_data = data.json() 24 | 25 | with step("Checking mapping data DB <> API"): 26 | check_mapping_db_to_api(reference=assert_data, compared=db_data) 27 | 28 | @title("Getting a non-existent product") 29 | @description( 30 | "WHEN requesting to get by a non-existent product ID, THEN the user receives a 404 status code" 31 | ) 32 | def test_get_product(self): 33 | with step("Generating a non-existent ID"): 34 | non_exist_id = str(uuid.uuid4()) 35 | 36 | with step( 37 | f'Getting product info by non-existent product ID via API "{non_exist_id}"' 38 | ): 39 | data = ProductAPI().get_by_id(_id=non_exist_id) 40 | 41 | with step("Checking response API"): 42 | assert_status_code(data, 404) 43 | expected_message = ( 44 | f"The product with productId = {non_exist_id} is not found." 45 | ) 46 | assert_response_message(data, expected_message) 47 | -------------------------------------------------------------------------------- /UI/test_change_first_name_negative.py: -------------------------------------------------------------------------------- 1 | from .pages.BasePage import BasePage 2 | from .pages.ProfilePage import ProfilePage 3 | from .pages.EditProfilePage import EditProfilePage 4 | from .set_of_steps import go_to_edit_profile_page 5 | from .configs import link, new_first_name_negative 6 | 7 | from allure import step, title, severity, story, severity_level 8 | import pytest 9 | 10 | 11 | @title("Test Change First Name Negative") 12 | @story("Personal Account") 13 | # @allure.description("") 14 | # @allure.tag("") 15 | @severity(severity_level.NORMAL) 16 | @pytest.mark.parametrize('new_first_name', new_first_name_negative) 17 | def test_user_cant_change_first_name(browser, new_first_name): 18 | with step('Go to Edit Profile Page'): 19 | go_to_edit_profile_page(browser, link) 20 | with step('Enter new Negative First Name'): 21 | page = EditProfilePage(browser, browser.current_url) 22 | page.change_first_name(new_first_name) 23 | with step('Click "Save Changes" button'): 24 | page.save_change() 25 | with step('Assert Error Message is Present'): 26 | assert page.is_error_message_first_name_present(('Invalid name format. Use extended Latin letters, spaces, and specified symbols', 27 | 'name is required', 28 | 'Server Error: Internal server error') 29 | ), 'Error message is not present' 30 | with step('Go to Main Page'): 31 | page.go_to_main_page() 32 | with step('Go to Profile Page'): 33 | page = BasePage(browser, browser.current_url) 34 | page.go_to_profile_page() 35 | with step('Assert New Negative First Name is not Present in Profile'): 36 | page = ProfilePage(browser, browser.current_url) 37 | assert not page.is_new_first_name_present(new_first_name), 'New Negative First Name is Present in Profile' 38 | -------------------------------------------------------------------------------- /framework/asserts/user_asserts.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_ 2 | 3 | 4 | def assert_user_data_matches(response_data: dict, expected_user: dict) -> None: 5 | """Asserts that all relevant user data in the response matches the expected user data. 6 | 7 | Args: 8 | response_data: The user data from the API response. 9 | expected_user: The expected user data. 10 | 11 | Raises: 12 | AssertionError: If any of the assertions fail. 13 | """ 14 | 15 | fields = [key for key in response_data if key != "address"] 16 | 17 | assert_partial_match(expected_user, response_data, fields) 18 | 19 | 20 | def assert_update_user_data_matches(response_data: dict, expected_user: dict) -> None: 21 | """Asserts that all relevant user data in the response matches the expected user data. 22 | 23 | Args: 24 | response_data: The user data from the API response. 25 | expected_user: The expected user data. 26 | 27 | Raises: 28 | AssertionError: If any of the assertions fail. 29 | """ 30 | 31 | fields = ["firstName", "lastName", "birthDate", "phoneNumber", "email"] 32 | 33 | assert_partial_match(expected_user, response_data, fields) 34 | 35 | 36 | def assert_partial_match(expected: dict, actual: dict, fields_to_compare: list) -> None: 37 | """ 38 | Asserts that the specified fields in the actual object match those in the expected object. 39 | 40 | Args: 41 | expected: The object with expected values. 42 | actual: The object to compare against the expected values. 43 | fields_to_compare: List of field names (str) to compare. 44 | 45 | Raises: 46 | AssertionError: If the fields do not match. 47 | """ 48 | for field in fields_to_compare: 49 | expected_value = expected.get(field) 50 | actual_value = actual.get(field) 51 | assert_that( 52 | actual_value, 53 | is_(expected_value), 54 | f"Field '{field}' mismatch: Expected '{expected_value}', Found '{actual_value}'", 55 | ) 56 | -------------------------------------------------------------------------------- /UI/pages/FavoritesPage.py: -------------------------------------------------------------------------------- 1 | from allure import step 2 | from time import sleep 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as ec 5 | from .BasePage import BasePage 6 | from .locators import FavoritesPageLocators, BasePageLocators 7 | from ..configs import login_page_link 8 | 9 | 10 | class FavoritesPage(BasePage): 11 | @step('Click "Continue Shopping" button') 12 | def click_continue_shopping_button(self) -> None: 13 | continue_shopping_button = self.browser.find_element(*FavoritesPageLocators.CONTINUE_SHOPPING_BUTTON) 14 | continue_shopping_button.click() 15 | 16 | # waiting for the main page to load 17 | WebDriverWait(self.browser, 4).until(ec.presence_of_element_located(BasePageLocators.HEADING_ELEMENT)) 18 | 19 | @step('Click "Log in" button') 20 | def click_log_in_button(self) -> None: 21 | log_in_button = self.browser.find_element(*FavoritesPageLocators.LOG_IN_BUTTON) 22 | log_in_button.click() 23 | 24 | # waiting for the login page to load 25 | WebDriverWait(self.browser, 4).until(ec.url_to_be(login_page_link)) 26 | 27 | def go_to_product_page(self) -> None: 28 | link = self.browser.find_element(*FavoritesPageLocators.PRODUCT_LINK) 29 | link.click() 30 | 31 | def is_favorites_empty(self) -> bool: 32 | return self.is_element_present(*FavoritesPageLocators.EMPTY_FAVORITES_MESSAGE) 33 | 34 | def is_product_in_favorites(self, product_name: str) -> bool: 35 | favorites_page_product_name = self.browser.find_element(*FavoritesPageLocators.PRODUCT_NAME) 36 | if favorites_page_product_name.text == product_name: 37 | return True 38 | else: 39 | return False 40 | 41 | @step('Remove products from favorites') 42 | def remove_favorites_products(self) -> None: 43 | buttons = self.browser.find_elements(*FavoritesPageLocators.UNLIKE_BUTTONS) 44 | for button in buttons: 45 | button.click() 46 | sleep(2) # waiting is mandatory (do not remove) 47 | -------------------------------------------------------------------------------- /tests/cart/test_get_shopping_cart.py: -------------------------------------------------------------------------------- 1 | from allure import description, step, title, feature 2 | from hamcrest import assert_that, has_key, has_length, equal_to 3 | 4 | from framework.asserts.common import assert_response_message, assert_content_type 5 | from framework.endpoints.cart_api import CartAPI 6 | from framework.endpoints.users_api import UsersAPI 7 | 8 | 9 | @feature("Get shopping cart") 10 | class TestCart: 11 | @title("Get shopping cart with no item in it ") 12 | @description( 13 | "GIVEN user is registered and does not have shopping cart" 14 | "WHEN user get the cart" 15 | "THEN status HTTP CODE = 200 and the response body contains response items=[]/empty." 16 | ) 17 | def test_get_user_cart(self, create_authorized_user): 18 | with step("Registration of user"): 19 | user, token = ( 20 | create_authorized_user["user"], 21 | create_authorized_user["token"], 22 | ) 23 | 24 | with step("Get user's id info."): 25 | response_get_user_info = UsersAPI().get_user(token=token) 26 | new_user_id = response_get_user_info.json()["id"] 27 | 28 | with step("Get user's shopping cart. "): 29 | response_get_cart = CartAPI().get_user_cart( 30 | token=token, expected_status_code=200 31 | ) 32 | 33 | with step( 34 | "Verify user's ID in the shopping cart/response body contain correct user's id." 35 | ): 36 | expected_user_id_in_cart = response_get_cart.json()["userId"] 37 | assert_that( 38 | new_user_id, 39 | equal_to(expected_user_id_in_cart), 40 | "Expected user ID does not match.", 41 | ) 42 | 43 | with step("Verify that user doesn't have items in shopping cart"): 44 | data = response_get_cart.json() 45 | assert_that(data, has_key("items")) 46 | assert_that(data["items"], has_length(0)) 47 | 48 | with step("Verify content-type."): 49 | assert_content_type(response_get_cart, "application/json") 50 | -------------------------------------------------------------------------------- /framework/clients/db_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from typing import Optional, List 4 | 5 | from psycopg2 import connect 6 | from psycopg2.extras import RealDictCursor 7 | 8 | 9 | class DBClient: 10 | def __init__(self, dbname: str, host: str, port: str, user: str, password: str): 11 | """Initializing the connection 12 | 13 | Args: 14 | dbname: name Postgres database; 15 | host: URL for connecting to the Postgres database; 16 | port: port for connecting to the Postgres database; 17 | user: username for connecting to the Postgres database; 18 | password: password for connecting to the Postgres database. 19 | """ 20 | self.conn = connect( 21 | host=host, port=port, dbname=dbname, user=user, password=password 22 | ) 23 | self.conn.autocommit = True 24 | self.cur = self.conn.cursor(cursor_factory=RealDictCursor) 25 | logging.info(self.conn) 26 | 27 | def close(self) -> None: 28 | if not self.conn: 29 | logging.info("Not connection") 30 | return 31 | if self.cur: 32 | logging.info("Cursor the closed") 33 | self.cur.close() 34 | logging.info("Connection the closed") 35 | self.conn.close() 36 | 37 | def execute(self, query: str) -> None: 38 | """Executing a query to the Postgres database without returning data 39 | 40 | Args: 41 | query: query to the Postgres database 42 | """ 43 | logging.debug(query) 44 | self.cur.execute(str(query), None) 45 | 46 | def fetch_all(self, query: str) -> Optional[List[dict]]: 47 | """Executing a query to the Postgres database with returning data in the form of list 48 | 49 | Args: 50 | query: query to the Postgres database 51 | 52 | Returns: 53 | [{row1}, {row2}, ...] 54 | """ 55 | self.execute(query) 56 | records = self.cur.fetchall() 57 | if records: 58 | rows = [dict(rec) for rec in records] 59 | return rows 60 | 61 | return [] 62 | -------------------------------------------------------------------------------- /framework/tools/class_email.py: -------------------------------------------------------------------------------- 1 | from imap_tools import MailBox, A 2 | import re 3 | from typing import Union 4 | import time 5 | 6 | 7 | class Email: 8 | def __init__(self, imap_server: str, email_address: str, mail_password: str): 9 | self.imap_server = imap_server 10 | self.email_address = email_address 11 | self.password = mail_password 12 | 13 | def connect_mailbox(self, email_box: str): 14 | """Method to establish a connection to the mailbox. 15 | 16 | Args: 17 | email_box: The email folder to connect to. 18 | 19 | Returns: 20 | MailBox: The connected mailbox object. 21 | """ 22 | return MailBox(self.imap_server).login( 23 | self.email_address, self.password, initial_folder=email_box 24 | ) 25 | 26 | def extract_confirmation_code_from_email( 27 | self, email_box: str, key: str, value: str 28 | ) -> Union[str, None]: 29 | """Method for extraction confirmation code for registration from user's email. 30 | 31 | Args: 32 | email_box: email's folder, where search is performed 33 | key: criterion to search message through email (e.g., "FROM") 34 | value: value for the search criterion (e.g., "email.email@gmail.com") 35 | """ 36 | attempt = 0 37 | delay = 10 38 | max_attempts = 4 39 | while attempt < max_attempts: 40 | current_time = time.time() 41 | 42 | with self.connect_mailbox(email_box) as mailbox: 43 | messages = mailbox.fetch( 44 | criteria=A(**{key.lower(): value}), mark_seen=False, bulk=True 45 | ) 46 | for msg in messages: 47 | email_time = msg.date.timestamp() 48 | if current_time - email_time <= 13.5: 49 | text = msg.text or "" 50 | pattern = r"\d{9}" 51 | match = re.search(pattern, text) 52 | if match: 53 | return match.group() 54 | 55 | time.sleep(delay) 56 | attempt += 1 57 | 58 | return None 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # PyInstaller 7 | # Usually these files are written by a python script from a template 8 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 9 | *.manifest 10 | *.spec 11 | 12 | # Installer logs 13 | pip-log.txt 14 | pip-delete-this-directory.txt 15 | 16 | # Unit test / coverage reports 17 | htmlcov/ 18 | .tox/ 19 | .nox/ 20 | .coverage 21 | .coverage.* 22 | .cache 23 | nosetests.xml 24 | coverage.xml 25 | *.cover 26 | *.py,cover 27 | .hypothesis/ 28 | .pytest_cache/ 29 | cover/ 30 | 31 | # pyenv 32 | # For a library or package, you might want to ignore these files since the code is 33 | # intended to run in multiple environments; otherwise, check them in: 34 | # .python-version 35 | 36 | # pipenv 37 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 38 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 39 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 40 | # install all needed dependencies. 41 | #Pipfile.lock 42 | 43 | # poetry 44 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 45 | # This is especially recommended for binary packages to ensure reproducibility, and is more 46 | # commonly ignored for libraries. 47 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 48 | #poetry.lock 49 | 50 | # Environments 51 | .env 52 | .venv 53 | env/ 54 | venv/ 55 | ENV/ 56 | env.bak/ 57 | venv.bak/ 58 | 59 | # PyCharm 60 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 61 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 62 | # and can be added to the global gitignore or merged into this file. For a more nuclear 63 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 64 | .idea/ 65 | 66 | # Mac 67 | .DS_Store 68 | 69 | # Allure 70 | allure_report 71 | # Config 72 | 73 | tests/__pycache__/ 74 | data/data_for_auth.py 75 | my_data.py 76 | 77 | # VSCode 78 | .vscode/ -------------------------------------------------------------------------------- /UI/set_of_steps.py: -------------------------------------------------------------------------------- 1 | from allure import step 2 | from time import sleep 3 | from selenium.webdriver.remote.webdriver import WebDriver 4 | 5 | from .pages.BasePage import BasePage 6 | from .pages.CartPage import CartPage 7 | from .pages.FavoritesPage import FavoritesPage 8 | from .pages.LoginPage import LoginPage 9 | from .pages.ProductPage import ProductPage 10 | from .pages.ProfilePage import ProfilePage 11 | 12 | from .configs import email, password 13 | 14 | 15 | def delete_old_review(browser: WebDriver, link: str) -> None: 16 | with step('Go to product page'): 17 | main_page = BasePage(browser, link) 18 | main_page.sort_by('price', 'high') 19 | main_page.go_to_product_page() 20 | with step('Delete old review'): 21 | product_page = ProductPage(browser, browser.current_url) 22 | product_page.delete_review() 23 | 24 | 25 | @step('Login User') 26 | def login_user(browser: WebDriver, link: str) -> None: 27 | with step('Open main page'): 28 | page = BasePage(browser, link) 29 | page.open() 30 | with step('Go to login page'): 31 | page.go_to_login_page() 32 | with step('Login existing user'): 33 | login_page = LoginPage(browser, browser.current_url) 34 | login_page.login_existing_user(email, password) 35 | sleep(2) 36 | 37 | 38 | def go_to_edit_profile_page(browser: WebDriver, link: str) -> None: 39 | with step('Login existing user'): 40 | login_user(browser, link) 41 | with step('Go to profile page'): 42 | page = BasePage(browser, browser.current_url) 43 | page.go_to_profile_page() 44 | with step('Click "Edit" button'): 45 | page = ProfilePage(browser, browser.current_url) 46 | page.go_to_edit_page() 47 | 48 | 49 | @step('Remove products from cart and favorites') 50 | def remove_products_from_cart_and_favorites(browser: WebDriver, link: str) -> None: 51 | main_page = BasePage(browser, link) 52 | main_page.go_to_cart_page() 53 | with step('Remove all products from the cart'): 54 | cart_page = CartPage(browser, browser.current_url) 55 | cart_page.remove_products() 56 | with step('Go to favorites page'): 57 | cart_page.go_to_favorites_page() 58 | favorites_page = FavoritesPage(browser, browser.current_url) 59 | with step('Remove all products from the favorites'): 60 | favorites_page.remove_favorites_products() 61 | favorites_page.go_to_main_page() 62 | -------------------------------------------------------------------------------- /tests/authentication/test_authentication.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from allure import severity 4 | from hamcrest import assert_that, is_not, empty, is_ 5 | 6 | from configs import ( 7 | email_iced_late, 8 | imap_server, 9 | email_address_to_connect, 10 | gmail_password, 11 | firstName, 12 | lastName, 13 | password, 14 | ) 15 | from framework.endpoints.authenticate_api import AuthenticateAPI 16 | from framework.endpoints.users_api import UsersAPI 17 | 18 | 19 | @feature("Authentication of user") 20 | class TestAuthentication: 21 | @pytest.mark.critical 22 | @severity(severity_level="MAJOR") 23 | @title("Test authentication") 24 | @description( 25 | "GIVEN user is registered" 26 | "WHEN user submit valid credential for authentication" 27 | "THEN status HTTP CODE = 200 and get JWT token" 28 | ) 29 | @pytest.mark.parametrize( 30 | "registration_and_cleanup_user_through_api", 31 | [ 32 | { 33 | "firstName": firstName, 34 | "lastName": lastName, 35 | "password": password, 36 | "email_iced_late": email_iced_late, 37 | "imap_server": imap_server, 38 | "email_address_to_connect": email_address_to_connect, 39 | "gmail_password": gmail_password, 40 | } 41 | ], 42 | indirect=True, 43 | ) 44 | def test_authentication(self, registration_and_cleanup_user_through_api): 45 | with step("Registration user"): 46 | data_for_registration = registration_and_cleanup_user_through_api["user"] 47 | 48 | with step("Authentication user"): 49 | data_post = { 50 | "email": data_for_registration["email"], 51 | "password": data_for_registration["password"], 52 | } 53 | response = AuthenticateAPI().authentication( 54 | email=data_post["email"], password=data_post["password"] 55 | ) 56 | 57 | with step("Assert Response JSON have a 'token' key"): 58 | token = response.json().get("token") 59 | assert_that(token, is_not(empty())) 60 | 61 | with step("Validation token by retrieving user information via API request"): 62 | response = UsersAPI().get_user(token=token) 63 | assert_that( 64 | response.status_code, is_(200), reason="Expected status code 200" 65 | ) 66 | -------------------------------------------------------------------------------- /UI/pages/CartPage.py: -------------------------------------------------------------------------------- 1 | from allure import step 2 | from time import sleep 3 | 4 | from .BasePage import BasePage 5 | from .locators import CartPageLocators 6 | 7 | 8 | class CartPage(BasePage): 9 | @step('Click Continue Shopping Button on Empty Cart') 10 | def click_continue_shopping_button(self) -> None: 11 | button = self.browser.find_element(*CartPageLocators.CONTINUE_SHOPPING_BUTTON) 12 | button.click() 13 | 14 | def click_minus_button(self): 15 | button = self.browser.find_element(*CartPageLocators.MINUS_BUTTON) 16 | button.click() 17 | 18 | def click_minus_2_button(self): 19 | button = self.browser.find_element(*CartPageLocators.MINUS_2_BUTTON) 20 | button.click() 21 | 22 | def click_plus_button(self): 23 | button = self.browser.find_element(*CartPageLocators.PLUS_BUTTON) 24 | button.click() 25 | 26 | def click_plus_2_button(self): 27 | button = self.browser.find_element(*CartPageLocators.PLUS_2_BUTTON) 28 | button.click() 29 | 30 | def get_product_cost(self): 31 | return self.browser.find_element(*CartPageLocators.PRODUCT_COST).text 32 | 33 | def get_product_2_cost(self): 34 | return self.browser.find_element(*CartPageLocators.PRODUCT_2_COST).text 35 | 36 | def get_subtotal(self): 37 | return self.browser.find_element(*CartPageLocators.SUBTOTAL).text 38 | 39 | def is_cart_empty(self): 40 | self.is_element_present(*CartPageLocators.EMPTY_CART_MESSAGE) 41 | 42 | # check that amount changed after click "Plus" or "Minus" 43 | def is_change_amount(self, amount): 44 | amount_element = self.browser.find_element(*CartPageLocators.AMOUNT) 45 | if amount_element.text == amount: 46 | return True 47 | else: 48 | return False 49 | 50 | # check that product name on main page equal product name on cart page 51 | def is_product_in_cart(self, product_name): 52 | cart_page_product_name = self.browser.find_element(*CartPageLocators.PRODUCT_NAME) 53 | if cart_page_product_name.text == product_name: 54 | return True 55 | else: 56 | return False 57 | 58 | def remove_products(self): 59 | buttons = self.browser.find_elements(*CartPageLocators.REMOVE_BUTTON) 60 | while len(buttons) > 0: 61 | buttons[0].click() 62 | sleep(2) # waiting is mandatory (do not remove) 63 | buttons = self.browser.find_elements(*CartPageLocators.REMOVE_BUTTON) 64 | -------------------------------------------------------------------------------- /docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | networks: 4 | iced-latte-network-qa: 5 | name: iced-latte-network-qa 6 | attachable: true 7 | 8 | volumes: 9 | pg_data: 10 | nginx_data: 11 | minio_data: 12 | backend_logs: {} 13 | 14 | services: 15 | iced-latte-backend-qa: 16 | image: 'zufarexplainedit/iced-latte-backend:${DOCKER_IMAGE_TAG}' 17 | container_name: iced-latte-backend-qa 18 | environment: 19 | APP_SERVER_PORT: 8083 20 | APP_JWT_SECRET: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 21 | APP_JWT_REFRESH_SECRET: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 22 | DATASOURCE_PORT: 5432 23 | DATASOURCE_NAME: testdb 24 | DATASOURCE_USERNAME: postgres 25 | DATASOURCE_PASSWORD: postgres 26 | DATASOURCE_HOST: iced-latte-postgresdb-qa 27 | REDIS_HOST: iced-latte-redis-qa 28 | REDIS_PORT: 6380 29 | AWS_DEFAULT_PRODUCT_IMAGES_PATH: ./products 30 | AWS_ACCESS_KEY: vbfgngfdndgndgndgndgndgndgndg 31 | AWS_SECRET_KEY: vbfgngfdndgndgndgndgndgndgndg 32 | AWS_REGION: eu-west-1 33 | AWS_PRODUCT_BUCKET: products 34 | AWS_USER_BUCKET: users 35 | GOOGLE_AUTH_CLIENT_ID: vbfgngfdndgndgndgndgndgndgndg 36 | GOOGLE_AUTH_CLIENT_SECRET: vbfgngfdndgndgndgndgndgndgndg 37 | GOOGLE_AUTH_REDIRECT_URI: vbfgngfdndgndgndgndgndgndgndg 38 | ports: 39 | - '8083:8083' 40 | - '5005:5005' 41 | networks: 42 | - iced-latte-network-qa 43 | depends_on: 44 | - iced-latte-postgresdb-qa 45 | volumes: 46 | - backend_logs:/usr/app/logs 47 | - ./products:/usr/app/products 48 | restart: on-failure 49 | 50 | iced-latte-postgresdb-qa: 51 | image: 'postgres:13.11-bullseye' 52 | container_name: iced-latte-postgresdb-qa 53 | environment: 54 | - POSTGRES_USER=postgres 55 | - POSTGRES_PASSWORD=postgres 56 | - POSTGRES_DB=testdb 57 | ports: 58 | - '5432:5432' 59 | networks: 60 | - iced-latte-network-qa 61 | volumes: 62 | - pg_data:/var/lib/postgresql/data 63 | healthcheck: 64 | test: [ "CMD-SHELL", "pg_isready -U postgres" ] 65 | interval: 30s 66 | timeout: 10s 67 | retries: 5 68 | restart: on-failure 69 | 70 | iced-latte-redis: 71 | image: redis/redis-stack:latest 72 | container_name: iced-latte-redis-qa 73 | networks: 74 | - iced-latte-network-qa 75 | environment: 76 | - REDIS_HOST=localhost 77 | - REDIS_PORT=6380 78 | ports: 79 | - "6380:6380" -------------------------------------------------------------------------------- /framework/asserts/assert_favorite.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any, Dict 2 | 3 | from hamcrest import assert_that as ham_assert_that, has_item 4 | from assertpy import assert_that as assertpy_assert_that 5 | from requests import Response 6 | 7 | 8 | def assert_added_product_in_favorites( 9 | response: Response, product_add_to_favorite: list[str] 10 | ): 11 | """Asserts that the given products were added to user favorites. 12 | 13 | Args: 14 | response: The response from API request. 15 | product_add_to_favorite: A list of product IDs added to favorites. 16 | 17 | Raises: 18 | ValueError: If the response JSON is invalid. 19 | AssertionError: If any of the given product IDs are not found in the response. 20 | """ 21 | try: 22 | response_data = response.json() 23 | except ValueError: 24 | return {"error": "Invalid JSON response"} 25 | 26 | data = response_data["products"] 27 | if not data: 28 | raise ValueError("Response does not contain 'products' or it's empty") 29 | 30 | list_id_from_data = [d["id"] for d in data if "id" in d] 31 | for item in product_add_to_favorite: 32 | ham_assert_that( 33 | list_id_from_data, has_item(item), f"ID '{item}' not found in the response" 34 | ) 35 | 36 | 37 | def assert_id_key_and_its_value_is_not_empty_in_response( 38 | response: Response, 39 | ) -> Union[Dict[str, str], None]: 40 | """Asserts product IDs exist and are not empty in response. 41 | 42 | Checks that the JSON response contains an "id" key for each product 43 | and that the id values are not empty. Raises error if issues found. 44 | 45 | Args: 46 | response: Response object from API request 47 | 48 | Returns: 49 | None if the assertions pass, or a dictionary with error information if the JSON is invalid. 50 | 51 | Raises: 52 | ValueError: If JSON response is invalid or does not meet expectations. 53 | """ 54 | 55 | try: 56 | response_data = response.json() 57 | except ValueError: 58 | return {"error": "Invalid JSON response"} 59 | 60 | favorite_product_info = response_data.get("products") 61 | 62 | if not favorite_product_info: 63 | raise ValueError("Response does not contain 'products' or it's empty") 64 | 65 | for product in favorite_product_info: 66 | assertpy_assert_that(product).contains_key("id").described_as( 67 | "Response does not contain an 'id' key" 68 | ) 69 | assertpy_assert_that(product["id"]).is_not_empty().described_as( 70 | "Product 'id' has no value" 71 | ) 72 | -------------------------------------------------------------------------------- /framework/endpoints/favorite_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional, Union 3 | 4 | import requests 5 | from requests import Response 6 | 7 | from configs import HOST 8 | from framework.asserts.common import assert_status_code, assert_content_type 9 | from framework.tools.logging_allure import log_request 10 | 11 | 12 | class FavoriteAPI: 13 | def __init__(self): 14 | """Initializing parameters for request""" 15 | self.url = HOST + "/api/v1/favorites" 16 | self.headers = {"Content-Type": "application/json"} 17 | 18 | def add_favorites( 19 | self, token: str, favorite_product: list[str], expected_status_code: int = 200 20 | ) -> Response: 21 | """Add product to favorites 22 | 23 | Args: 24 | favorite_product: list product/products to add to favorites 25 | expected_status_code: expected http status code from response 26 | token: JWT token for authorization of request 27 | """ 28 | data = {"productIds": favorite_product} 29 | 30 | headers = self.headers 31 | headers["Authorization"] = f"Bearer {token}" 32 | path = self.url 33 | response = requests.post(url=path, data=json.dumps(data), headers=headers) 34 | assert_status_code(response, expected_status_code=expected_status_code) 35 | log_request(response) 36 | 37 | return response 38 | 39 | def get_favorites(self, token: str, expected_status_code: int = 200) -> Response: 40 | """Getting info about user's shopping cart 41 | 42 | Args: 43 | expected_status_code: expected http status code from response 44 | token: JWT token for authorization of request 45 | """ 46 | 47 | headers = self.headers 48 | headers["Authorization"] = f"Bearer {token}" 49 | path = self.url 50 | response = requests.get(url=path, headers=headers) 51 | assert_status_code(response, expected_status_code=expected_status_code) 52 | log_request(response) 53 | 54 | return response 55 | 56 | def delete_favorites( 57 | self, token: str, id_product: str, expected_status_code: int = 200 58 | ) -> Response: 59 | """Add product to favorites 60 | 61 | Args: 62 | id_product: product to delete from favorite list 63 | expected_status_code: expected http status code from response 64 | token: JWT token for authorization of request 65 | """ 66 | 67 | headers = self.headers 68 | headers["Authorization"] = f"Bearer {token}" 69 | path = f"{self.url}/{id_product}" 70 | response = requests.delete(url=path, headers=headers) 71 | assert_status_code(response, expected_status_code=expected_status_code) 72 | log_request(response) 73 | 74 | return response 75 | -------------------------------------------------------------------------------- /tests/favorite/test_add_product_to_favorite.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, empty, none, any_of, is_ 4 | 5 | from framework.asserts.assert_favorite import assert_added_product_in_favorites 6 | from framework.asserts.common import assert_content_type 7 | from framework.endpoints.favorite_api import FavoriteAPI 8 | from framework.endpoints.product_api import ProductAPI 9 | from framework.tools.favorite_methods import extract_random_product_ids 10 | 11 | 12 | @pytest.mark.critical 13 | @feature("Adding product to favorite ") 14 | class TestFavorite: 15 | @title("Test add products to favorite") 16 | @description( 17 | "GIVEN user is registered and does not have favorite list" 18 | "WHEN user add a products to favorite" 19 | "THEN status HTTP CODE = 200 and response body contains added product to favorite" 20 | ) 21 | def test_adding_products_to_favorite(self, create_authorized_user): 22 | with step("Registration of user"): 23 | user, token = ( 24 | create_authorized_user["user"], 25 | create_authorized_user["token"], 26 | ) 27 | 28 | with step("Verify that user does not have favorite list"): 29 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 30 | favorite_product_info = response_get_favorites.json().get("products") 31 | assert_that(favorite_product_info, any_of(is_(none()), empty())) 32 | 33 | with step("Getting all products via API"): 34 | response_get_product = ProductAPI().get_all() 35 | 36 | with step("Select and add random products to favorite"): 37 | product_list_add_to_favorite = extract_random_product_ids( 38 | response_get_product, product_quantity=4 39 | ) 40 | response_add_to_favorite = FavoriteAPI().add_favorites( 41 | token=token, favorite_product=product_list_add_to_favorite 42 | ) 43 | 44 | with step("Verify that response contain info about added products to favorite"): 45 | assert_added_product_in_favorites( 46 | response_add_to_favorite, product_list_add_to_favorite 47 | ) 48 | 49 | with step("Verify content-type"): 50 | assert_content_type(response_add_to_favorite, "application/json") 51 | 52 | with step( 53 | "Get info about favorite products after add products to favorite list " 54 | ): 55 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 56 | 57 | with step("Verify that response contain info about added products to favorite"): 58 | assert_added_product_in_favorites( 59 | response_get_favorites, product_list_add_to_favorite 60 | ) 61 | -------------------------------------------------------------------------------- /tests/favorite/test_add_product_to_favorite_negative.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, empty, none, any_of, is_ 4 | 5 | from framework.asserts.assert_favorite import assert_added_product_in_favorites 6 | from framework.asserts.common import assert_content_type 7 | from framework.endpoints.favorite_api import FavoriteAPI 8 | from framework.endpoints.product_api import ProductAPI 9 | from framework.tools.favorite_methods import extract_random_product_ids 10 | 11 | 12 | @feature("Adding product to favorite ") 13 | class TestFavorite: 14 | @pytest.mark.critical 15 | # @pytest.mark.skip( 16 | # reason="BUG, user add product that not exist in BD or empty product's list [], status code should be = 400" 17 | # ) 18 | @title("Test add products to favorite negative") 19 | @description( 20 | "GIVEN user is registered and does not have favorite list" 21 | "WHEN user add a products with incorrect/not exist id to favorite" 22 | "THEN status HTTP CODE = " 23 | ) 24 | @pytest.mark.parametrize( 25 | "id_product_add_to_favorite, expected_status_code", 26 | [ 27 | pytest.param(["1234_8900_88900_OPPIU"], 400), 28 | pytest.param(["12324456678888"], 400), 29 | pytest.param(["ytyty8888GGG-jgjjggj6666-555GGGhhhjj"], 400), 30 | pytest.param(["#$$%^&*@##$$%%^^&&&&&&*"], 400), 31 | pytest.param(["fc88cd5d-5049-4b04-OPAA-df1d974a3ce1"], 400), 32 | ], 33 | ) 34 | def test_adding_product_with_incorrect_id_format_to_favorite( 35 | self, create_authorized_user, id_product_add_to_favorite, expected_status_code 36 | ): 37 | with step("Registration of user"): 38 | user, token = ( 39 | create_authorized_user["user"], 40 | create_authorized_user["token"], 41 | ) 42 | 43 | with step("Verify that user does not have favorite list"): 44 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 45 | favorite_product_info = response_get_favorites.json().get("products") 46 | assert_that(favorite_product_info, any_of(is_(none()), empty())) 47 | 48 | with step("Add product with incorrect id product to favorite"): 49 | product_list_add_to_favorite = id_product_add_to_favorite 50 | FavoriteAPI().add_favorites( 51 | token=token, 52 | favorite_product=product_list_add_to_favorite, 53 | expected_status_code=expected_status_code, 54 | ) 55 | 56 | with step("Verify that user's favorite 'products' list = null"): 57 | favorite_product_info = response_get_favorites.json().get("products") 58 | assert_that(favorite_product_info, any_of(is_(none()), empty())) 59 | -------------------------------------------------------------------------------- /tests/cart/test_add_item_to_cart_negative.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, has_length, has_key 4 | 5 | from data.data_for_cart import id_product_not_exist_in_BD_for_adding_to_cart 6 | from framework.asserts.assert_cart import assert_added_product_not_in_api_response 7 | from framework.asserts.common import assert_content_type 8 | from framework.endpoints.cart_api import CartAPI 9 | from framework.endpoints.users_api import UsersAPI 10 | 11 | 12 | @feature("Adding item(product) to cart ") 13 | class TestCart: 14 | @pytest.mark.critical 15 | # @pytest.mark.skip( 16 | # reason="BUG, user add product that not exist in BD, status code should be = 400, actual = 200" 17 | # ) 18 | @title("Test add item(product) to new user's cart negative") 19 | @description( 20 | "GIVEN user is registered and does not have shopping cart" 21 | "WHEN user add the items that not exist in BD" 22 | "THEN response HTTP CODE = 200 and user's shopping cart is not contained this item" 23 | ) 24 | def test_adding_not_exist_item_to_cart(self, create_authorized_user): 25 | with step("Registration of user"): 26 | user, token = ( 27 | create_authorized_user["user"], 28 | create_authorized_user["token"], 29 | ) 30 | 31 | with step("Getting user's info via API"): 32 | getting_user_response = UsersAPI().get_user(token=token) 33 | new_user_id = getting_user_response.json()["id"] 34 | 35 | with step( 36 | "Get user's shopping cart and verify that user doesn't have items in shopping cart" 37 | ): 38 | response_get_cart = CartAPI().get_user_cart(token=token) 39 | data = response_get_cart.json() 40 | assert_that(data, has_key("items")) 41 | assert_that(data["items"], has_length(0)) 42 | 43 | with step("Generating data to add to the shopping cart"): 44 | product_add_to_cart = id_product_not_exist_in_BD_for_adding_to_cart 45 | 46 | with step("Add not exist product to the shopping cart "): 47 | response_add_to_cart = CartAPI().add_item_to_cart( 48 | token=token, items=product_add_to_cart, expected_status_code=200 49 | ) 50 | 51 | with step("Verify,response does not contain not exist product."): 52 | assert_added_product_not_in_api_response( 53 | response_add_to_cart, product_add_to_cart 54 | ) 55 | 56 | with step("Verify,not exist product was not added to the cart."): 57 | response_get_cart_after_add = CartAPI().get_user_cart(token=token) 58 | assert_added_product_not_in_api_response( 59 | response_get_cart_after_add, product_add_to_cart 60 | ) 61 | assert_content_type(response_get_cart, "application/json") 62 | -------------------------------------------------------------------------------- /UI/pages/EditProfilePage.py: -------------------------------------------------------------------------------- 1 | from .BasePage import BasePage 2 | from .locators import EditProfilePageLocators 3 | 4 | 5 | class EditProfilePage(BasePage): 6 | def change_email(self, new_email): 7 | email_field = self.browser.find_element(*EditProfilePageLocators.EMAIL_FIELD) 8 | email_field.clear() 9 | email_field.send_keys(new_email) 10 | 11 | def change_first_name(self, new_first_name): 12 | first_name_field = self.browser.find_element(*EditProfilePageLocators.FIRST_NAME_FIELD) 13 | first_name_field.clear() 14 | first_name_field.send_keys(new_first_name) 15 | 16 | def change_last_name(self, new_last_name): 17 | last_name_field = self.browser.find_element(*EditProfilePageLocators.LAST_NAME_FIELD) 18 | last_name_field.clear() 19 | last_name_field.send_keys(new_last_name) 20 | 21 | def save_change(self): 22 | save_change_button = self.browser.find_element(*EditProfilePageLocators.SAVE_CHANGE_BUTTON) 23 | save_change_button.click() 24 | 25 | def is_error_message_first_name_present(self, error_message): 26 | try: 27 | message_empty = self.browser.find_element(*EditProfilePageLocators.EMPTY_FIRST_NAME_MESSAGE).text 28 | except: 29 | message_empty = '' 30 | try: 31 | message_nonlatin = self.browser.find_element(*EditProfilePageLocators.NONLATIN_FIRST_NAME_MESSAGE).text 32 | except: 33 | message_nonlatin = '' 34 | try: 35 | message_server_error = self.browser.find_element(*EditProfilePageLocators.SERVER_ERROR_MESSAGE).text 36 | except: 37 | message_server_error = '' 38 | if (message_empty in error_message) or \ 39 | (message_nonlatin in error_message) or \ 40 | (message_server_error in error_message): 41 | return True 42 | else: 43 | return False 44 | 45 | def is_error_message_last_name_present(self, error_message): 46 | try: 47 | message_empty = self.browser.find_element(*EditProfilePageLocators.EMPTY_LAST_NAME_MESSAGE).text 48 | except: 49 | message_empty = '' 50 | try: 51 | message_nonlatin = self.browser.find_element(*EditProfilePageLocators.NONLATIN_LAST_NAME_MESSAGE).text 52 | except: 53 | message_nonlatin = '' 54 | try: 55 | message_server_error = self.browser.find_element(*EditProfilePageLocators.SERVER_ERROR_MESSAGE).text 56 | except: 57 | message_server_error = '' 58 | if (message_empty in error_message) or \ 59 | (message_nonlatin in error_message) or \ 60 | (message_server_error in error_message): 61 | return True 62 | else: 63 | return False 64 | 65 | 66 | ''' def is_success_message_present(self, success_message): 67 | message_element = self.browser.find_element(*EditProfilePageLocators.SUCCESS_MESSAGE) 68 | if message_element.text == success_message: 69 | return True 70 | else: 71 | return False 72 | ''' 73 | -------------------------------------------------------------------------------- /tests/favorite/test_get_favorite_products_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, none, empty, any_of, is_, is_not 4 | 5 | from framework.asserts.assert_favorite import ( 6 | assert_id_key_and_its_value_is_not_empty_in_response, 7 | ) 8 | from framework.asserts.common import assert_content_type 9 | from framework.endpoints.favorite_api import FavoriteAPI 10 | 11 | 12 | @feature("Get info about favorite products ") 13 | class TestFavorite: 14 | @pytest.mark.critical 15 | @title("Test get info about not created list of favorite products") 16 | @description( 17 | "GIVEN user is registered and does not have favorite products list" 18 | "WHEN user get info about favorite products list" 19 | "THEN status HTTP CODE = 200 and response body contains info about favorite 'products' list = null" 20 | ) 21 | def test_getting_info_about_not_created_favorite_products_list( 22 | self, create_authorized_user 23 | ): 24 | with step("Registration of user"): 25 | user, token = ( 26 | create_authorized_user["user"], 27 | create_authorized_user["token"], 28 | ) 29 | 30 | with step("Get info about favorite products"): 31 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 32 | 33 | with step("Verify that user's favorite 'products' list = null"): 34 | favorite_product_info = response_get_favorites.json().get("products") 35 | assert_that(favorite_product_info, any_of(is_(none()), empty())) 36 | 37 | with step("Verify content-type"): 38 | assert_content_type(response_get_favorites, "application/json") 39 | 40 | @pytest.mark.critical 41 | @pytest.mark.parametrize("product_quantity", [4], indirect=True) 42 | @title("Test get info about created list of favorite products") 43 | @description( 44 | "GIVEN user is registered and has favorite products list.len=2" 45 | "WHEN user get info about favorite products list" 46 | "THEN status HTTP CODE = 200 and response body contains info about favorite products list" 47 | ) 48 | def test_getting_info_about_created_favorite_products_list( 49 | self, add_product_to_favorite_list 50 | ): 51 | with step("Add random product to the favorite list"): 52 | ( 53 | token, 54 | _, 55 | ) = add_product_to_favorite_list 56 | 57 | with step("Get info about favorite products list"): 58 | response_get_favorites = FavoriteAPI().get_favorites(token=token) 59 | 60 | with step("Verify that user's favorite products list not None"): 61 | favorite_product_list_info = response_get_favorites.json().get("products") 62 | assert_that(favorite_product_list_info, any_of(is_not(none()), empty())) 63 | assert_id_key_and_its_value_is_not_empty_in_response(response_get_favorites) 64 | 65 | with step("Verify content-type"): 66 | assert_content_type(response_get_favorites, "application/json") 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Iced Latte QA 2 | [![Total Lines of Code](https://tokei.rs/b1/github/Sunagatov/Iced-Latte-QA?category=lines)](https://github.com/Sunagatov/Iced-Latte-QA) 3 | 4 | Automated tests in Python for project Iced Latte -> https://github.com/Sunagatov/Iced-Latte/ 5 | 6 | ## Instruction 7 | For running tests should be created configs.py in root directory of format: 8 | ```Python 9 | HOST = '' 10 | HOST_DB = '' 11 | PORT_DB = '' 12 | DB_NAME = '' 13 | DB_USER = '' 14 | DB_PASS = '' 15 | DEFAULT_PASSWORD = '' 16 | JWT_SECRET = '' 17 | ``` 18 | ## Start Local Iced-Latte Backend 19 | 20 | ```sh 21 | ./start_be.sh 22 | ``` 23 | 24 | or 25 | 26 | ```sh 27 | ./start_be.sh [tag] 28 | # for example 29 | ./start_be.sh development-bf1ba24 30 | ``` 31 | 32 | > Notes: 33 | > * optional parameter is a tag from the Docker Hub for [BE image](https://hub.docker.com/r/zufarexplainedit/iced-latte-backend/tags), default is the latest tag 34 | > * it might be necessary to make script executable before the first run `chmod +x ./start_be.sh` 35 | > * the script will pull the specified version of BE image and start BE, Postgres and Minio 36 | > * if the tag on DockerHub is different from the latest commit on BE `development` branch, the script will print warning 37 | > * periodically clean up the system by running [`docker rm`](https://docs.docker.com/engine/reference/commandline/container_rm/) 38 | 39 | Swagger will be available here [http://localhost:8083/api/docs/swagger-ui/index.html](http://localhost:8083/api/docs/swagger-ui/index.html). 40 | 41 | To check the logs use: 42 | 43 | ```sh 44 | docker-compose -f docker-compose.local.yml logs --tail 500 45 | ``` 46 | 47 | ## Report 48 | (!) BE SURE TO INSTALL ALLURE -> https://allurereport.org/docs/gettingstarted/installation/ 49 | 50 | To get the Allure report on the local computer, follow these steps in root directory: 51 | ```bash 52 | python -m pytest ./tests --alluredir=allure_report --clean-alluredir 53 | allure serve allure_report 54 | ``` 55 | 56 | ## Pre-commit hooks 57 | For running pre-commit hooks should be installed pre-commit -> https://pre-commit.com/#install 58 | ```bash 59 | pre-commit install 60 | ``` 61 | To run pre-commit hooks, execute next command before commit: 62 | ```bash 63 | pre-commit run --all-files 64 | ``` 65 | 66 | ## Database Navigator 67 | 68 | > For Ultimate Edition consider using [Database Tools and SQL plugin](https://www.jetbrains.com/help/idea/relational-databases.html) 69 | 70 | Install Database Navigator Plugin: 71 | 72 | 1. double click **Shift** 73 | 2. type **Plugins** 74 | 3. type **Database Navigator** > click **Install** 75 | 4. navigate to **View** > **Tool Windows** > **DB Browser** 76 | 5. click New Connection 77 | 6. select PostgesSQL 78 | 79 | Go to **View** > **Tool Windows** > **DB Browser**. 80 | 81 | Add new PostgresSQL connection: 82 | * Host `127.0.0.1` 83 | * Database `testdb` 84 | * User `postgres` 85 | * Password `postgres` 86 | 87 | ![](db_navigator_setup.png) 88 | 89 | Enjoy! 90 | 91 | ![](db_navigator.png) 92 | -------------------------------------------------------------------------------- /tests/cart/test_delete_items_from_cart.py: -------------------------------------------------------------------------------- 1 | from allure import description, step, title, feature 2 | 3 | from framework.asserts.assert_cart import assert_deleted_item_ids_in_response 4 | from framework.asserts.common import assert_content_type 5 | from framework.endpoints.cart_api import CartAPI 6 | from framework.tools.methods_to_cart import get_item_id 7 | 8 | 9 | @feature("Deleting item/items from cart ") 10 | class TestCart: 11 | @title("Test deleting ALL items from new user's cart") 12 | @description( 13 | "GIVEN user is registered and has shopping cart" 14 | "WHEN user delete ALL items from cart" 15 | "THEN status HTTP CODE = 200" 16 | ) 17 | def test_deleting_items_from_cart( 18 | self, creating_and_adding_product_to_shopping_cart 19 | ): 20 | with step( 21 | "Registration new user, and add product to the shopping cart" 22 | "Getting info about user and user's shopping cart " 23 | ): 24 | ( 25 | token, 26 | new_user_id, 27 | response_get_cart_after_added, 28 | ) = creating_and_adding_product_to_shopping_cart 29 | 30 | with step("Generating data for delete"): 31 | item_to_delete_more_than_one = get_item_id(response_get_cart_after_added) 32 | response_delete_item = CartAPI().delete_item_from_cart( 33 | token=token, cart_item_id=item_to_delete_more_than_one 34 | ) 35 | 36 | with step("Checking the response and Content-Type"): 37 | assert_content_type(response_delete_item, "application/json") 38 | 39 | with step("Verify that deleted items do not exist in shopping cart"): 40 | assert_deleted_item_ids_in_response( 41 | response_delete_item, item_to_delete_more_than_one 42 | ) 43 | 44 | @title("Test deleting item from new user's cart") 45 | @description( 46 | "GIVEN user is registered and does not have shopping cart" 47 | "WHEN user delete item from the cart" 48 | "THEN status HTTP CODE = 200" 49 | ) 50 | def test_deleting_one_item_from_cart( 51 | self, creating_and_adding_product_to_shopping_cart 52 | ): 53 | with step( 54 | "Registration new user, and add product to the shopping cart" 55 | "Getting info about user and user's shopping cart " 56 | ): 57 | ( 58 | token, 59 | new_user_id, 60 | response_get_cart_after_added, 61 | ) = creating_and_adding_product_to_shopping_cart 62 | 63 | with step("Generating data for delete one product from cart"): 64 | id_one_item_to_delete = [ 65 | response_get_cart_after_added.json()["items"][0]["id"] 66 | ] 67 | 68 | with step("Deleting item from cart"): 69 | response_delete_item = CartAPI().delete_item_from_cart( 70 | token=token, cart_item_id=id_one_item_to_delete 71 | ) 72 | assert_content_type(response_delete_item, "application/json") 73 | 74 | with step("Verify that deleted items do not exist in shopping cart"): 75 | assert_deleted_item_ids_in_response( 76 | response_delete_item, id_one_item_to_delete 77 | ) 78 | -------------------------------------------------------------------------------- /tests/cart/test_add_item_to_cart_positive.py: -------------------------------------------------------------------------------- 1 | from allure import description, step, title, feature 2 | from hamcrest import assert_that, has_length, equal_to, has_key 3 | 4 | from data.data_for_cart import data_for_adding_product_to_cart 5 | from framework.asserts.common import assert_content_type 6 | from framework.endpoints.cart_api import CartAPI 7 | from framework.endpoints.users_api import UsersAPI 8 | from framework.tools.methods_to_cart import ( 9 | get_product_info, 10 | assert_product_to_add_matches_response, 11 | ) 12 | 13 | 14 | @feature("Adding items to cart ") 15 | class TestCart: 16 | @title("Test add items to new user's cart") 17 | @description( 18 | "GIVEN user is registered and does not have any item in shopping cart" 19 | "WHEN user add the items to the cart" 20 | "THEN status HTTP CODE = 200 and response body contains added item" 21 | ) 22 | def test_add_item_to_cart(self, create_authorized_user): 23 | with step("Registration of user"): 24 | user, token = ( 25 | create_authorized_user["user"], 26 | create_authorized_user["token"], 27 | ) 28 | 29 | with step("Get user's info via API"): 30 | response_get_user_info = UsersAPI().get_user(token=token) 31 | new_user_id = response_get_user_info.json()["id"] 32 | 33 | with step( 34 | "Get user's shopping cart and verify that user doesn't have items in shopping cart" 35 | ): 36 | response_get_cart = CartAPI().get_user_cart(token=token) 37 | data = response_get_cart.json() 38 | assert_that(data, has_key("items")) 39 | assert_that(data["items"], has_length(0)) 40 | 41 | with step("Add product to the shopping cart "): 42 | product_add_to_cart = data_for_adding_product_to_cart 43 | 44 | response_add_to_cart = CartAPI().add_item_to_cart( 45 | token=token, items=product_add_to_cart 46 | ) 47 | 48 | with step("Verify product to add in response after API request"): 49 | product_list_in_response_add_to_cart = get_product_info( 50 | response=response_add_to_cart 51 | ) 52 | assert_product_to_add_matches_response( 53 | product_add_to_cart, product_list_in_response_add_to_cart 54 | ) 55 | 56 | with step( 57 | "Verify the shopping cart created under new user/response body contain correct user's id)." 58 | ): 59 | response_get_cart_after_add_product = CartAPI().get_user_cart(token=token) 60 | expected_user_id_in_cart = response_get_cart_after_add_product.json()[ 61 | "userId" 62 | ] 63 | assert_that( 64 | new_user_id, 65 | equal_to(expected_user_id_in_cart), 66 | "Expected user ID does not match.", 67 | ) 68 | 69 | with step("Verify added products are in a shopping cart"): 70 | product_list_in_cart_after_add = get_product_info( 71 | response=response_get_cart_after_add_product 72 | ) 73 | assert_product_to_add_matches_response( 74 | product_add_to_cart, product_list_in_cart_after_add 75 | ) 76 | assert_content_type(response_get_cart, "application/json") 77 | -------------------------------------------------------------------------------- /tests/registration/test_confirm_email_negative.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature, severity 3 | 4 | from configs import ( 5 | email_iced_late, 6 | imap_server, 7 | email_address_to_connect, 8 | gmail_password, 9 | firstName, 10 | lastName, 11 | password, 12 | ) 13 | from data.data_format_token import incorrect_format_token 14 | from framework.asserts.common import assert_message_in_response, assert_response_message 15 | from framework.endpoints.authenticate_api import AuthenticateAPI 16 | 17 | 18 | @feature("Confirm email for registration with code, negative scenario") 19 | class TestConfirmationEmail: 20 | @pytest.mark.medium 21 | @severity(severity_level="MAJOR") 22 | @title("Test confirmation email for user's registration with empty code") 23 | @description( 24 | "WHEN the user submits the empty code for confirmation email for registration" 25 | "THEN status HTTP CODE = 400 and error message" 26 | ) 27 | @pytest.mark.parametrize( 28 | "code, expected_status_code, expected_message_part", incorrect_format_token 29 | ) 30 | def test_confirmation_email_for_registration_with_empty_and_invalid_format_code( 31 | self, code, expected_status_code, expected_message_part 32 | ): 33 | with step("Confirm email for registration with empty code"): 34 | response_confirmation = AuthenticateAPI().confirmation_email( 35 | code=code, expected_status_code=400 36 | ) 37 | 38 | assert_message_in_response( 39 | response=response_confirmation, expected_message=expected_message_part 40 | ) 41 | # assert_response_message(response=response_confirmation, expected_message=expected_message) 42 | 43 | @pytest.mark.critical 44 | @severity(severity_level="MAJOR") 45 | @title("Test confirmation email for registration with already used code") 46 | @description( 47 | "WHEN the user submits for confirmation email already used code" 48 | "THEN status HTTP CODE = 404 and error message" 49 | ) 50 | @pytest.mark.parametrize( 51 | "registration_and_cleanup_user_through_api", 52 | [ 53 | { 54 | "firstName": firstName, 55 | "lastName": lastName, 56 | "password": password, 57 | "email_iced_late": email_iced_late, 58 | "imap_server": imap_server, 59 | "email_address_to_connect": email_address_to_connect, 60 | "gmail_password": gmail_password, 61 | } 62 | ], 63 | indirect=True, 64 | ) 65 | def test_confirmation_email_with_used_code( 66 | self, registration_and_cleanup_user_through_api 67 | ): 68 | with step("Registration user"): 69 | code_from_email = registration_and_cleanup_user_through_api["code"] 70 | 71 | with step("Confirm email for registration with already used code"): 72 | code_already_used = code_from_email 73 | response_confirmation = AuthenticateAPI().confirmation_email( 74 | code=code_already_used, expected_status_code=400 75 | ) 76 | expected_message = "Incorrect token" 77 | assert_response_message( 78 | response=response_confirmation, expected_message=expected_message 79 | ) 80 | -------------------------------------------------------------------------------- /tests/authentication/test_authentication_blank_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest.core import assert_that, is_ 4 | from hamcrest.library import contains_string 5 | 6 | from framework.endpoints.authenticate_api import AuthenticateAPI 7 | from framework.tools.generators import generate_user_data 8 | from framework.tools.matcher import is_timestamp_valid 9 | from tests.conftest import create_authorized_user 10 | 11 | TIMESTAMP_PATTERN = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}" 12 | 13 | 14 | @feature("Authentication of user") 15 | class TestAuthentication: 16 | @title("Test authentication, negative scenario") 17 | @description( 18 | "GIVEN user is registered" 19 | "WHEN fields password, email are blank " 20 | "THEN status HTTP CODE = 400" 21 | ) 22 | @pytest.mark.parametrize( 23 | "email, password, expected_status_code, expected_message_part", 24 | [ 25 | pytest.param( 26 | " ", 27 | " ", 28 | 400, 29 | "Email is the mandatory attribute", 30 | ), 31 | pytest.param( 32 | None, 33 | " ", 34 | 400, 35 | "Password is the mandatory attribute", 36 | ), 37 | pytest.param(" ", None, 400, "Email is the mandatory attribute"), 38 | ], 39 | ) 40 | def test_authentication_with_various_blank_inputs( 41 | self, 42 | email: str, 43 | password: str, 44 | expected_status_code: int, 45 | expected_message_part: str, 46 | create_authorized_user, 47 | ): 48 | with step("Registration of user"): 49 | user, token = ( 50 | create_authorized_user["user"], 51 | create_authorized_user["token"], 52 | ) 53 | with step("Preparation data for authentication request"): 54 | email = user["email"] if email is None else email 55 | password = user["password"] if password is None else password 56 | 57 | with step("Authentication user with given credentials"): 58 | response = AuthenticateAPI().authentication( 59 | email=email, password=password, expected_status_code=400 60 | ) 61 | 62 | with step( 63 | "Verify the response contains the expected part of message, http status code,and " 64 | "correct format of the timestamp" 65 | ): 66 | response_body = response.json() 67 | actual_status_code = response.status_code 68 | actual_message = response_body.get("message", "") 69 | actual_timestamp = response_body.get("timestamp", "") 70 | 71 | assert_that( 72 | actual_message, 73 | contains_string(expected_message_part), 74 | reason=f"Expected response contains '{expected_message_part}', found: '{actual_message}'", 75 | ) 76 | assert_that( 77 | response.status_code, 78 | is_(expected_status_code), 79 | reason=f"Expected HTTP status code '{expected_status_code}', found: '{actual_status_code}'", 80 | ) 81 | assert_that( 82 | is_timestamp_valid(actual_timestamp, TIMESTAMP_PATTERN), 83 | reason=f"Timestamp '{actual_timestamp}' does not match the expected format YYYY-MM-DD HH:MM:SS", 84 | ) 85 | -------------------------------------------------------------------------------- /framework/clients/db_client_ssh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | from psycopg2 import connect 5 | from psycopg2.extras import RealDictCursor 6 | from sshtunnel import SSHTunnelForwarder 7 | 8 | 9 | class DBClient: 10 | def __init__( 11 | self, 12 | ssh_username: str, 13 | ssh_password: str, 14 | local_server_ip: str, 15 | db_username: str, 16 | db_password: str, 17 | database_name: str, 18 | remote_server_ip: str, 19 | port_ssh: int, 20 | ): 21 | """ 22 | Initializes a database connection tunnel via SSH. 23 | 24 | Creates an SSH tunnel to a remote server, binds to the remote PostgresSQL port, 25 | and starts the SSH server, then connects to a PostgresSQL database through the tunnel. 26 | 27 | Args: 28 | ssh_username: SSH server username. 29 | ssh_password: SSH server password. 30 | local_server_ip: IP address of local Postgres server. 31 | db_username: Database username. 32 | db_password: Database password. 33 | database_name: Name of database to connect. 34 | remote_server_ip: IP address of remote SSH server. 35 | port_ssh: SSH server port. 36 | """ 37 | # Start SSH tunnel 38 | self.server = SSHTunnelForwarder( 39 | (remote_server_ip, port_ssh), 40 | ssh_username=ssh_username, 41 | ssh_password=ssh_password, 42 | remote_bind_address=(local_server_ip, 5432), 43 | local_bind_address=("localhost", 0), # Automatically select a free port 44 | ) 45 | self.server.start() 46 | logging.info( 47 | f"SSH tunnel established. Local port: {self.server.local_bind_port}" 48 | ) 49 | 50 | # Connect to PostgresSQL through the tunnel 51 | self.conn = connect( 52 | host="localhost", 53 | port=self.server.local_bind_port, 54 | dbname=database_name, 55 | user=db_username, 56 | password=db_password, 57 | ) 58 | self.conn.autocommit = True 59 | self.cur = self.conn.cursor(cursor_factory=RealDictCursor) 60 | logging.info("Database connection established") 61 | 62 | def close(self) -> None: 63 | """Close the cursor, database connection, and SSH tunnel""" 64 | if self.cur: 65 | self.cur.close() 66 | logging.info("Cursor closed") 67 | if self.conn: 68 | self.conn.close() 69 | logging.info("Database connection closed") 70 | if self.server: 71 | self.server.stop() 72 | logging.info("SSH tunnel closed") 73 | 74 | # def execute(self, query: str) -> None: 75 | # """Execute a query to the Postgres database without returning data""" 76 | # logging.debug(f"Executing query: {query}") 77 | # self.cur.execute(query) 78 | # 79 | # def fetch_all(self, query: str) -> Optional[List[dict]]: 80 | # """Execute a query to the Postgres database and return data as a list of dictionaries""" 81 | # self.execute(query) 82 | # records = self.cur.fetchall() 83 | # return [dict(rec) for rec in records] if records else [] 84 | def fetch_all(self, query: str, params: tuple = ()) -> List[tuple]: 85 | with self.conn.cursor() as cursor: 86 | cursor.execute(query, params) 87 | return cursor.fetchall() 88 | 89 | def execute(self, query: str, params: tuple = ()) -> None: 90 | with self.conn.cursor() as cursor: 91 | cursor.execute(query, params) 92 | self.conn.commit() 93 | -------------------------------------------------------------------------------- /tests/authentication/test_authentication_invalid_credential.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, is_ 4 | 5 | from framework.asserts.common import assert_response_message 6 | from framework.endpoints.authenticate_api import AuthenticateAPI 7 | from framework.tools.generators import generate_user_data 8 | from tests.conftest import create_authorized_user 9 | 10 | 11 | @pytest.mark.critical 12 | @feature("Authentication of user") 13 | class TestAuthentication: 14 | @title("Test authentication, negative scenario") 15 | @description( 16 | "GIVEN user is registered" 17 | "WHEN user submit invalid password, email " 18 | "THEN status HTTP CODE = 401" 19 | ) 20 | def test_authentication_incorrect_password(self, create_authorized_user): 21 | with step("Registration of user"): 22 | user, token = ( 23 | create_authorized_user["user"], 24 | create_authorized_user["token"], 25 | ) 26 | 27 | with step("Authentication user with incorrect email"): 28 | data_post = { 29 | "email": user["email"], 30 | "password": user["password"] + "invalid", 31 | } 32 | response_authentication = AuthenticateAPI().authentication( 33 | email=data_post["email"], 34 | password=data_post["password"], 35 | expected_status_code=401, 36 | ) 37 | with step("Verify error message from response"): 38 | expected_message = f"Invalid credentials for user's account with email = '{data_post['email']}'" 39 | assert_response_message( 40 | response=response_authentication, expected_message=expected_message 41 | ) 42 | 43 | def test_authentication_incorrect_email(self, create_authorized_user): 44 | with step("Registration of user"): 45 | user, token = ( 46 | create_authorized_user["user"], 47 | create_authorized_user["token"], 48 | ) 49 | 50 | with step("Authentication user with incorrect email"): 51 | data_post = { 52 | "email": user["email"] + "_invalid", 53 | "password": user["password"], 54 | } 55 | response_authentication = AuthenticateAPI().authentication( 56 | email=data_post["email"], 57 | password=data_post["password"], 58 | expected_status_code=401, 59 | ) 60 | with step("Verify error message from response"): 61 | expected_message = f"Invalid credentials for user's account with email = '{data_post['email']}'" 62 | assert_response_message( 63 | response=response_authentication, expected_message=expected_message 64 | ) 65 | 66 | def test_authentication_incorrect_password_email(self, create_authorized_user): 67 | with step("Registration of user"): 68 | user, token = ( 69 | create_authorized_user["user"], 70 | create_authorized_user["token"], 71 | ) 72 | 73 | with step("Authentication user with incorrect email"): 74 | data_post = { 75 | "email": user["email"] + "_invalid", 76 | "password": user["password"] + "invalid", 77 | } 78 | response_authentication = AuthenticateAPI().authentication( 79 | email=data_post["email"], 80 | password=data_post["password"], 81 | expected_status_code=401, 82 | ) 83 | with step("Verify error message from response"): 84 | expected_message = f"Invalid credentials for user's account with email = '{data_post['email']}'" 85 | assert_response_message( 86 | response=response_authentication, expected_message=expected_message 87 | ) 88 | -------------------------------------------------------------------------------- /tests/cart/test_update_item_in_shopping_cart.py: -------------------------------------------------------------------------------- 1 | from allure import description, step, title, feature 2 | from hamcrest import assert_that, equal_to 3 | 4 | from framework.endpoints.cart_api import CartAPI 5 | from framework.tools.methods_to_cart import ( 6 | extract_random_item_detail, 7 | get_quantity_specific_cart_item, 8 | ) 9 | 10 | 11 | @feature("Updating item quantity in cart ") 12 | class TestCart: 13 | @title("Test updating item quantity in new user's cart") 14 | @description( 15 | "GIVEN user is registered and have shopping cart" 16 | "WHEN user update item quantity in the shopping cart" 17 | "THEN status HTTP CODE = 200 and response body that contain updated 'productQuantity' returns" 18 | ) 19 | def test_updating_increase_item_quantity_in_cart( 20 | self, creating_and_adding_product_to_shopping_cart 21 | ): 22 | with step( 23 | "Registration new user, and add product to the shopping cart" 24 | "Getting info about user and user's shopping cart " 25 | ): 26 | ( 27 | token, 28 | new_user_id, 29 | response_get_cart_after_added, 30 | ) = creating_and_adding_product_to_shopping_cart 31 | 32 | with step( 33 | "Getting random items to update from shopping cart and determination quantity for update " 34 | ): 35 | random_item_from_cart = extract_random_item_detail( 36 | response_get_cart_after_added 37 | ) 38 | item_id_to_update = random_item_from_cart["id"] 39 | item_quantity_before_update = random_item_from_cart["productQuantity"] 40 | quantity_to_update = 1 41 | 42 | with step("Update quantity of item"): 43 | response_after_update = CartAPI().update_quantity_product( 44 | token=token, item_id=item_id_to_update, item_quantity=quantity_to_update 45 | ) 46 | 47 | with step("Verify that quantity of item is updated"): 48 | actual_item_quantity_after_update = get_quantity_specific_cart_item( 49 | response_after_update, item_id_to_update 50 | ) 51 | expected_item_quantity_after_update = ( 52 | item_quantity_before_update + quantity_to_update 53 | ) 54 | assert_that( 55 | expected_item_quantity_after_update, 56 | equal_to(actual_item_quantity_after_update), 57 | ) 58 | 59 | def test_updating_decrease_item_quantity_in_cart( 60 | self, creating_and_adding_product_to_shopping_cart 61 | ): 62 | with step( 63 | "Registration new user, and add product to the shopping cart" 64 | "Getting info about user and user's shopping cart " 65 | ): 66 | ( 67 | token, 68 | new_user_id, 69 | response_get_cart_after_added, 70 | ) = creating_and_adding_product_to_shopping_cart 71 | 72 | with step( 73 | "Getting random items to update from shopping cart and determination quantity for update " 74 | ): 75 | random_item_from_cart = extract_random_item_detail( 76 | response_get_cart_after_added 77 | ) 78 | item_id_to_update = random_item_from_cart["id"] 79 | item_quantity_before_update = random_item_from_cart["productQuantity"] 80 | quantity_to_update = -1 81 | 82 | with step("Update quantity of item"): 83 | response_after_update = CartAPI().update_quantity_product( 84 | token=token, item_id=item_id_to_update, item_quantity=quantity_to_update 85 | ) 86 | 87 | with step("Verify that quantity of item is updated"): 88 | actual_item_quantity_after_update = get_quantity_specific_cart_item( 89 | response_after_update, item_id_to_update 90 | ) 91 | expected_item_quantity_after_update = ( 92 | item_quantity_before_update + quantity_to_update 93 | ) 94 | assert_that( 95 | expected_item_quantity_after_update, 96 | equal_to(actual_item_quantity_after_update), 97 | ) 98 | -------------------------------------------------------------------------------- /framework/endpoints/cart_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hamcrest import assert_that, is_ 3 | import requests 4 | from requests import Response 5 | 6 | from configs import HOST 7 | from framework.asserts.common import assert_status_code 8 | 9 | from framework.tools.logging_allure import log_request 10 | 11 | 12 | class CartAPI: 13 | def __init__(self): 14 | """Initializing parameters for request""" 15 | self.url = HOST + "/api/v1/cart" 16 | self.headers = {"Content-Type": "application/json"} 17 | 18 | def get_user_cart(self, token: str, expected_status_code: int = 200) -> Response: 19 | """Getting info about user's shopping cart 20 | 21 | Args: 22 | expected_status_code: expected http status code from response 23 | token: JWT token for authorization of request 24 | """ 25 | headers = self.headers 26 | headers["Authorization"] = f"Bearer {token}" 27 | path = self.url 28 | response = requests.get(url=path, headers=headers) 29 | assert_status_code(response, expected_status_code=expected_status_code) 30 | log_request(response) 31 | 32 | return response 33 | 34 | def update_quantity_product( 35 | self, 36 | token: str, 37 | item_id: str, 38 | item_quantity: int, 39 | expected_status_code: int = 200, 40 | ) -> Response: 41 | """Updating product's quantity 42 | 43 | Args: 44 | item_quantity: quantity to update 45 | item_id: item to update 46 | expected_status_code: expected http status code from response 47 | token: JWT token for authorization of request 48 | """ 49 | body = {"shoppingCartItemId": item_id, "productQuantityChange": item_quantity} 50 | headers = self.headers 51 | headers["Authorization"] = f"Bearer {token}" 52 | path = self.url + "/items" 53 | response = requests.patch(url=path, data=json.dumps(body), headers=headers) 54 | assert_status_code(response, expected_status_code=expected_status_code) 55 | log_request(response) 56 | return response 57 | 58 | def add_item_to_cart( 59 | self, token: str, items: list[object], expected_status_code: int = 200 60 | ) -> Response: 61 | """Adding multiple products to cart and verifying if they are in the response 62 | 63 | Args: 64 | items: A list of items where each item is a dictionary containing 'productId' and 'productQuantity' 65 | expected_status_code (int): Expected HTTP status code from response 66 | token (str): JWT token for authorization of request 67 | 68 | Example: 69 | items = [ 70 | {"productId": "123ffg-333-jjj78", "productQuantity": 2}, 71 | {"productId": "6788gg-uh8-hajj6", "productQuantity": 3} 72 | ] 73 | """ 74 | headers = self.headers 75 | headers["Authorization"] = f"Bearer {token}" 76 | path = self.url + "/items" 77 | body = {"items": items} 78 | response = requests.post(url=path, data=json.dumps(body), headers=headers) 79 | assert_status_code(response, expected_status_code=expected_status_code) 80 | log_request(response) 81 | return response 82 | 83 | def delete_item_from_cart( 84 | self, token: str, cart_item_id: list, expected_status_code: int = 200 85 | ) -> Response: 86 | """Deleting item from shopping cart 87 | 88 | Args: 89 | cart_item_id: data for deleting items from shopping cart with required fields 90 | expected_status_code: expected http status code from response 91 | token: JWT token for authorization of request 92 | """ 93 | 94 | body = {"shoppingCartItemIds": cart_item_id} 95 | headers = self.headers 96 | headers["Authorization"] = f"Bearer {token}" 97 | path = self.url + "/items" 98 | response = requests.delete(url=path, headers=headers, data=json.dumps(body)) 99 | assert_status_code(response, expected_status_code=expected_status_code) 100 | log_request(response) 101 | 102 | return response 103 | -------------------------------------------------------------------------------- /tests/authentication/test_authentication_limit.py: -------------------------------------------------------------------------------- 1 | from allure import description, feature, step, title 2 | from hamcrest import assert_that, is_ 3 | 4 | from framework.endpoints.authenticate_api import AuthenticateAPI 5 | from framework.tools.matcher import is_timestamp_valid 6 | 7 | LIMIT_ATTEMPTS = 5 8 | DURATION_BLOCKING_MINUTES = 60 9 | TIMESTAMP_PATTERN = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}" 10 | 11 | 12 | @feature("Authentication of user") 13 | class TestAuthentication: 14 | @title("Failed authentication is limited in number") 15 | @description( 16 | f"WHEN a user enters authentication data incorrectly more than {LIMIT_ATTEMPTS} times, " 17 | "THEN the service blocks the user for authentication" 18 | ) 19 | def test_of_limitation_attempts(self, create_authorized_user): 20 | with step("Registration of user"): 21 | user, token = ( 22 | create_authorized_user["user"], 23 | create_authorized_user["token"], 24 | ) 25 | 26 | with step("Authentication user with incorrect email"): 27 | data_post = { 28 | "email": user["email"], 29 | "password": user["password"] + "invalid", 30 | } 31 | email = data_post["email"] 32 | incorrect_password = data_post["password"] 33 | with step("User authentication attempt allowed number of times"): 34 | for attempt in range(LIMIT_ATTEMPTS): 35 | with step(f"Attempt {attempt}"): 36 | response = AuthenticateAPI().authentication( 37 | email=email, 38 | password=incorrect_password, 39 | expected_status_code=401, 40 | ) 41 | response_authentication = response.json() 42 | assert_that( 43 | response.status_code, is_(401), reason="Invalid status code" 44 | ) 45 | assert_that( 46 | response_authentication["message"], 47 | is_( 48 | f"Invalid credentials for user's account with email = '{email}'" 49 | ), 50 | reason="Authentication passed. There must be a mistake", 51 | ) 52 | assert_that( 53 | response_authentication["httpStatusCode"], 54 | is_(401), 55 | reason="Invalid status code in body of response", 56 | ) 57 | assert_that( 58 | is_timestamp_valid( 59 | response_authentication["timestamp"], TIMESTAMP_PATTERN 60 | ), 61 | reason=f"Timestamp '{response_authentication['timestamp']}' does not match " 62 | f"the expected format YYYY-MM-DD HH:MM:SS", 63 | ) 64 | 65 | with step("Authentication attempt over the limit"): 66 | response = AuthenticateAPI().authentication( 67 | email=email, password=incorrect_password, expected_status_code=401 68 | ) 69 | response_locked_authentication = response.json() 70 | assert_that(response.status_code, is_(401), reason="Invalid status code") 71 | assert_that( 72 | response_locked_authentication["message"], 73 | is_( 74 | f"The request was rejected due to an incorrect number of login attempts for the user " 75 | f"with email='{email}'. Try again in {DURATION_BLOCKING_MINUTES} minutes or reset your password" 76 | ), 77 | reason="Authentication passed. There must be a mistake", 78 | ) 79 | assert_that( 80 | response_locked_authentication["httpStatusCode"], 81 | is_(401), 82 | reason="Invalid status code in body of response", 83 | ) 84 | assert_that( 85 | is_timestamp_valid( 86 | response_locked_authentication["timestamp"], TIMESTAMP_PATTERN 87 | ), 88 | reason=f"Timestamp '{response_locked_authentication['timestamp']}' does not match " 89 | f"the expected format YYYY-MM-DD HH:MM:SS", 90 | ) 91 | -------------------------------------------------------------------------------- /UI/test_review_negative.py: -------------------------------------------------------------------------------- 1 | from allure import step, title, severity, story, severity_level 2 | import pytest 3 | 4 | from .pages.ProductPage import ProductPage 5 | from .set_of_steps import login_user, delete_old_review 6 | from .configs import link 7 | from data.text_review import parameterize_text_review_negative 8 | 9 | 10 | @story("Review, Rating") 11 | @title("Negative test for review") 12 | # @allure.description("") 13 | # @allure.tag("") 14 | @severity(severity_level.NORMAL) 15 | class TestReviewNegative: 16 | @pytest.mark.xfail(reason="Requirements is not approved", run=True) 17 | def test_non_latin_review(self, browser): 18 | with step('Login user'): 19 | login_user(browser, link) 20 | with step('Delete old review'): 21 | delete_old_review(browser, link) 22 | with step('Add non latin letters review'): 23 | product_page = ProductPage(browser, browser.current_url) 24 | product_page.click_add_review() 25 | product_page.set_rating() 26 | product_page.fill_review(parameterize_text_review_negative[0][0]) 27 | assert product_page.is_submit_button_not_active(), "Submit review button is active" 28 | 29 | @pytest.mark.xfail(reason="Requirements is not approved", run=True) 30 | def test_not_allowed_symbols_review(self, browser): 31 | with step('Login user'): 32 | login_user(browser, link) 33 | with step('Delete old review'): 34 | delete_old_review(browser, link) 35 | with step('Add not allowed symbols review'): 36 | product_page = ProductPage(browser, browser.current_url) 37 | product_page.click_add_review() 38 | product_page.set_rating() 39 | product_page.fill_review(parameterize_text_review_negative[1][0]) 40 | assert product_page.is_submit_button_not_active(), "Submit review button is active" 41 | 42 | def test_empty_review(self, browser): 43 | with step('Login user'): 44 | login_user(browser, link) 45 | with step('Delete old review'): 46 | delete_old_review(browser, link) 47 | with step('Add empty review'): 48 | product_page = ProductPage(browser, browser.current_url) 49 | product_page.click_add_review() 50 | product_page.set_rating() 51 | product_page.fill_review(parameterize_text_review_negative[2][0]) 52 | assert product_page.is_submit_button_not_active(), "Submit review button is active" 53 | 54 | def test_1501_char_review(self, browser): 55 | with step('Login user'): 56 | login_user(browser, link) 57 | with step('Delete old review'): 58 | delete_old_review(browser, link) 59 | with step('Add 1501 char review'): 60 | product_page = ProductPage(browser, browser.current_url) 61 | product_page.click_add_review() 62 | product_page.set_rating() 63 | product_page.fill_review(parameterize_text_review_negative[3][0]) 64 | counter = product_page.get_review_symbols_counter() 65 | assert counter == 1500, "User can add 1501 symbols review" 66 | 67 | def test_1857_char_review(self, browser): 68 | with step('Login user'): 69 | login_user(browser, link) 70 | with step('Delete old review'): 71 | delete_old_review(browser, link) 72 | with step('Add 1857 char review'): 73 | product_page = ProductPage(browser, browser.current_url) 74 | product_page.click_add_review() 75 | product_page.set_rating() 76 | product_page.fill_review(parameterize_text_review_negative[4][0]) 77 | counter = product_page.get_review_symbols_counter() 78 | assert counter == 1500, "User can add 1857 symbols review" 79 | 80 | def test_whitespaces_review(self, browser): 81 | with step('Login user'): 82 | login_user(browser, link) 83 | with step('Delete old review'): 84 | delete_old_review(browser, link) 85 | with step('Add whitespaces review'): 86 | product_page = ProductPage(browser, browser.current_url) 87 | product_page.click_add_review() 88 | product_page.set_rating() 89 | product_page.fill_review(parameterize_text_review_negative[5][0]) 90 | assert product_page.is_submit_button_not_active(), "Submit review button is active" 91 | -------------------------------------------------------------------------------- /tests/review/conftest.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | from allure_commons._allure import step 5 | from hamcrest import assert_that, is_ 6 | 7 | from data.text_reviews_for_product import reviews 8 | from framework.endpoints.authenticate_api import AuthenticateAPI 9 | from framework.endpoints.product_api import ProductAPI 10 | from framework.endpoints.review_api import ReviewAPI 11 | from framework.endpoints.users_api import UsersAPI 12 | from framework.tools.favorite_methods import extract_random_product_ids 13 | from framework.tools.generators import generate_user 14 | from framework.tools.review_methods import ( 15 | verify_user_review_by_user_name_in_all_product_reviews, 16 | ) 17 | 18 | 19 | def generate_and_insert_user(postgres): 20 | """Generating and inserting a user into the database 21 | 22 | Args: 23 | postgres: connection to Postgres DataBase 24 | """ 25 | 26 | user = generate_user() 27 | 28 | key_mapping = { 29 | "firstName": "first_name", 30 | "lastName": "last_name", 31 | "birthDate": "birth_date", 32 | "phoneNumber": "phone_number", 33 | "stripeCustomerToken": "stripe_customer_token", 34 | } 35 | user_to_insert = {key_mapping.get(k, k): v for k, v in user.items()} 36 | 37 | postgres.create_user(user_to_insert) 38 | 39 | return user 40 | 41 | 42 | def create_and_authorize_user(postgres): 43 | """Creating and authorizing a user within the test. 44 | 45 | Args: 46 | postgres: connection to Postgres DataBase 47 | """ 48 | with step("Creating user in DB"): 49 | user_to_create = generate_and_insert_user(postgres) 50 | 51 | with step("Authentication of user and getting token"): 52 | authentication_response = AuthenticateAPI().authentication( 53 | email=user_to_create["email"], password=user_to_create["password"] 54 | ) 55 | token = authentication_response.json()["token"] 56 | refresh_token = authentication_response.json()["refreshToken"] 57 | return {"user": user_to_create, "token": token, "refreshToken": refresh_token} 58 | 59 | 60 | def delete_user(token): 61 | """Deleting a user from the database. 62 | 63 | Args: 64 | token (): Authentication token of the user to delete 65 | """ 66 | with step("Deleting user"): 67 | UsersAPI().delete_user(token=token) 68 | 69 | 70 | @pytest.fixture(scope="function") 71 | def create_certain_number_of_reviews(postgres, request): 72 | created_users = [] 73 | num_reviews = request.param 74 | selected_reviews = random.sample(reviews, num_reviews) 75 | try: 76 | with step("Getting all products via API"): 77 | response_get_product = ProductAPI().get_all() 78 | 79 | with step("Verify that user does not have review for product"): 80 | get_random_product = extract_random_product_ids( 81 | response_get_product, product_quantity=1 82 | ) 83 | (product_id,) = get_random_product 84 | response_get_all_review = ReviewAPI().get_all_product_reviews( 85 | product_id=product_id 86 | ) 87 | 88 | for i, review in enumerate(selected_reviews): 89 | with step("Creating and authorizing user"): 90 | user_data = create_and_authorize_user(postgres) 91 | user, token = user_data["user"], user_data["token"] 92 | created_users.append(token) 93 | 94 | with step("Verify that user does not have review for product"): 95 | assert_that( 96 | verify_user_review_by_user_name_in_all_product_reviews( 97 | response_get_all_review, user 98 | ), 99 | is_(False), 100 | f"user {i + 1} has review", 101 | ) 102 | 103 | with step("Add review to randomly selected product by user"): 104 | ReviewAPI().add_product_review( 105 | token=token, 106 | product_id=product_id, 107 | text_review=review["text_review"], 108 | rating=review["rating"], 109 | ) 110 | 111 | with step( 112 | "Verify that the user's review is successfully added to the product by retrieving all product reviews" 113 | ): 114 | response_get_all_review = ReviewAPI().get_all_product_reviews( 115 | product_id=product_id 116 | ) 117 | 118 | yield {"user": user, "token": token, "product_id": product_id} 119 | finally: 120 | # Cleanup: delete all users created during the test 121 | for token in created_users: 122 | delete_user(token) 123 | -------------------------------------------------------------------------------- /tests/cart/test_update_item_in_cart_negative.py: -------------------------------------------------------------------------------- 1 | from allure import description, step, title, feature 2 | from hamcrest import assert_that, equal_to 3 | 4 | from data.data_for_cart import data_for_not_exist_shopping_cart_item_id 5 | from framework.asserts.common import assert_response_message, assert_content_type 6 | from framework.endpoints.cart_api import CartAPI 7 | from framework.tools.methods_to_cart import ( 8 | extract_random_item_detail, 9 | get_quantity_specific_cart_item, 10 | ) 11 | 12 | 13 | @feature("Updating item's quantity negative") 14 | class TestCart: 15 | @title("Updating item's quantity negative") 16 | @description( 17 | "GIVEN user is registered and has items shopping cart" 18 | "WHEN user try to decrease item's quantity that > than actual item quantity in shopping cart" 19 | "THEN status HTTP CODE = 400 and response body that contain appropriate error message returns" 20 | ) 21 | def test_updating_decrease_item_quantity_in_cart( 22 | self, creating_and_adding_product_to_shopping_cart 23 | ): 24 | with step( 25 | "Registration new user, and add product to the shopping cart" 26 | "Getting info about user and user's shopping cart " 27 | ): 28 | ( 29 | token, 30 | new_user_id, 31 | response_get_cart_after_added, 32 | ) = creating_and_adding_product_to_shopping_cart 33 | 34 | with step( 35 | "Getting random items to update from shopping cart and determination quantity for update " 36 | ): 37 | random_item_from_cart = extract_random_item_detail( 38 | response_get_cart_after_added 39 | ) 40 | item_id_to_update = random_item_from_cart["id"] 41 | item_quantity_before_update = random_item_from_cart["productQuantity"] 42 | quantity_to_update = -5 43 | 44 | with step("Update quantity of item"): 45 | response_after_update = CartAPI().update_quantity_product( 46 | token=token, 47 | item_id=item_id_to_update, 48 | item_quantity=quantity_to_update, 49 | expected_status_code=400, 50 | ) 51 | 52 | with step("Verify the error message from request for update"): 53 | expected_message = f"Invalid product quantity = {item_quantity_before_update + quantity_to_update} or product quantity without changes" 54 | assert_response_message( 55 | response_after_update, expected_message=expected_message 56 | ) 57 | 58 | with step("Get shopping cart of user and verify that quantity is not updated"): 59 | response_get_cart = CartAPI().get_user_cart(token=token) 60 | actual_item_in_cart = get_quantity_specific_cart_item( 61 | response_get_cart, item_id_to_update 62 | ) 63 | assert_that(item_quantity_before_update, equal_to(actual_item_in_cart)) 64 | 65 | @title("Test updating NOT existing item quantity in user's shopping cart") 66 | @description( 67 | "GIVEN user is registered and have shopping cart" 68 | "WHEN user try to update not exist item's quantity in the shopping cart" 69 | "THEN status HTTP CODE = 404 and response body that contain appropriate error message returns" 70 | ) 71 | def test_updating_not_exist_item_in_cart( 72 | self, creating_and_adding_product_to_shopping_cart 73 | ): 74 | with step( 75 | "Registration new user, and add product to the shopping cart" 76 | "Getting info about user and user's shopping cart " 77 | ): 78 | ( 79 | token, 80 | new_user_id, 81 | response_get_cart_after_added, 82 | ) = creating_and_adding_product_to_shopping_cart 83 | 84 | with step("Update quantity of product"): 85 | data_for_update_not_exist_item = data_for_not_exist_shopping_cart_item_id 86 | shopping_cart_item_id = data_for_update_not_exist_item["shoppingCartItemId"] 87 | response_after_update = CartAPI().update_quantity_product( 88 | token=token, 89 | item_id=data_for_update_not_exist_item["shoppingCartItemId"], 90 | item_quantity=data_for_update_not_exist_item["productQuantityChange"], 91 | expected_status_code=404, 92 | ) 93 | 94 | with step("Checking the response body and the Content-Type"): 95 | expected_message_after_update = f"The shopping cart item with shoppingCartItemId = {shopping_cart_item_id} is not found." 96 | assert_response_message( 97 | response_after_update, expected_message_after_update 98 | ) 99 | assert_content_type(response_after_update, "application/json") 100 | -------------------------------------------------------------------------------- /tests/authentication/test_refresh_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import feature, description, link, step, title 3 | from allure import severity 4 | from hamcrest import assert_that, is_, empty, is_not 5 | 6 | from framework.endpoints.authenticate_api import AuthenticateAPI 7 | from framework.endpoints.users_api import UsersAPI 8 | from framework.tools.generators import generate_jwt_token 9 | 10 | 11 | @feature("Refresh token") 12 | @link( 13 | url="http://localhost:8083/api/docs/swagger-ui/index.html#/Security/refreshToken", 14 | ) 15 | class TestLogout: 16 | @pytest.mark.critical 17 | @severity(severity_level="CRITICAL") 18 | @title("Test refresh access token and refresh with refresh token") 19 | @description( 20 | "Give user registered and login in." 21 | "WHEN access token is expired and user sent request to refresh access token with refreshToken." 22 | "THEN status HTTP CODE = 200 and get JWT refresh token and access token" 23 | ) 24 | def test_refresh_access_token_with_valid_refresh_token( 25 | self, create_authorized_user 26 | ): 27 | with step("Registration of user"): 28 | user, access_token, token_sent_for_refresh = ( 29 | create_authorized_user["user"], 30 | create_authorized_user["token"], 31 | create_authorized_user["refreshToken"], 32 | ) 33 | 34 | with step("Generate expired token"): 35 | expired_access_token = generate_jwt_token(email=user["email"], expired=True) 36 | 37 | with step("Verify that access token is expired by getting info about user"): 38 | UsersAPI().get_user(token=expired_access_token, expected_status_code=401) 39 | 40 | with step("Sent request for refresh access token using refresh token."): 41 | response_refresh_token = AuthenticateAPI().refresh_token( 42 | token=token_sent_for_refresh 43 | ) 44 | 45 | with step("Verify access token and refresh token in response"): 46 | access_token_after_refresh = response_refresh_token.json().get("token", "") 47 | refresh_token_after_refresh = response_refresh_token.json().get( 48 | "refreshToken", "" 49 | ) 50 | assert_that( 51 | access_token_after_refresh, 52 | is_not(empty()), 53 | reason="Token is not in response", 54 | ) 55 | assert_that( 56 | refresh_token_after_refresh, 57 | is_not(empty()), 58 | reason="Token is not in response", 59 | ) 60 | 61 | with step( 62 | "Verify that access token and refresh token are valid after refresh by getting user's info" 63 | ): 64 | UsersAPI().get_user( 65 | token=access_token_after_refresh, expected_status_code=200 66 | ) 67 | UsersAPI().get_user( 68 | token=refresh_token_after_refresh, expected_status_code=200 69 | ) 70 | 71 | @pytest.mark.critical 72 | @severity(severity_level="CRITICAL") 73 | @title("Test refresh access token and refresh with access token") 74 | @description( 75 | "Give user registered and login in." 76 | "WHEN user sent request to refresh access token with access token." 77 | "THEN status HTTP CODE = 200 and get JWT refresh token and access token" 78 | ) 79 | def test_refresh_access_token_with_valid_access_token(self, create_authorized_user): 80 | with step("Registration of user"): 81 | user, access_token, token_sent_for_refresh = ( 82 | create_authorized_user["user"], 83 | create_authorized_user["token"], 84 | create_authorized_user["refreshToken"], 85 | ) 86 | 87 | with step("Sent request for refresh access token using access token."): 88 | response_refresh_token = AuthenticateAPI().refresh_token(token=access_token) 89 | 90 | with step("Verify access token and refresh token in response"): 91 | access_token_after_refresh = response_refresh_token.json().get("token", "") 92 | refresh_token_after_refresh = response_refresh_token.json().get( 93 | "refreshToken", "" 94 | ) 95 | assert_that( 96 | access_token_after_refresh, 97 | is_not(empty()), 98 | reason="Token is not in response", 99 | ) 100 | assert_that( 101 | refresh_token_after_refresh, 102 | is_not(empty()), 103 | reason="Token is not in response", 104 | ) 105 | 106 | with step( 107 | "Verify that access token and refresh token are valid after refresh by getting user's info" 108 | ): 109 | UsersAPI().get_user( 110 | token=access_token_after_refresh, expected_status_code=200 111 | ) 112 | UsersAPI().get_user( 113 | token=refresh_token_after_refresh, expected_status_code=200 114 | ) 115 | -------------------------------------------------------------------------------- /framework/asserts/common.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_, contains_string 2 | from requests import Response 3 | 4 | 5 | def assert_status_code(response: Response, expected_status_code: int) -> None: 6 | """Asserts that the actual status code matches the expected status code. 7 | 8 | Args: 9 | response: The response object from the API call. 10 | expected_status_code: The expected status code. 11 | """ 12 | assert_that( 13 | response.status_code, 14 | is_(expected_status_code), 15 | reason=f"Expected status code {expected_status_code}, found: {response.status_code}", 16 | ) 17 | 18 | 19 | def assert_content_type(response: Response, expected_content_type: str) -> None: 20 | """Asserts that the Content-Type of the response matches the expected Content-Type. 21 | 22 | Args: 23 | response: The response object from the API call. 24 | expected_content_type: The expected Content-Type string. 25 | """ 26 | content_type = response.headers.get("Content-Type", "") 27 | assert_that( 28 | content_type, 29 | contains_string(expected_content_type), 30 | reason=f"Expected Content-Type '{expected_content_type}', found: '{content_type}'", 31 | ) 32 | 33 | 34 | def assert_response_message(response: Response, expected_message: str) -> None: 35 | """Asserts that the message in the response body matches the expected message. 36 | 37 | Args: 38 | response: The response object from the API call. 39 | expected_message: The expected message string. 40 | """ 41 | actual_message = response.json().get("message", "") 42 | assert_that( 43 | actual_message, 44 | is_(expected_message), 45 | reason=f"Expected message '{expected_message}', found: '{actual_message}'", 46 | ) 47 | 48 | 49 | def assert_message_in_response(response: Response, expected_message: str) -> None: 50 | """Asserts that the message in the response body matches the expected message. 51 | 52 | Args: 53 | response: The response object from the API call. 54 | expected_message: The expected message string. 55 | """ 56 | actual_message = response.json().get("message", "") 57 | assert_that( 58 | actual_message, 59 | contains_string(expected_message), 60 | reason=f"Expected response contains '{expected_message}', found: '{actual_message}'", 61 | ) 62 | 63 | 64 | def assert_review_text(response: Response, expected_review: str) -> None: 65 | """Asserts that the message in the response body matches the expected message. 66 | 67 | Args: 68 | response: The response object from the API call. 69 | expected_review: The expected review string. 70 | """ 71 | actual_review_text = response.json().get("text", "") 72 | assert_that( 73 | actual_review_text, 74 | is_(expected_review), 75 | reason=f"Expected text review is '{expected_review}', found: '{actual_review_text}'", 76 | ) 77 | 78 | 79 | def assert_rating(response: Response, expected_rating: int) -> None: 80 | """Asserts that the product rating in the response body matches the expected product rating. 81 | 82 | Args: 83 | response: The response object from the API call. 84 | expected_rating: The expected product rating. 85 | """ 86 | actual_product_rating = response.json()["rating"] 87 | assert_that( 88 | actual_product_rating, 89 | is_(expected_rating), 90 | reason=f"Expected rating product is '{expected_rating}', found: '{actual_product_rating}'", 91 | ) 92 | 93 | 94 | def assert_user_name_in_response( 95 | response: Response, expected_first_name: str, expected_last_name: str 96 | ) -> None: 97 | """Asserts that the user's name in the response body matches the expected user's name. 98 | 99 | Args: 100 | response: The response object from the API call. 101 | expected_first_name: User's first name. 102 | expected_last_name: User's last name. 103 | """ 104 | actual_first_name = response.json()["userName"] 105 | actual_last_name = response.json()["userLastName"] 106 | assert_that( 107 | actual_first_name, 108 | is_(expected_first_name), 109 | reason=f"Expected first name is '{expected_first_name}', found: '{actual_first_name}'", 110 | ) 111 | assert_that( 112 | actual_last_name, 113 | is_(expected_last_name), 114 | reason=f"Expected last name is '{expected_last_name}', found: '{actual_last_name}'", 115 | ) 116 | 117 | 118 | def assert_key_and_value_in_response(response: Response, *key: str) -> None: 119 | """Asserts that the id review in the response body 120 | 121 | Args: 122 | key: specific key in the response body 123 | response: The response object from the API call. 124 | 125 | """ 126 | 127 | data = response.json() 128 | assert key in data, f"Key '{key}' not found in response" 129 | 130 | value = data[key] 131 | assert_that(value is not None, f"Value for key '{key}' is None.") 132 | assert_that(value != "", f"Value for key '{key}' is empty.") 133 | -------------------------------------------------------------------------------- /framework/endpoints/users_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | import requests 6 | from requests import Response 7 | 8 | from configs import HOST 9 | from framework.asserts.common import assert_status_code 10 | from framework.tools.logging_allure import log_request 11 | 12 | 13 | class UsersAPI: 14 | def __init__(self): 15 | self.url = HOST + "/api/v1/users" 16 | self.headers = {"Content-Type": "application/json"} 17 | 18 | def get_user(self, token: str = "", expected_status_code: int = 200) -> Response: 19 | """Getting info about user via API 20 | 21 | Args: 22 | expected_status_code: Expected HTTP code from Response 23 | token: JWT token for authorization of request 24 | """ 25 | headers = self.headers 26 | headers["Authorization"] = f"Bearer {token}" 27 | response = requests.get(headers=headers, url=self.url) 28 | assert_status_code(response, expected_status_code=expected_status_code) 29 | log_request(response) 30 | 31 | return response 32 | 33 | def delete_user(self, token: str, expected_status_code: int = 200) -> Response: 34 | """Deleting user 35 | 36 | Args: 37 | expected_status_code: Expected HTTP code from Response 38 | token: JWT token for authorization of request 39 | 40 | """ 41 | headers = self.headers 42 | headers["Authorization"] = f"Bearer {token}" 43 | response = requests.delete(headers=headers, url=self.url) 44 | assert_status_code(response, expected_status_code=expected_status_code) 45 | log_request(response) 46 | return response 47 | 48 | def update_user(self, token: str = "", user_data: dict = None) -> Response: 49 | """Updating user info 50 | 51 | Args: 52 | token: JWT token for authorization of request 53 | user_data: data for updating user info 54 | """ 55 | headers = self.headers 56 | headers["Authorization"] = f"Bearer {token}" 57 | response = requests.put(headers=headers, url=self.url, json=user_data) 58 | log_request(response) 59 | 60 | return response 61 | 62 | def change_password( 63 | self, 64 | token: str, 65 | new_password: str, 66 | old_password: str, 67 | expected_status_code: int = 200, 68 | ) -> Response: 69 | """Change user password 70 | 71 | Args: 72 | old_password: old password 73 | new_password: new password 74 | expected_status_code: Expected HTTP code from Response 75 | token: JWT token for authorization of request 76 | 77 | """ 78 | data = {"newPassword": new_password, "oldPassword": old_password} 79 | headers = self.headers 80 | headers["Authorization"] = f"Bearer {token}" 81 | response = requests.patch(headers=headers, url=self.url, data=json.dumps(data)) 82 | assert_status_code(response, expected_status_code=expected_status_code) 83 | log_request(response) 84 | 85 | return response 86 | 87 | def get_user_avatar( 88 | self, token: str = "", expected_status_code: int = 200 89 | ) -> Response: 90 | """Getting info about user's avatar via API 91 | 92 | Args: 93 | expected_status_code: Expected HTTP code from Response 94 | token: JWT token for authorization of request 95 | """ 96 | headers = self.headers 97 | path = f"{self.url}/avatar" 98 | headers["Authorization"] = f"Bearer {token}" 99 | response = requests.get(headers=headers, url=path) 100 | assert_status_code(response, expected_status_code=expected_status_code) 101 | log_request(response) 102 | 103 | return response 104 | 105 | def post_user_avatar( 106 | self, token: str, image_path: str, expected_status_code: int = 200 107 | ) -> Response: 108 | """Posts a user's avatar image to the API 109 | 110 | Args: 111 | token: JWT token for authorization of the request 112 | image_path: Path to the image file to be uploaded 113 | expected_status_code: Expected HTTP status code from the response 114 | """ 115 | headers = self.headers 116 | path = f"{self.url}/avatar" 117 | headers["Authorization"] = f"Bearer {token}" 118 | 119 | # Open the image file in binary mode 120 | with open(image_path, "rb") as image_file: 121 | 122 | files = { 123 | "file": ( 124 | os.path.basename(image_path), 125 | image_file, 126 | "multipart/form-data", 127 | ) 128 | } 129 | response = requests.post(url=path, headers=headers, files=files) 130 | 131 | assert_status_code(response, expected_status_code=expected_status_code) 132 | log_request( 133 | response 134 | ) # Make sure you have a function to log the request and response details 135 | 136 | return response 137 | -------------------------------------------------------------------------------- /tests/product/test_all_products.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from allure import description, feature, link, step, title 4 | from hamcrest import assert_that, is_ 5 | from random import randint 6 | 7 | from framework.asserts.product_asserts import check_mapping_db_to_api 8 | from framework.endpoints.product_api import ProductAPI 9 | 10 | 11 | @feature("Getting a list of all products") 12 | class TestAllProducts: 13 | @pytest.mark.skip(reason="Need to fix the bug in the test with lengths comparison") 14 | @title("Getting all products not authorized") 15 | @description( 16 | "WHEN not authorized requesting to get all products without parameters, " 17 | "THEN all products from the list are returned" 18 | ) 19 | def test_get_all_products_not_auth(self, postgres): 20 | with step("Getting info about the random product in DB"): 21 | db_data = postgres.get_random_products()[0] 22 | 23 | with step("Getting all products via API"): 24 | api_data = ProductAPI().get_all() 25 | products = api_data.json()["products"] 26 | 27 | with step("Selecting an item from a list received via API by id"): 28 | assert_data = [el for el in products if el["id"] == db_data["id"]][0] 29 | 30 | with step("Checking mapping data DB <> API"): 31 | check_mapping_db_to_api(reference=db_data, compared=assert_data) 32 | 33 | with step("Checking count items DB <> API"): 34 | assert_that(len(db_data), is_(len(products))) 35 | 36 | @title("Getting all products with query parameters") 37 | @description( 38 | "WHEN not authorized requesting to get all products with query parameters, " 39 | "THEN the products sorted by attributes are returned" 40 | ) 41 | @pytest.mark.parametrize( 42 | "params", 43 | [ 44 | pytest.param({}, id="Default parameters"), 45 | pytest.param( 46 | { 47 | "page": 0, 48 | "size": 1, 49 | "sort_attribute": "price", 50 | "sort_direction": "desc", 51 | }, 52 | id="?page=0&size=1&sort_attribute=price&sort_direction=desc", 53 | ), 54 | pytest.param( 55 | { 56 | "page": 0, 57 | "size": 1, 58 | "sort_attribute": "name", 59 | "sort_direction": "asc", 60 | }, 61 | id="?page=0&size=1&sort_attribute=name&sort_direction=asc", 62 | ), 63 | pytest.param( 64 | { 65 | "page": 1, 66 | "size": 1, 67 | "sort_attribute": "quantity", 68 | "sort_direction": "desc", 69 | }, 70 | id="?page=1&size=1&sort_attribute=quantity&sort_direction=desc", 71 | ), 72 | pytest.param( 73 | { 74 | "page": 1, 75 | "size": 1, 76 | "sort_attribute": "price", 77 | "sort_direction": "asc", 78 | }, 79 | id="?page=1&size=1&sort_attribute=price&sort_direction=asc", 80 | ), 81 | pytest.param( 82 | { 83 | "page": 1, 84 | "size": 1, 85 | "sort_attribute": "name", 86 | "sort_direction": "desc", 87 | }, 88 | id="?page=1&size=1&sort_attribute=name&sort_direction=desc", 89 | ), 90 | pytest.param( 91 | { 92 | "page": 0, 93 | "size": 1, 94 | "sort_attribute": "quantity", 95 | "sort_direction": "asc", 96 | }, 97 | id="?page=0&size=1&sort_attribute=quantity&sort_direction=asc", 98 | ), 99 | ], 100 | ) 101 | def test_get_all_products_with_params(self, params: dict, postgres): 102 | with step("Setting the parameters by default"): 103 | # These are the values required to query the database 104 | field = params["sort_attribute"] if params.get("sort_attribute") else "name" 105 | ascend = True if params.get("sort_direction") == "asc" else False 106 | size = params.get("size", 50) 107 | page = params.get("page", 0) 108 | 109 | with step("Getting products by query parameters via API"): 110 | api_data = ProductAPI().get_all(params=params).json()["products"] 111 | 112 | with step("Getting products by filters from DB"): 113 | db_data = postgres.get_product_by_filter( 114 | field=field, ascend=ascend, size=size, page=page 115 | ) 116 | 117 | with step("Checking count items DB <> API"): 118 | assert_that(len(db_data), is_(len(api_data))) 119 | 120 | with step("Checking mapping data DB <> API"): 121 | index = randint(0, len(db_data) - 1) 122 | check_mapping_db_to_api(reference=db_data[index], compared=api_data[index]) 123 | -------------------------------------------------------------------------------- /tests/registration/test_registration_with_email.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature, severity 3 | from hamcrest import assert_that, not_, is_not, empty 4 | 5 | from configs import ( 6 | gmail_password, 7 | imap_server, 8 | email_address_to_connect, 9 | EMAIL_DOMAIN, 10 | EMAIL_LOCAL_PART, 11 | ) 12 | from configs import password, firstName, lastName, email, email_iced_late 13 | from framework.asserts.common import assert_content_type 14 | from framework.asserts.registration_asserts import check_mapping_api_to_db 15 | from framework.endpoints.authenticate_api import AuthenticateAPI 16 | from framework.endpoints.users_api import UsersAPI 17 | 18 | from framework.tools.class_email import Email 19 | from framework.tools.generators import ( 20 | generate_string, 21 | append_random_to_local_part_email, 22 | ) 23 | 24 | 25 | # 26 | # Connection configuration 27 | # PostgresDB.dbname = DB_NAME 28 | # PostgresDB.host = HOST_DB 29 | # PostgresDB.port = PORT_DB 30 | # PostgresDB.user = DB_USER 31 | # PostgresDB.password = DB_PASS 32 | 33 | 34 | @feature("Registration of user") 35 | class TestAuthentication: 36 | @pytest.mark.critical 37 | @severity(severity_level="CRITICAL") 38 | @title("Test registration user with email confirmation") 39 | @description( 40 | "WHEN the user submits the minimum required data for registration," 41 | "WHEN registration's confirmation code from email is provided" 42 | "THEN status HTTP CODE = 200 and get JWT token" 43 | ) 44 | def test_registration(self, postgres): 45 | with step("Generation data for registration"): 46 | email_random = append_random_to_local_part_email( 47 | domain=EMAIL_DOMAIN, 48 | email_local_part=EMAIL_LOCAL_PART, 49 | length_random_part=5, 50 | ) 51 | data_for_registration = { 52 | "firstName": firstName, 53 | "lastName": lastName, 54 | "password": password, 55 | "email": email_random, 56 | } 57 | 58 | with step("Registration new user"): 59 | response_registration = AuthenticateAPI().registration( 60 | body=data_for_registration 61 | ) 62 | expected_content_type = "text/plain;charset=UTF-8" 63 | assert_content_type( 64 | response_registration, expected_content_type=expected_content_type 65 | ) 66 | 67 | with step("Extract code from email for confirmation registration"): 68 | email_box = "Inbox" 69 | key = "from_" 70 | value = email_iced_late 71 | code_from_email = Email( 72 | imap_server=imap_server, 73 | email_address=email_address_to_connect, 74 | mail_password=gmail_password, 75 | ).extract_confirmation_code_from_email( 76 | email_box=email_box, key=key, value=value 77 | ) 78 | assert_that(code_from_email, not_(None), "Token should not be empty") 79 | 80 | with step("Confirm registration using code from email"): 81 | response_after_confirmation = AuthenticateAPI().confirmation_email( 82 | code=code_from_email 83 | ) 84 | 85 | with step("Verify JWT token is returned after confirmation and content type"): 86 | assert_that(response_after_confirmation.json()["token"], is_not(empty())) 87 | expected_content_type = "application/json" 88 | assert_content_type( 89 | response_after_confirmation, expected_content_type=expected_content_type 90 | ) 91 | 92 | with step( 93 | "Verify that user is successfully registered by getting info about user" 94 | ): 95 | UsersAPI().get_user(token=response_after_confirmation.json()["token"]) 96 | 97 | with step( 98 | "Verify that user is successfully registered by authentication through user's credentials" 99 | ): 100 | email_for_authentication = data_for_registration["email"] 101 | password_for_authentication = data_for_registration["password"] 102 | 103 | response_authentication = AuthenticateAPI().authentication( 104 | email=email_for_authentication, password=password_for_authentication 105 | ) 106 | expected_content_type = "application/json" 107 | assert_content_type( 108 | response_authentication, expected_content_type=expected_content_type 109 | ) 110 | token = response_authentication.json().get("token") 111 | 112 | with step("Getting info about the user in DB"): 113 | user_data = postgres.get_data_by_filter( 114 | table="user_details", 115 | field="email", 116 | value=data_for_registration["email"], 117 | ) 118 | 119 | with step("Checking mapping data from the API request to the database"): 120 | check_mapping_api_to_db( 121 | api_request=data_for_registration, database_data=user_data[0] 122 | ) 123 | 124 | with step("Deleting user"): 125 | UsersAPI().delete_user(token=token) 126 | -------------------------------------------------------------------------------- /tests/user/test_change_password.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from allure import severity 4 | 5 | from framework.asserts.common import assert_content_type 6 | from framework.endpoints.authenticate_api import AuthenticateAPI 7 | from framework.endpoints.users_api import UsersAPI 8 | from framework.tools.generators import ( 9 | generate_password, 10 | generate_string, 11 | generate_numeric_password, 12 | ) 13 | 14 | 15 | @feature("Change user password") 16 | class TestChangePassword: 17 | @pytest.mark.critical 18 | @severity(severity_level="MAJOR") 19 | @title("Test change user password with valid length") 20 | @description( 21 | "GIVEN user is registered" 22 | "WHEN user sends a request to change password with length of password according to requirement" 23 | "THEN status HTTP CODE = 200" 24 | ) 25 | @pytest.mark.parametrize( 26 | "new_password", 27 | [ 28 | generate_password(8), 29 | generate_password(9), 30 | generate_password(63), 31 | generate_password(64), 32 | generate_password(65), 33 | generate_password(127), 34 | generate_password(128), 35 | ], 36 | ) 37 | def test_change_user_password_with_valid_length( 38 | self, create_authorized_user, new_password 39 | ): 40 | with step("Registration of user"): 41 | user, token = ( 42 | create_authorized_user["user"], 43 | create_authorized_user["token"], 44 | ) 45 | 46 | with step("Change user password"): 47 | response_change_password = UsersAPI().change_password( 48 | token=token, 49 | old_password=user["password"], 50 | new_password=new_password, 51 | expected_status_code=200, 52 | ) 53 | 54 | # bug:content_type is "" ??? 55 | # with step("Checking the response type of the body"): 56 | # assert_content_type(response_change_password, "application/json") 57 | with step( 58 | "Verify that password successfully changes by" 59 | " sent request for Authentication with new password " 60 | ): 61 | response_auth = AuthenticateAPI().authentication( 62 | email=user["email"], password=new_password, expected_status_code=200 63 | ) 64 | new_token = response_auth.json()["token"] 65 | UsersAPI().get_user(token=new_token, expected_status_code=200) 66 | 67 | @pytest.mark.critical 68 | @severity(severity_level="MAJOR") 69 | @title("Test change user's password that meet the requirements.") 70 | @description( 71 | "GIVEN user is registered" 72 | "WHEN user sends a request to change password " 73 | "with password that contains at least one letters or at least one digit " 74 | "and may contain special characters ('@$!%*?&') in the password" 75 | "THEN status HTTP CODE = 200" 76 | ) 77 | @pytest.mark.parametrize( 78 | "new_password", 79 | [ 80 | pytest.param(generate_string(4) + generate_numeric_password(length=4)), 81 | pytest.param( 82 | generate_string(4) + generate_numeric_password(length=4) + "@" 83 | ), 84 | pytest.param( 85 | generate_string(4) + generate_numeric_password(length=4) + "$" 86 | ), 87 | pytest.param( 88 | generate_string(4) + generate_numeric_password(length=4) + "%" 89 | ), 90 | pytest.param( 91 | generate_string(4) + generate_numeric_password(length=4) + "&" 92 | ), 93 | pytest.param( 94 | generate_string(4) + generate_numeric_password(length=4) + "*" 95 | ), 96 | pytest.param( 97 | generate_string(4) + generate_numeric_password(length=4) + "?" 98 | ), 99 | pytest.param( 100 | generate_string(4) + generate_numeric_password(length=4) + "%" 101 | ), 102 | ], 103 | ) 104 | def test_change_user_password_with_correct_format( 105 | self, create_authorized_user, new_password 106 | ): 107 | with step("Registration of user"): 108 | user, token = ( 109 | create_authorized_user["user"], 110 | create_authorized_user["token"], 111 | ) 112 | 113 | with step("Change user password"): 114 | response_change_password = UsersAPI().change_password( 115 | token=token, 116 | old_password=user["password"], 117 | new_password=new_password, 118 | expected_status_code=200, 119 | ) 120 | 121 | # bug:content_type is "" ??? 122 | # with step("Checking the response type of the body"): 123 | # assert_content_type(response_change_password, "application/json") 124 | 125 | with step( 126 | "Verify that password successfully changes by" 127 | " sent request for Authentication with new password " 128 | ): 129 | AuthenticateAPI().authentication( 130 | email=user["email"], password=new_password, expected_status_code=200 131 | ) 132 | -------------------------------------------------------------------------------- /framework/queries/postgres_db.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from framework.clients.db_client import DBClient 4 | from framework.tools.generators import generate_string, generate_user 5 | 6 | 7 | class PostgresDB: 8 | host = None 9 | port = None 10 | dbname = None 11 | user = None 12 | password = None 13 | 14 | def __init__(self): 15 | """Initializing the connection""" 16 | 17 | self.db = DBClient( 18 | host=self.host, 19 | port=self.port, 20 | dbname=self.dbname, 21 | user=self.user, 22 | password=self.password, 23 | ) 24 | 25 | def close(self) -> None: 26 | """Closing the connection""" 27 | self.db.close() 28 | 29 | def get_data_by_filter( 30 | self, table: str, field: str, value: str 31 | ) -> Optional[List[dict]]: 32 | """Getting data from table by filter field and its value 33 | 34 | Args: 35 | table: table in database; 36 | field: field of table; 37 | value: field value. 38 | """ 39 | return self.db.fetch_all(f"SELECT * FROM {table} WHERE {field} = '{value}';") 40 | 41 | def get_random_products(self, quantity: int = 1) -> Optional[List[dict]]: 42 | """Getting a random product 43 | 44 | Args: 45 | quantity: number of random products 46 | """ 47 | return self.db.fetch_all( 48 | "SELECT *" 49 | "FROM product" 50 | "WHERE active = true" 51 | "ORDER BY RANDOM()" 52 | f"LIMIT {quantity};" 53 | ) 54 | 55 | def get_product_by_filter( 56 | self, field: str, ascend: bool = False, size: int = -1, page: int = -1 57 | ) -> Optional[List[dict]]: 58 | """Getting sorted products by size and page by page 59 | 60 | Args: 61 | field: field for sorted; 62 | ascend: ascending sorted, True - ascending, False - descending; 63 | size: the amount of data per page; 64 | page: page number. 65 | """ 66 | 67 | response = ( 68 | "SELECT * " 69 | "FROM product " 70 | f"ORDER BY {field} {'ASC' if ascend else 'DESC'}" 71 | ) 72 | 73 | if size > 0: 74 | response += f" LIMIT {size}" 75 | if page >= 0: 76 | response += f" OFFSET {size * page}" 77 | return self.db.fetch_all(response) 78 | 79 | def get_random_users(self, quantity: int = 1) -> List[dict]: 80 | """Getting a random user 81 | 82 | Args: 83 | quantity: number of random users 84 | """ 85 | return self.db.fetch_all( 86 | f"SELECT * FROM user_details ORDER BY RANDOM() LIMIT {quantity};" 87 | ) 88 | 89 | def create_user(self, user: dict) -> None: 90 | """Inserting user into database 91 | 92 | Args: 93 | user: user data: 94 | - id - user of id; 95 | - first_name - first name of user; 96 | - last_name - last name of user; 97 | - email - email of user; 98 | - password - password for user; 99 | - hashed_password - hash of password for user. 100 | """ 101 | self.db.execute( 102 | f""" 103 | INSERT INTO public.user_details(id 104 | , first_name 105 | , last_name 106 | , stripe_customer_token 107 | , birth_date 108 | , phone_number 109 | , email 110 | , password 111 | , address_id 112 | , account_non_expired 113 | , account_non_locked 114 | , credentials_non_expired 115 | , enabled 116 | ) 117 | VALUES ('{user["id"]}' 118 | , '{user["first_name"]}' 119 | , '{user["last_name"]}' 120 | , '{user["stripe_customer_token"]}' 121 | , '{user["birth_date"]}' 122 | , '{user["phone_number"]}' 123 | , '{user["email"]}' 124 | , '{user["hashed_password"]}' 125 | , null 126 | , true 127 | , true 128 | , true 129 | , true 130 | ); 131 | """ 132 | ) 133 | 134 | def create_random_users(self, quantity: int = 1) -> List[dict]: 135 | """Creating random user(s) 136 | 137 | Args: 138 | quantity: number of random users 139 | """ 140 | users = [self.create_user(generate_user()) for _ in range(quantity)] 141 | 142 | return users 143 | 144 | def delete_user(self, user_id: str) -> None: 145 | """Deletes a user from the database based on user ID 146 | 147 | Args: 148 | user_id: user ID 149 | """ 150 | delete_query = f"DELETE FROM public.user_details WHERE id = '{user_id}';" 151 | self.db.execute(delete_query) 152 | 153 | def select_user_by_email(self, email) -> Optional[List[dict]]: 154 | """Search user by email in BD 155 | 156 | Args: 157 | email: user's email 158 | """ 159 | select_query = f"SELECT COUNT(*) FROM user_details WHERE email = '{email}'" 160 | return self.db.fetch_all(select_query) 161 | -------------------------------------------------------------------------------- /framework/endpoints/authenticate_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | import requests 5 | from requests import Response 6 | 7 | from configs import HOST 8 | from framework.asserts.common import assert_status_code, assert_content_type 9 | from framework.tools.logging_allure import log_request 10 | 11 | 12 | class AuthenticateAPI: 13 | def __init__(self): 14 | """Initializing parameters for request""" 15 | self.url = HOST + "/api/v1/auth" 16 | self.headers = {"Content-Type": "application/json"} 17 | 18 | def authentication( 19 | self, email: str, password: str, expected_status_code: int = 200 20 | ) -> Response: 21 | """Endpoint for authentication of user 22 | 23 | Args: 24 | expected_status_code: expected http status code from response 25 | email: user's email address; 26 | password: password for email. 27 | """ 28 | data = { 29 | "email": email, 30 | "password": password, 31 | } 32 | path = self.url + "/authenticate" 33 | response = requests.post(url=path, data=json.dumps(data), headers=self.headers) 34 | assert_status_code(response, expected_status_code=expected_status_code) 35 | log_request(response) 36 | 37 | return response 38 | 39 | def logout(self, token: str) -> Response: 40 | """User logout 41 | 42 | Args: 43 | token: JWT token for authorization of request 44 | """ 45 | headers = self.headers 46 | headers["Authorization"] = f"Bearer {token}" 47 | path = self.url + "/logout" 48 | response = requests.post(url=path, headers=headers) 49 | log_request(response) 50 | 51 | return response 52 | 53 | def registration(self, body: dict, expected_status_code=200) -> Response: 54 | """Endpoint for registration of user 55 | 56 | Args: 57 | expected_status_code: expected http status code from response 58 | body: registration data with required fields: 59 | email: electronic mail; 60 | firstName: name; 61 | lastName: surname; 62 | password: password for electronic mail. 63 | """ 64 | path = self.url + "/register" 65 | response = requests.post(url=path, data=json.dumps(body), headers=self.headers) 66 | assert_status_code(response, expected_status_code=expected_status_code) 67 | log_request(response) 68 | 69 | return response 70 | 71 | def confirmation_email( 72 | self, code: str, expected_status_code: int = 201 73 | ) -> Response: 74 | """Endpoint for authentication of user 75 | 76 | Args: 77 | expected_status_code: expected http status code from response 78 | code: confirmation code from email 79 | """ 80 | data = {"token": code} 81 | path = self.url + "/confirm" 82 | response = requests.post(url=path, data=json.dumps(data), headers=self.headers) 83 | assert_status_code(response, expected_status_code=expected_status_code) 84 | log_request(response) 85 | 86 | return response 87 | 88 | def refresh_token(self, token: str, expected_status_code: int = 200) -> Response: 89 | """Endpoint for authentication of user 90 | 91 | Args: 92 | expected_status_code: expected http status code from response 93 | token: bearer token 94 | """ 95 | 96 | path = self.url + "/refresh" 97 | headers = self.headers 98 | headers["Authorization"] = f"Bearer {token}" 99 | response = requests.post(url=path, headers=self.headers) 100 | assert_status_code(response, expected_status_code=expected_status_code) 101 | log_request(response) 102 | 103 | return response 104 | 105 | def forgot_password(self, email: str, expected_status_code: int = 200) -> Response: 106 | """Endpoint for authentication of user 107 | 108 | Args: 109 | expected_status_code: expected http status code from response 110 | email: email for reset password 111 | """ 112 | data = {"email": email} 113 | path = self.url + "/password/forgot" 114 | response = requests.post(url=path, data=json.dumps(data), headers=self.headers) 115 | assert_status_code(response, expected_status_code=expected_status_code) 116 | log_request(response) 117 | 118 | return response 119 | 120 | def change_password_through_reset( 121 | self, 122 | email: str, 123 | code_for_reset_password: str, 124 | new_password: str, 125 | expected_status_code: int = 200, 126 | ) -> Response: 127 | """Endpoint for authentication of user 128 | 129 | Args: 130 | code_for_reset_password: code for reset password, which was sent to user's email 131 | new_password: new password 132 | expected_status_code: expected http status code from response 133 | email: email for reset password 134 | """ 135 | data = { 136 | "email": email, 137 | "code": code_for_reset_password, 138 | "password": new_password, 139 | } 140 | path = self.url + "/password/change" 141 | response = requests.post(url=path, data=json.dumps(data), headers=self.headers) 142 | assert_status_code(response, expected_status_code=expected_status_code) 143 | log_request(response) 144 | 145 | return response 146 | -------------------------------------------------------------------------------- /framework/endpoints/review_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | from requests import Response 5 | 6 | from configs import HOST 7 | from framework.asserts.common import assert_status_code 8 | from framework.tools.logging_allure import log_request 9 | 10 | 11 | class ReviewAPI: 12 | def __init__(self): 13 | self.url = f"{HOST}/api/v1/products" 14 | self.headers = {"Content-Type": "application/json"} 15 | 16 | def get_all_product_reviews( 17 | self, product_id: str, expected_status_code: int = 200, timeout=10, **filters 18 | ) -> Response: 19 | """Getting info about user via API 20 | 21 | Args: 22 | timeout: timeout for request 23 | product_id: id of product 24 | expected_status_code: Expected HTTP code from Response 25 | 26 | """ 27 | headers = self.headers 28 | url = f"{self.url}/{product_id}/reviews" 29 | try: 30 | response = requests.get( 31 | url, headers=headers, params=filters, timeout=timeout 32 | ) 33 | assert_status_code(response, expected_status_code=expected_status_code) 34 | log_request(response) 35 | except requests.exceptions.Timeout: 36 | print("The request timed out") 37 | raise 38 | except requests.exceptions.RequestException as e: 39 | print(f"An error occurred: {e}") 40 | raise 41 | 42 | return response 43 | 44 | def delete_product_review( 45 | self, 46 | token: str, 47 | product_id: str, 48 | review_id: str, 49 | expected_status_code: int = 200, 50 | ) -> Response: 51 | """Deleting user 52 | 53 | Args: 54 | review_id: id of review 55 | product_id: id of product 56 | expected_status_code: Expected HTTP code from Response 57 | token: JWT token for authorization of request 58 | 59 | """ 60 | headers = self.headers 61 | headers["Authorization"] = f"Bearer {token}" 62 | url = f"{self.url}/{product_id}/reviews/{review_id}" 63 | response = requests.delete(headers=headers, url=url) 64 | assert_status_code(response, expected_status_code=expected_status_code) 65 | log_request(response) 66 | return response 67 | 68 | def add_product_review( 69 | self, 70 | token: str, 71 | product_id: str, 72 | text_review: str, 73 | rating: int, 74 | expected_status_code: int = 200, 75 | ) -> Response: 76 | """Deleting user 77 | 78 | Args: 79 | rating: rate of product 80 | text_review: text for product review 81 | product_id: product id for review 82 | expected_status_code: Expected HTTP code from Response 83 | token: JWT token for authorization of request 84 | 85 | """ 86 | data = {"text": text_review, "rating": rating} 87 | headers = self.headers 88 | headers["Authorization"] = f"Bearer {token}" 89 | url = f"{self.url}/{product_id}/reviews" 90 | response = requests.post(headers=headers, url=url, data=json.dumps(data)) 91 | assert_status_code(response, expected_status_code=expected_status_code) 92 | log_request(response) 93 | return response 94 | 95 | def get_user_product_review( 96 | self, product_id: str, token: str, expected_status_code: int = 200 97 | ) -> Response: 98 | """Getting info about user via API 99 | 100 | Args: 101 | token: JWT token for authorization of request 102 | product_id: ID of product 103 | expected_status_code: Expected HTTP code from Response 104 | 105 | """ 106 | headers = self.headers 107 | headers["Authorization"] = f"Bearer {token}" 108 | url = f"{self.url}/{product_id}/review" 109 | response = requests.get(headers=headers, url=url) 110 | assert_status_code(response, expected_status_code=expected_status_code) 111 | log_request(response) 112 | 113 | return response 114 | 115 | def like_dislike_product_review( 116 | self, 117 | product_id: str, 118 | token: str, 119 | product_review_id: str, 120 | expected_status_code: int = 200, 121 | is_like: bool = True, 122 | ) -> Response: 123 | """Getting info about user via API 124 | 125 | Args: 126 | is_like: like or dislike(True or False) 127 | product_review_id: ID of product review 128 | token: JWT token for authorization of request 129 | product_id: ID of product 130 | expected_status_code: Expected HTTP code from Response 131 | 132 | """ 133 | headers = self.headers 134 | data = {"isLike": is_like} 135 | headers["Authorization"] = f"Bearer {token}" 136 | url = f"{self.url}/{product_id}/reviews/{product_review_id}/rate" 137 | response = requests.post(headers=headers, data=json.dumps(data), url=url) 138 | assert_status_code(response, expected_status_code=expected_status_code) 139 | log_request(response) 140 | 141 | return response 142 | 143 | def get_product_review_statistics(self, product_id: str) -> Response: 144 | """Getting product review statistics 145 | 146 | Args: 147 | product_id: ID of product 148 | """ 149 | url = f"{self.url}/{product_id}/reviews/statistics" 150 | response = requests.get(url=url) 151 | log_request(response) 152 | 153 | return response 154 | -------------------------------------------------------------------------------- /tests/registration/test_registration_negative.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, feature, link, step, title, severity 3 | from hamcrest import assert_that, has_length, is_not, empty, equal_to 4 | 5 | # from configs import DB_NAME, HOST_DB, PORT_DB, DB_USER, DB_PASS 6 | from framework.asserts.common import assert_status_code, assert_message_in_response 7 | from framework.asserts.registration_asserts import check_mapping_api_to_db 8 | from framework.clients.db_client import DBClient 9 | from framework.endpoints.authenticate_api import AuthenticateAPI 10 | from framework.queries.postgres_db import PostgresDB 11 | from framework.steps.registration_steps import RegistrationSteps 12 | from framework.tools.generators import generate_string, generate_user_data 13 | 14 | # Connection configuration 15 | # PostgresDB.dbname = DB_NAME 16 | # PostgresDB.host = HOST_DB 17 | # PostgresDB.port = PORT_DB 18 | # PostgresDB.user = DB_USER 19 | # PostgresDB.password = DB_PASS 20 | 21 | 22 | @feature("Registration of user") 23 | @link( 24 | url="https://github.com/Sunagatov/Online-Store/wiki/", 25 | name="(!) WAIT LINK. Description of the tested functionality", 26 | ) 27 | class TestRegistration: 28 | email = None 29 | user_to_register = None 30 | 31 | def setup_method(self): 32 | """ 33 | Generate data for registration and user registration 34 | """ 35 | with step("Generation data for registration and user registration"): 36 | self.email = generate_string(length=10, additional_characters=["@te.st"]) 37 | first_name = generate_string(length=2) 38 | last_name = generate_string(length=2) 39 | password = generate_string( 40 | length=8, additional_characters=["@1"] 41 | ).capitalize() 42 | 43 | self.user_to_register = RegistrationSteps().data_for_sent( 44 | email=self.email, 45 | first_name=first_name, 46 | last_name=last_name, 47 | password=password, 48 | ) 49 | 50 | @pytest.mark.critical 51 | @severity(severity_level="MAJOR") 52 | @title("User registration with not unique email") 53 | @description( 54 | "WHEN the user submits data with a non-unique email for registration, " 55 | "THEN the user receives an error message, and the user's information is not stored in the database" 56 | ) 57 | def test_of_registration_email_uniqueness(self, postgres, create_authorized_user): 58 | with step("Registration of user"): 59 | user, token = ( 60 | create_authorized_user["user"], 61 | create_authorized_user["token"], 62 | ) 63 | 64 | with step("Generation data for registration with already exist email in DB"): 65 | data = generate_user_data( 66 | first_name_length=5, 67 | last_name_length=5, 68 | password_length=8, 69 | email=user["email"], 70 | ) 71 | 72 | with step("Registration user with already exist email in DB"): 73 | duplicate_registration_response = AuthenticateAPI().registration( 74 | body=data, expected_status_code=400 75 | ) 76 | 77 | with step("Checking the response body"): 78 | expected_message = "Email must be unique" 79 | assert_message_in_response( 80 | duplicate_registration_response, expected_message 81 | ) 82 | 83 | with step( 84 | "Checking that new user with duplicate email has not been registered " 85 | ): 86 | email_to_check = user["email"] 87 | result = postgres.select_user_by_email(email=email_to_check) 88 | count = result 89 | assert_that( 90 | count, 91 | equal_to(1), 92 | f"Expected 1 user with email {email_to_check}, but found {count}.", 93 | ) 94 | 95 | fields = [ 96 | # ("email", "Email is the mandatory attribute"), # Bug in the API => wrong error message for missing required field 97 | ("firstName", "First name is the mandatory attribute"), 98 | ("lastName", "Last name is the mandatory attribute"), 99 | ("password", "Password is the mandatory attribute"), 100 | ] 101 | 102 | @pytest.mark.parametrize("data", fields) 103 | @severity(severity_level="MAJOR") 104 | @title("User registration with missing required field") 105 | @description( 106 | f"WHEN the user submits data with a missing required field for registration, " 107 | "THEN the user receives an error message, and the user's information is not stored in the database" 108 | ) 109 | def test_of_registration_required_fields(self, postgres, data): 110 | with step("Preparing data for registration"): 111 | [field, expected_message] = data 112 | self.user_to_register.pop(field) 113 | 114 | with step("Registration of user"): 115 | registration_response = AuthenticateAPI().registration( 116 | body=self.user_to_register, expected_status_code=400 117 | ) 118 | 119 | with step("Checking the response body"): 120 | assert_message_in_response(registration_response, expected_message) 121 | 122 | with step( 123 | f"Checking that new user with missing field {field} has not been registered " 124 | ): 125 | user_data = postgres.get_data_by_filter( 126 | table="user_details", field="email", value=self.email 127 | ) 128 | assert_that(user_data, has_length(0)) 129 | -------------------------------------------------------------------------------- /framework/tools/methods_to_cart.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List, Dict, Any, Union 3 | 4 | from hamcrest import assert_that as hamcrest_assert_that, equal_to, has_key 5 | from requests import Response 6 | 7 | 8 | def get_product_info(response) -> List[Dict]: 9 | """Extracts product info (ID and quantity) from a response. 10 | 11 | Args: 12 | response: The JSON response. 13 | """ 14 | try: 15 | response_data = response.json() 16 | except ValueError as e: 17 | raise AssertionError(f"Response is not in valid JSON format: {e}") from e 18 | 19 | extracted_products = [] 20 | for item in response_data["items"]: 21 | product = item["productInfo"] 22 | product_info = {"id": product["id"], "productQuantity": item["productQuantity"]} 23 | 24 | extracted_products.append(product_info) 25 | return extracted_products 26 | 27 | 28 | def assert_product_to_add_matches_response( 29 | add_product: List[Dict], expected_product: List[Dict] 30 | ): 31 | """Assertion that product were added to the shopping cart 32 | 33 | Args: 34 | add_product: list of products to add to the user's shopping cart 35 | expected_product: expected products from response 36 | """ 37 | add_product_dict = {product["productId"]: product for product in add_product} 38 | 39 | for expected in expected_product: 40 | expected_id = expected["id"] 41 | expected_quantity = expected["productQuantity"] 42 | 43 | hamcrest_assert_that( 44 | add_product_dict, 45 | has_key(expected_id), 46 | f"Expected product with ID {expected_id} not found in add_product", 47 | ) 48 | 49 | matching_product = add_product_dict[expected_id] 50 | hamcrest_assert_that( 51 | matching_product["productQuantity"], 52 | equal_to(expected_quantity), 53 | f"Quantity mismatch for product {expected_id}: Expected {expected_quantity}, found {matching_product['productQuantity']} in add_product", 54 | ) 55 | 56 | 57 | def get_item_id(response) -> Union[dict, list]: 58 | """Gets item id from a JSON response. 59 | 60 | Args: 61 | response: The JSON response to extract ids from. 62 | Raises: 63 | ValueError: If JSON decoding of response fails. 64 | """ 65 | try: 66 | response_data = response.json() 67 | except ValueError: 68 | 69 | return {"error": "Invalid JSON response"} 70 | 71 | extracted_ids = [] 72 | 73 | for item in response_data.get("items", []): 74 | if item_id := item.get("id"): 75 | extracted_ids.append(item_id) 76 | 77 | return extracted_ids 78 | 79 | 80 | def get_define_quantity_items_id(response: Response, num_items_to_delete: int) -> Dict: 81 | """Gets item ids from a JSON response. 82 | Args: 83 | num_items_to_delete: the quantity of items for delete 84 | response: The JSON response to extract ids from. 85 | Raises: 86 | ValueError: If JSON decoding of response fails. 87 | """ 88 | 89 | try: 90 | response_data = response.json() 91 | except ValueError: 92 | return {"error": "Invalid JSON response"} 93 | 94 | extracted_ids = [] 95 | 96 | for item in response_data.get("items", []): 97 | if item_id := item.get("id"): 98 | extracted_ids.append(item_id) 99 | 100 | if num_items_to_delete > 0: 101 | extracted_ids = extracted_ids[:num_items_to_delete] 102 | return {"shoppingCartItemIds": extracted_ids} 103 | 104 | 105 | def extract_items_details(response: Response) -> Union[dict, list]: 106 | """Extracts item ID and product quantity from each item in the given JSON response. 107 | 108 | Args: 109 | response: The JSON response containing item details. 110 | """ 111 | try: 112 | json_response = response.json() 113 | except ValueError: 114 | return {"error": "Invalid JSON response"} 115 | 116 | extracted_details = [] 117 | 118 | for item in json_response.get("items", []): 119 | item_id = item.get("id", "") 120 | product_quantity = item.get("productQuantity", 0) 121 | extracted_details.append((item_id, product_quantity)) 122 | 123 | return extracted_details 124 | 125 | 126 | def extract_random_item_detail(response: Response) -> Dict: 127 | """Extracts item ID and product quantity of a randomly selected item from the JSON response. 128 | 129 | Args: 130 | response: The JSON response containing item details. 131 | 132 | """ 133 | try: 134 | json_response = response.json() 135 | except ValueError: 136 | return {"error": "Invalid JSON response"} 137 | 138 | items = json_response.get("items", []) 139 | 140 | if not items: 141 | raise ValueError("No items found in the JSON response.") 142 | 143 | random_item = random.choice(items) 144 | item_id = random_item.get("id", "") 145 | product_quantity = random_item.get("productQuantity", 0) 146 | return {"id": item_id, "productQuantity": product_quantity} 147 | 148 | 149 | def get_quantity_specific_cart_item(response: Response, specific_item_id: str) -> Any: 150 | """Extracts the quantity of a specific item from json response based on the provided specific ID. 151 | Args: 152 | response: The JSON response from request. 153 | specific_item_id: The specific ID of the item to find. 154 | 155 | Return: The quantity of the item with the specific ID, or a message if not found. 156 | """ 157 | try: 158 | json_response = response.json() 159 | except ValueError: 160 | return {"error": "Invalid JSON response"} 161 | data = json_response.get("items", []) 162 | for item in data: 163 | if item.get("id") == specific_item_id: 164 | return item.get("productQuantity") 165 | return f"Item with ID {specific_item_id} not found." 166 | -------------------------------------------------------------------------------- /tests/review/test_delete_review.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import description, step, title, feature 3 | from hamcrest import assert_that, is_, equal_to 4 | 5 | from data.text_review import text_review_750_char 6 | from framework.endpoints.product_api import ProductAPI 7 | from framework.endpoints.review_api import ReviewAPI 8 | from framework.tools.review_methods import ( 9 | verify_user_review_in_all_reviews, 10 | extract_product_info_from_list_of_products, 11 | ) 12 | 13 | 14 | @pytest.mark.critical 15 | @feature("Delete review and rating to product") 16 | class TestReviewWithRating: 17 | @pytest.mark.parametrize( 18 | "add_review_to_product", 19 | [{"text_review": text_review_750_char, "rating": 5}], 20 | indirect=True, 21 | ) 22 | @title("Test delete review and rating to product") 23 | @description( 24 | "GIVEN user is registered and posted review on product" 25 | "WHEN user delete review and rating to product" 26 | "THEN status HTTP CODE = 200 and product review count and average rating getting from statistics request" 27 | " and products request should be equal" 28 | ) 29 | def test_delete_review_and_rating(self, add_review_to_product): 30 | with step("Registration of user and add review to randomly selected product"): 31 | random_product_id = add_review_to_product["random_product_id"] 32 | user = add_review_to_product["user"] 33 | token = add_review_to_product["token"] 34 | product_review_id = add_review_to_product["product_review_id"] 35 | 36 | with step( 37 | "Extract review count and average rating for product before delete review" 38 | ): 39 | response_get_all_products = ProductAPI().get_all() 40 | product_info = extract_product_info_from_list_of_products( 41 | response_get_all_products, random_product_id 42 | ) 43 | product_review_count_before_delete_review = product_info[0]["reviewsCount"] 44 | product_average_rating_before_delete_review = product_info[0][ 45 | "averageRating" 46 | ] 47 | 48 | with step("Get statistic for product before delete review"): 49 | response_get_statistic = ReviewAPI().get_product_review_statistics( 50 | product_id=random_product_id 51 | ) 52 | product_review_count_statistic_before_delete_review = ( 53 | response_get_statistic.json().get("reviewsCount") 54 | ) 55 | product_average_rating_statistic_before_delete_review = ( 56 | response_get_statistic.json().get("avgRating") 57 | ) 58 | 59 | with step( 60 | "Compare data from statistic response with data from get product response before delete review" 61 | ): 62 | assert_that( 63 | product_review_count_before_delete_review, 64 | is_(equal_to(product_review_count_statistic_before_delete_review)), 65 | ) 66 | 67 | assert_that( 68 | float(product_average_rating_before_delete_review), 69 | is_( 70 | equal_to( 71 | float(product_average_rating_statistic_before_delete_review) 72 | ) 73 | ), 74 | ) 75 | 76 | with step("Delete review"): 77 | response_after_delete_review = ReviewAPI().delete_product_review( 78 | token=token, product_id=random_product_id, review_id=product_review_id 79 | ) 80 | 81 | with step( 82 | "Extract review count and average rating for product after delete review" 83 | ): 84 | response_get_all_products = ProductAPI().get_all() 85 | product_info = extract_product_info_from_list_of_products( 86 | response_get_all_products, random_product_id 87 | ) 88 | product_review_count_after_delete_review = product_info[0]["reviewsCount"] 89 | product_average_rating_after_delete_review = product_info[0][ 90 | "averageRating" 91 | ] 92 | 93 | with step("Verify that review was deleted"): 94 | response_get_all_review = ReviewAPI().get_all_product_reviews( 95 | product_id=random_product_id 96 | ) 97 | result, message = verify_user_review_in_all_reviews( 98 | response_get_all_review, 99 | user, 100 | text_review=text_review_750_char, 101 | rating=5, 102 | ) 103 | assert_that(result, is_(equal_to(False)), reason=message) 104 | 105 | with step("Get statistic for product after delete review"): 106 | response_get_statistic = ReviewAPI().get_product_review_statistics( 107 | product_id=random_product_id 108 | ) 109 | product_review_count_statistic_after_delete_review = ( 110 | response_get_statistic.json().get("reviewsCount") 111 | ) 112 | product_average_rating_statistic_after_delete_review = ( 113 | response_get_statistic.json().get("avgRating") 114 | ) 115 | 116 | with step( 117 | "Compare data from statistic response with data from get product response after delete review" 118 | ): 119 | assert_that( 120 | product_review_count_after_delete_review, 121 | is_(equal_to(product_review_count_statistic_after_delete_review)), 122 | ) 123 | 124 | assert_that( 125 | float(product_average_rating_after_delete_review), 126 | is_( 127 | equal_to( 128 | float(product_average_rating_statistic_after_delete_review) 129 | ) 130 | ), 131 | ) 132 | -------------------------------------------------------------------------------- /tests/authentication/test_fogot_password.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from allure import feature, description, link, step, title 3 | from allure import severity 4 | from hamcrest import assert_that, is_, not_, empty, is_not 5 | from assertpy import assert_that as assertpy_assert_that 6 | 7 | from configs import ( 8 | email_iced_late, 9 | imap_server, 10 | email_address_to_connect, 11 | gmail_password, 12 | firstName, 13 | lastName, 14 | password, 15 | ) 16 | from framework.asserts.common import assert_content_type, assert_response_message 17 | from framework.endpoints.authenticate_api import AuthenticateAPI 18 | from framework.tools.class_email import Email 19 | from framework.tools.generators import faker 20 | 21 | 22 | @feature("Forgot password") 23 | @link( 24 | url="http://localhost:8083/api/docs/swagger-ui/index.html#/Security/forgotPassword", 25 | ) 26 | class TestForgotPassword: 27 | @pytest.mark.critical 28 | @severity(severity_level="CRITICAL") 29 | @title("Test forgot password") 30 | @description( 31 | "Give user registered." 32 | "WHEN user sent request to reset password." 33 | "THEN status HTTP CODE = 200 " 34 | ) 35 | @pytest.mark.parametrize( 36 | "registration_and_cleanup_user_through_api", 37 | [ 38 | { 39 | "firstName": firstName, 40 | "lastName": lastName, 41 | "password": password, 42 | "email_iced_late": email_iced_late, 43 | "imap_server": imap_server, 44 | "email_address_to_connect": email_address_to_connect, 45 | "gmail_password": gmail_password, 46 | } 47 | ], 48 | indirect=True, 49 | ) 50 | def test_forgot_password(self, registration_and_cleanup_user_through_api): 51 | with step("Registration user"): 52 | user = registration_and_cleanup_user_through_api["user"] 53 | email = user["email"] 54 | 55 | with step("Sent request to reset password"): 56 | response_to_reset_password = AuthenticateAPI().forgot_password(email=email) 57 | assert_that(response_to_reset_password.status_code, is_(200)) 58 | 59 | with step("Verify reset code successfully delivered to user's email"): 60 | email_box = "Inbox" 61 | key = "from_" 62 | value = email_iced_late 63 | code_from_email = Email( 64 | imap_server=imap_server, 65 | email_address=email_address_to_connect, 66 | mail_password=gmail_password, 67 | ).extract_confirmation_code_from_email( 68 | email_box=email_box, key=key, value=value 69 | ) 70 | assertpy_assert_that(code_from_email).is_not_empty() 71 | 72 | @pytest.mark.xfail( 73 | reason="HTTP CODE - 400 but got 401. Error message not correct according to requirement" 74 | ) 75 | @pytest.mark.critical 76 | @severity(severity_level="MAJOR") 77 | @title( 78 | "Test forgot password, sent request to reset password with invalid/empty email" 79 | ) 80 | @description( 81 | "Give user registered." 82 | "WHEN user sent request to reset password with invalid format email, or empty email field." 83 | "THEN status HTTP CODE = 400, and error message" 84 | ) 85 | @pytest.mark.parametrize( 86 | "email_to_reset_password, expected_message", 87 | [ 88 | pytest.param("", "Email is the mandatory attribute"), 89 | pytest.param("example.gmail.com", "Email must be valid"), 90 | pytest.param("sernamegmail.com", "Email must be valid"), 91 | pytest.param("sername@", "Email must be valid"), 92 | pytest.param("john doe@gmail.com", "Email must be valid"), 93 | pytest.param("user!name@gmail.com", "Email must be valid"), 94 | pytest.param("@gmail.com", "Email must be valid"), 95 | pytest.param("username@gmail", "Email must be valid"), 96 | pytest.param("user@name@gmail.com", "Email must be valid"), 97 | pytest.param(".username@gmail.com", "Email must be valid"), 98 | pytest.param("username@example_gmail.com", "Email must be valid"), 99 | ], 100 | ) 101 | def test_forgot_password_email_not_valid( 102 | self, email_to_reset_password, expected_message 103 | ): 104 | with step("Sent request to reset password with invalid email"): 105 | response_to_reset_password = AuthenticateAPI().forgot_password( 106 | email=email_to_reset_password, expected_status_code=400 107 | ) 108 | assert_response_message( 109 | response=response_to_reset_password, expected_message=expected_message 110 | ) 111 | assert_content_type(response_to_reset_password, "application/json") 112 | 113 | @pytest.mark.xfail(reason="Error message not correct") 114 | @pytest.mark.critical 115 | @severity(severity_level="MAJOR") 116 | @title("Test forgot password, send request to reset password with not exist email") 117 | @description( 118 | "Give user registered." 119 | "WHEN user sent request to reset password with not exist email in BD." 120 | "THEN status HTTP CODE = 401, and error message" 121 | ) 122 | def test_forgot_password_email_not_exist(self): 123 | with step("Sent request to reset password with not exist email in BD"): 124 | email_not_exist = faker.email() 125 | response_to_reset_password = AuthenticateAPI().forgot_password( 126 | email=email_not_exist, expected_status_code=401 127 | ) 128 | expected_message = "Email must be valid" 129 | assert_response_message( 130 | response=response_to_reset_password, expected_message=expected_message 131 | ) 132 | assert_content_type(response_to_reset_password, "application/json") 133 | -------------------------------------------------------------------------------- /framework/tools/generators.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | import random 4 | import string 5 | from random import choice 6 | from typing import Optional, Any 7 | 8 | import bcrypt 9 | import jwt 10 | from faker import Faker 11 | 12 | from configs import DEFAULT_PASSWORD, JWT_SECRET 13 | 14 | faker = Faker() 15 | 16 | 17 | def generate_string(length: int, additional_characters: list = None) -> str: 18 | """Generating a string of the specified length with the possibility of adding special characters 19 | 20 | Args: 21 | length: length of the generated string; 22 | additional_characters: addition of special characters. 23 | """ 24 | result = [choice(string.ascii_lowercase) for _ in range(length)] 25 | if additional_characters: 26 | result += additional_characters 27 | 28 | return "".join(result) 29 | 30 | 31 | def generate_user( 32 | first_name_length: Optional[int] = None, 33 | last_name_length: Optional[int] = None, 34 | password: str = DEFAULT_PASSWORD, 35 | with_address: bool = False, 36 | email: Optional[str] = None, 37 | **kwargs, 38 | ): 39 | """ 40 | Generate a user with customizable attributes. 41 | 42 | Args: 43 | email: specific email for user. 44 | first_name_length: Optional[int] - Length of the first name. 45 | last_name_length: Optional[int] - Length of the last name. 46 | password: password for user. 47 | with_address: Include address information if True. 48 | **kwargs: Additional attributes to override. 49 | 50 | Returns: 51 | dict: Generated user data. 52 | """ 53 | encrypted_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() 54 | 55 | user_data = { 56 | "id": faker.uuid4(), 57 | "firstName": generate_string(first_name_length) 58 | if first_name_length is not None 59 | else faker.first_name(), 60 | "lastName": generate_string(last_name_length) 61 | if last_name_length is not None 62 | else faker.last_name(), 63 | "email": email if email is not None else faker.email(), 64 | "birthDate": faker.date_of_birth().strftime("%Y-%m-%d"), 65 | "phoneNumber": faker.phone_number(), 66 | "stripeCustomerToken": faker.uuid4(), 67 | "password": password, 68 | "hashed_password": encrypted_password, 69 | } 70 | 71 | if with_address: 72 | user_data["address"] = { 73 | "country": faker.country(), 74 | "city": faker.city(), 75 | "line": faker.street_address(), 76 | "postcode": faker.postcode(), 77 | } 78 | 79 | user_data.update(kwargs) 80 | 81 | return user_data 82 | 83 | 84 | def generate_jwt_token(email: str = "", expired: bool = False) -> str: 85 | """Generating a JWT token 86 | 87 | Args: 88 | email: user email 89 | expired: flag for expired token (True - expired, False - not expired) 90 | """ 91 | 92 | payload = {"sub": email, "iat": datetime.datetime.now(datetime.timezone.utc)} 93 | if expired: 94 | payload["exp"] = datetime.datetime.now( 95 | datetime.timezone.utc 96 | ) - datetime.timedelta(seconds=1) 97 | else: 98 | payload["exp"] = datetime.datetime.now( 99 | datetime.timezone.utc 100 | ) + datetime.timedelta(days=1) 101 | 102 | encoded_secret_key = JWT_SECRET 103 | secret_key = base64.b64decode(encoded_secret_key) 104 | token = jwt.encode(payload, secret_key, algorithm="HS256", headers={"alg": "HS256"}) 105 | 106 | return token 107 | 108 | 109 | def generate_password(length: int) -> str: 110 | """Function generates and returns random generated password. 111 | 112 | Args: 113 | length: length of password 114 | """ 115 | letters = string.ascii_letters 116 | digits = string.digits 117 | special_chars = "!@%*$&" 118 | all_chars = digits + special_chars + letters 119 | password = "".join(random.choice(all_chars) for _ in range(length)) 120 | 121 | """Digits and letters is required char in password. This statement add 1 digits and letter to the password" \ 122 | if it absent during generation random password """ 123 | if all(char not in digits for char in password): 124 | random_index = random.randint(0, length - 1) 125 | password = ( 126 | password[:random_index] + random.choice(digits) + password[random_index:] 127 | ) 128 | if all(char not in letters for char in password): 129 | random_index = random.randint(0, length - 1) 130 | password = ( 131 | password[:random_index] + random.choice(letters) + password[random_index:] 132 | ) 133 | 134 | return password 135 | 136 | 137 | def generate_user_data( 138 | password_length: int, 139 | first_name_length: int, 140 | last_name_length: int, 141 | email: Any = faker.email(), 142 | ) -> dict: 143 | """Function for generation random user data 144 | 145 | Args: 146 | email: email to register 147 | password_length: length generated password 148 | first_name_length: length generated string 149 | last_name_length: length generated string 150 | """ 151 | first_name = "".join( 152 | random.choice(string.ascii_lowercase) for _ in range(first_name_length) 153 | ) 154 | last_name = "".join( 155 | random.choice(string.ascii_lowercase) for _ in range(last_name_length) 156 | ) 157 | return { 158 | "firstName": first_name, 159 | "lastName": last_name, 160 | "password": generate_password(password_length), 161 | "email": email, 162 | } 163 | 164 | 165 | def generate_numeric_password(length: int) -> str: 166 | """Generate a numeric password. 167 | 168 | Parameters: 169 | - length: int, the length of the password to generate. 170 | 171 | Returns: 172 | - A string representing the generated password. 173 | """ 174 | 175 | return "".join(str(random.randint(0, 9)) for _ in range(length)) 176 | 177 | 178 | def append_random_to_local_part_email( 179 | domain: str = "", email_local_part: str = "", length_random_part: int = 5 180 | ): 181 | """Generates a random email address based on the existing prefix and domain email. 182 | 183 | Args: 184 | length_random_part: Length of the random part of the email address. 185 | email_local_part: Prefix of the existing email address. 186 | domain: Domain of the existing email address. 187 | """ 188 | random_part = "".join( 189 | random.choice(string.ascii_letters + string.digits) 190 | for _ in range(length_random_part) 191 | ) 192 | return f"{email_local_part}{random_part}@{domain}" 193 | --------------------------------------------------------------------------------