├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── API ├── DATA │ ├── email_invalid.py │ ├── loopback_url.py │ ├── original_url_invalid.py │ ├── password_invalid.py │ ├── user_invalid.py │ └── user_valid.py ├── FRAMEWORK │ ├── api_endpoints │ │ ├── __init__.py │ │ ├── api_auth.py │ │ ├── api_short_link.py │ │ └── api_url.py │ ├── assertion │ │ ├── __init__.py │ │ ├── assert_content_type.py │ │ ├── assert_response_header_value.py │ │ ├── assert_response_message.py │ │ ├── assert_status_code.py │ │ ├── assert_unique_short_url.py │ │ └── assert_user_in_mongodb.py │ ├── mongodb │ │ ├── MongoDB.py │ │ └── __init__.py │ └── tools │ │ ├── __init__.py │ │ ├── loggin_allure.py │ │ ├── redirect_to_original_url.py │ │ ├── take_hash_from_url.py │ │ └── verification_time_expiration.py ├── TEST │ ├── __init__.py │ ├── conftest.py │ ├── test_create_short_url.py │ ├── test_create_short_url_negative.py │ ├── test_expiration_time.py │ ├── test_loop_redirection.py │ ├── test_redirection_to_original_url.py │ ├── test_sign_in_negative.py │ ├── test_sign_in_positive.py │ ├── test_sign_up.py │ ├── test_sign_up_negative.py │ └── test_uniqueness_short_url.py └── __init__.py ├── README.md ├── configs.py ├── pytest.ini └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.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 | venv 52 | .env 53 | .venv 54 | env/ 55 | venv/ 56 | ENV/ 57 | env.bak/ 58 | venv.bak/ 59 | 60 | # PyCharm 61 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 62 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 63 | # and can be added to the global gitignore or merged into this file. For a more nuclear 64 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 65 | .idea/ 66 | 67 | # Mac 68 | .DS_Store 69 | 70 | # Allure 71 | allure-report 72 | allure-results 73 | allure_report 74 | allure_results 75 | # Config 76 | 77 | tests/__pycache__/ 78 | data/data_for_auth.py 79 | my_data.py 80 | 81 | # VSCode 82 | .vscode/ -------------------------------------------------------------------------------- /API/DATA/email_invalid.py: -------------------------------------------------------------------------------- 1 | EMAIL_INVALID = [ 2 | # email without @ 3 | '12345gmail.com', 4 | 5 | # domain-less email 6 | '12345@', 7 | 8 | # with spaces in the email 9 | '12345 @gmail.com', 10 | 11 | # email beginning with “.” 12 | '.12345@gmail.com', 13 | 14 | # email ending with “.” 15 | '12345@gmail.com.', 16 | 17 | # email with “..” 18 | '12345@gmail..com.', 19 | 20 | # email longer than 64 characters 21 | '152345123545123451234512345123451234512345123451234QWer@gmail.com', 22 | 23 | # email less than 7 characters 24 | 'x@x.xx' 25 | ] 26 | -------------------------------------------------------------------------------- /API/DATA/loopback_url.py: -------------------------------------------------------------------------------- 1 | LOOPBACK_URL = [("https://short-link.zufargroup.com/", "URL cannot point to a loopback address"), 2 | ("https://short-link.zufargroup.com/url/3DF3D", "URL cannot point to a loopback address")] 3 | -------------------------------------------------------------------------------- /API/DATA/original_url_invalid.py: -------------------------------------------------------------------------------- 1 | # (invalid URL, error message) 2 | ORIGINAL_URL_INVALID = [ 3 | (( 4 | 'https://KXZUrAEbJInXGl9mA8eiMa9JJhXrkQIJ0zqP1CMwpMtunRLaErWquvQoIaNqlJz3pZBeUWMcqAQ2Ekn1' 5 | '6bn2rI7L9WGV7EmA3Gy45GYw9mmq62DJYExUh9sqGSdpbZ6GKB4tcUTXGQedd5Ha2b8BKcEf1Fl0e6ZNEafx2NO' 6 | 'nBCw2h1oPdjaME9BtcIgfp02soqPm6RKxoaNvHRcAFoWdQLxodeieJeWJAOUyy9bm3hRGEVb8ksShFVv1tNCt0B' 7 | 'ivVzYqjvSOKjXZv8HA5U1fOk8veNjfhHGz9JcNBbbQClgREl1wIF26RDuuUi7jGcNxEyJvM0699FrldJtYllVW6' 8 | 'ncfNqksil2OAoV6Ayn8r4BEMW5gCwEIu3dKBjwfAycCadC1svmFMBpBrwJyVoKIg32CpjF5bI22aV6qrwV2toLC' 9 | 'ARh32jyF7a5ehQSJjur63g1BunukZZfFi0RjWTZEkBO7ZH5KU9ZlsmQWN7nivdstWQN6Wb8buX2N9q0RxZKzaWD' 10 | '5ta3XCRtQ4wj9kBrsfm5YR3TdmM5VYppZJolmequVDAHHC4BCIj8zvXOhdMRRcWgoCG2FDhnYCQyiG7wfbjtYIT' 11 | '2Tf2eSlykJhjQT8bqStwi0AxJvpMI54iGv22ZbE0db5B3R3QKiwXE5Dz7xjFYCEIME2hNKYQa4rXHElqB0Xo0Ew' 12 | 'FVxYDxGAmOF7ZDpw1tAPZ5dOXXRIaPSv2dzeR47lZDKqYeiXsufrNZUy81JReCzXdyLKiQaTdvK2Rz7lkI5p4rg' 13 | 'qURAWPfUPp2r0b3Wyr2NHvF3kuGdBoWsZJ7TEXusn8rkmAPLcmnslQvGnXsRTljR9lYnsKK5Noqt3DpwknoRoxa' 14 | 'lCHqsjs16aJM1PWhirykMki9FPdWcBa6CdF7uTDs96SugulOylQ9h673Q1QQuDcFXlwXoQL9dFIDb5OzhEAJKqA' 15 | 'GRDx1YowwKvPhYBandRD8qRD1ygntkjpfUBWtocafLPnXJpM3EX0d9cgmQGDqMBKUeOKaoXfyXBv6kxfNGhE2bs' 16 | 'ToxSfsOqxz34RTTiU2MGlNslGOSmACh9Y9HPE3L2YAUcQESsOUluiYmj8qu4qxycB6F8JpSmvGWtKGxHPYXLlje' 17 | 'llUFWDIPlc3ODHGtCbFDwhPEO92wEwSVNaOXt0l1fEgvr2hPbcFH8tpYr74bF2OwTUqegsjHEsmDqZX8Z95f6hx' 18 | 'iC70PRpFRAiRt34Ej7kr58cZkIhGEcokGvwhXuKz5ildsJolpwEgMicbNtiiggHFqiSymP5ApUqzxjNtx87gbVv' 19 | 'KRb8bLGAqqmgzi4x65TjUSOqB7s1MapLkKNy3SJAowIU7Rkz07bUyFg99h7R8fHI7sQKmHu43bM33qSBCPUTYqh' 20 | 'EeCVYSjsJ226yJQswJV0xtbvnUBOdQARVyrSBWB6WxioUM8N6nnlYkvPhai1GCcAUQowCredx5OMJS5VNplTWsu' 21 | 'rX2O68MiX8N02Z3Wy08oI659EGNMYyKfOWLKQMfKlBR8QjsXlDl4D7kqcENgvCKQ6KrFipsBXFqKLgpXgv90QHd' 22 | '8yPkUPv0tuz9WA3HHWX6xGDEsGDc19cG8yed1aK4JxqE0LQKcSsytUN33pvJInNpZEBSsXDCP2FuGOQgJXVpRSg' 23 | 'm4QaOwMzpe5PMsoIUthDKYn9lPr412ofgBOGPvHXXNwJnFekUDweqLLQUWyeL0Pic77PVRkmAM3uIB2LXudX7W6' 24 | 'E41tEceh62kGcWJtQ2lAuKy4Lw1Z3tqzc89oXzp8eJNhzUSpkayAAyEyzzR8yjiybknl8hMVjBV9pnHkqdswkFr' 25 | 'zItZQCqdWppXwe0MZzstIdoHNaAHf5U38904AZrH0IjRTVbDu2TDp1NDuhVBjTrrZC11PSXNTBaIBvDA83LudHX' 26 | 'Q1EfPBqTyeQU2RFoJhIAeiTnOCiXElhO31IaUTMrBsejet07gX4luqlhWl3ko3zm5WeAMj3Q6ipigRJO3cWKDiA' 27 | 'zkWN.EP' 28 | ), 29 | 'URL is not valid. Please ensure it has the correct format and syntax.'), 30 | ('', 31 | 'URL must not be empty or blank.'), 32 | (' ', 33 | 'URL must not be empty or blank.'), 34 | ('https://ic. ed-la. tte.uk/', 35 | 'URL must not contain spaces.'), 36 | ('http://.com', 37 | 'URL is not valid. Please ensure it has the correct format and syntax.'), 38 | ('https://iced-latte..uk', 39 | 'URL is not valid. Please ensure it has the correct format and syntax.'), 40 | # long label - max: 63 symbols 41 | ('https://2hJF3sxaEQPmjBD2R7GVfhviUqgkhhxhdHwfNiO9nMOURq0Pvz5mGTBBdlmGIWnqwertyuiop.uk', 42 | 'URL is not valid. Please ensure it has the correct format and syntax.'), 43 | ('https://uk', 44 | 'URL is not valid. Please ensure it has the correct format and syntax.'), 45 | ('https://', 46 | 'URL is not valid. Please ensure it has the correct format and syntax.', 47 | ), 48 | ('ftp://example.com/file', 49 | 'URL must have a proper scheme (http or https).') 50 | ] 51 | -------------------------------------------------------------------------------- /API/DATA/password_invalid.py: -------------------------------------------------------------------------------- 1 | PASSWORD_INVALID = [ 2 | # less 8 characters 3 | 'Pass1!', 4 | 5 | # longer 50 characters 6 | 'Password12345Password12345!Password12345Password123', 7 | 8 | # without uppercase 9 | 'password123!', 10 | 11 | # without lowercase 12 | 'PASSWORD123!', 13 | 14 | # without digits 15 | 'Password!', 16 | 17 | # with spaces 18 | 'Pas sword123!', 19 | 20 | # with invalid characters 21 | 'Пароль123!' 22 | ] 23 | -------------------------------------------------------------------------------- /API/DATA/user_invalid.py: -------------------------------------------------------------------------------- 1 | # (first name, last name, email, password, country, age) 2 | 3 | USER_EMAIL_EMPTY = [ 4 | ('Ilya', 'Ilyin', '', '123456Qwerty!', 'Russia', 28) 5 | ] 6 | 7 | USER_EMAIL_INVALID = [ 8 | # email without @ 9 | ('Ilya', 'Ilyin', '12345gmail.com', '123456Qwerty!', 'Russia', 28), 10 | 11 | # domain-less email 12 | ('Ilya', 'Ilyin', '12345@', '123456Qwerty!', 'Russia', 28), 13 | 14 | # with spaces in the email 15 | ('Ilya', 'Ilyin', '12345 @gmail.com', '123456Qwerty!', 'Russia', 28), 16 | 17 | # email beginning with “.” 18 | ('Ilya', 'Ilyin', '.12345@gmail.com', '123456Qwerty!', 'Russia', 28), 19 | 20 | # email ending with “.” 21 | ('Ilya', 'Ilyin', '12345@gmail.com.', '123456Qwerty!', 'Russia', 28), 22 | 23 | # email with “..” 24 | ('Ilya', 'Ilyin', '12345@gmail..com.', '123456Qwerty!', 'Russia', 28), 25 | ] 26 | 27 | USER_EMAIL_LONG = [ 28 | ('Ilya', 'Ilyin', ('mail@vOwMHeEWodFHcVaInngAkixEWDdT.mmnvudSGWjeHIHbJPecEF4gwX62xS' 29 | '.fkXJv4I365ylcfn7T4kvnCd91G9uz8OdH5RrNc6TsW.4AyHZg1FLNSTjjz1YO6IVHqE2v7pQLeGxySv0' 30 | '.asKIyyKSNxXLQqYakc0MfXoXIX8TQNX5.CKugWYrbcjYgchqzerhPGUM1ItNfTXYRbO' 31 | '.oT8Bk1Qnk6gCXg52Uwhqweeewwwwww5Dg5svDdAd5AxrFWazB.5P'), '123456Qwerty!', 'Russia', 28) 32 | ] 33 | 34 | USER_PASSWORD_EMPTY = [ 35 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '', 'Russia', 28) 36 | ] 37 | 38 | USER_PASSWORD_SHORT = [ 39 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'Pass1!', 'Russia', 28) 40 | ] 41 | 42 | USER_PASSWORD_WITHOUT_UPPERCASE = [ 43 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'password123!', 'Russia', 28) 44 | ] 45 | 46 | USER_PASSWORD_WITHOUT_LOWERCASE = [ 47 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'PASSWORD123!', 'Russia', 28) 48 | ] 49 | 50 | USER_PASSWORD_WITHOUT_DIGITS = [ 51 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'Password!', 'Russia', 28) 52 | ] 53 | 54 | USER_PASSWORD_WITHOUT_SPEC_CHAR = [ 55 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'Password123', 'Russia', 28) 56 | ] 57 | 58 | USER_PASSWORD_WITH_SPACES = [ 59 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'Pas sword123!', 'Russia', 28) 60 | ] 61 | 62 | USER_PASSWORD_LONG = [ 63 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'm8xSe8HYiZ2eVxTYUYXQmpmqNkbicXsyatIQLtCQftC3jlMvUM!', 'Russia', 28) 64 | ] 65 | 66 | USER_PASSWORD_INVALID = [ 67 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', 'Пароль123!', 'Russia', 28) 68 | ] 69 | 70 | USER_FIRST_NAME_EMPTY = [ 71 | ('', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28) 72 | ] 73 | 74 | USER_FIRST_NAME_INVALID = [ 75 | ('Alex12', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28), 76 | ('Mel@man!', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28), 77 | ('Anna Gloria', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28) 78 | ] 79 | 80 | USER_FIRST_NAME_LONG = [ 81 | ('AnnanAnnanAnnanAnnanAnnanAnnanAnnanAnnanAnnanAnnana', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 82 | 'Russia', 28) 83 | ] 84 | 85 | USER_LAST_NAME_EMPTY = [ 86 | ('Ilya', '', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28) 87 | ] 88 | 89 | USER_LAST_NAME_INVALID = [ 90 | ('Ilya', 'Testov12', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28), 91 | ('Ilya', 'Test)ov!', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28), 92 | ('Ilya', 'Oblo mov', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 28) 93 | ] 94 | 95 | USER_LAST_NAME_LONG = [ 96 | ('Ilya', 'PetrovpetrPetrovpetrPetrovpetrPetrovpetrPetrovpetre', '12345mailtest@gmail.com', '123456Qwerty!', 97 | 'Russia', 28) 98 | ] 99 | 100 | USER_COUNTRY_INVALID = [ 101 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'USA123', 21), 102 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'USA@', 21) 103 | ] 104 | 105 | USER_COUNTRY_EMPTY = [ 106 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', '', 21) 107 | ] 108 | 109 | USER_COUNTRY_LONG = [ 110 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 111 | 'AReallyLongCountryNameThatExceedsTheMaximumAllowedLengthForTestingPurposes', 21), 112 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 113 | 'RussiaRussiaRussiaRussiaRussiaRussiaRussiaRussiaRus', 21) 114 | ] 115 | 116 | USER_AGE_INVALID = [ 117 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', None), 118 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 12), 119 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 121), 120 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 0), 121 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', -1), 122 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', '121') 123 | ] 124 | 125 | USER_AGE_NOT_NUMBER = [ 126 | ('Ilya', 'Ilyin', '12345mailtest@gmail.com', '123456Qwerty!', 'Russia', 'qwerty') 127 | ] 128 | -------------------------------------------------------------------------------- /API/DATA/user_valid.py: -------------------------------------------------------------------------------- 1 | # (first name, last name, email, password, country, age) 2 | USER_VALID = [ 3 | ('A', 'A', 'mail123mail@ma123il.ru', '12345Qw!', 'B', 13), 4 | ('Ilya', 'Ilya', 'ZoxBfhLiynUuhzFxTdrtqzsTIqbsA4er8yeemYakgmHpvyCW3cXYu2oFT43@m.ru', '1234567Qwerty$', 5 | 'France', 28), 6 | ('iilGQgcrJpGkWHDQIPpzjGowkxVXwcEADCckNUDqsTwPeeVQMw', 'iilGQgcrJpGkWHDQIPpzjGowkxVXwcEADCckNUDqsTwPeeVQMw', 7 | 'ma@UM1ItNfTXYRbO.oT8Bk1Qnk6gCXg52Uwh5Dg5svDdAd5AxrFazB.qwerty.RU', 8 | '9BUYjF4hGiMGpbBGdoSwWVh2IuZ80K1EZwHepQWLedzwbsIzo%', 'OIcEYgBptFcrLVBOdebBzCfzBpFLxPnHaxwsMLCickFgFrIHTw', 120), 9 | ('Ilya-Petya', 'Ilya-Petya', 'mail_mail@mail.ru', '1234567Qwerty$', 'Russian Federation', 28), 10 | ("Ilya'Petya", "Ilya'Petya", 'mail-mail@mail.ru', '1234567Qwerty$', 'FRANCE', 28), 11 | ("Ilya-Petya'Vova", "Ilya-Petya'Vova", 'mail+mail@mail.ru', '1234567Qwerty$', 'france', 28), 12 | ('Ilya', 'Ilya', 'mail@mail-mail.ru', '1234567Qwerty$', 'France', 28) 13 | ] 14 | 15 | USER_TO_CREATE = [('Tetiana', 'Test', 'test@mail.ru', '1234567Qwerty$', 'France', 28)] 16 | -------------------------------------------------------------------------------- /API/FRAMEWORK/api_endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/FRAMEWORK/api_endpoints/__init__.py -------------------------------------------------------------------------------- /API/FRAMEWORK/api_endpoints/api_auth.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests import Response 3 | from allure import step 4 | 5 | from API.FRAMEWORK.tools.loggin_allure import log_request 6 | from configs import HOST 7 | 8 | 9 | class AuthAPI: 10 | def __init__(self): 11 | self.url = f'{HOST}/api/v1/auth' 12 | 13 | @step('Sign-Up') 14 | def sign_up( 15 | self, 16 | first_name: str, 17 | last_name: str, 18 | email: str, 19 | password: str, 20 | country: str, 21 | age: int 22 | ) -> Response: 23 | path = f'{self.url}/signup' 24 | body = { 25 | "firstName": first_name, 26 | "lastName": last_name, 27 | "email": email, 28 | "password": password, 29 | "country": country, 30 | "age": age 31 | } 32 | response = requests.post(url=path, json=body) 33 | log_request(response) 34 | return response 35 | 36 | @step('Sign-in') 37 | def sign_in(self, email: str, password: str) -> Response: 38 | path = f'{self.url}/signin' 39 | body = { 40 | "email": email, 41 | "password": password, 42 | } 43 | response = requests.post(url=path, json=body) 44 | log_request(response) 45 | return response 46 | -------------------------------------------------------------------------------- /API/FRAMEWORK/api_endpoints/api_short_link.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import requests 4 | from requests import Response 5 | 6 | from API.FRAMEWORK.tools.loggin_allure import log_request 7 | from configs import HOST 8 | 9 | 10 | class ShorteningLinkAPI: 11 | def __init__(self): 12 | """Initializing parameters for request""" 13 | self.url = f"{HOST}/api/v1/urls" 14 | self.headers = {"Content-Type": "application/json"} 15 | 16 | def shorten_link( 17 | self, url: str, days_count: Optional[str] = None, expected_status_code: int = 200 18 | ) -> Response: 19 | """Endpoint for creating short link 20 | 21 | Args: 22 | expected_status_code: expected http status code from response 23 | url: original link to shorten; 24 | days_count: time to expire the short link in days 25 | 26 | """ 27 | data = { 28 | 29 | "originalUrl": url, 30 | } 31 | if days_count is not None: 32 | data["daysCount"] = days_count 33 | path = self.url 34 | response = requests.post(url=path, json=data, headers=self.headers) 35 | log_request(response) 36 | return response 37 | -------------------------------------------------------------------------------- /API/FRAMEWORK/api_endpoints/api_url.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests import Response 3 | from allure import step 4 | 5 | from API.FRAMEWORK.tools.loggin_allure import log_request 6 | from configs import HOST 7 | 8 | 9 | class UrlAPI: 10 | def __init__(self): 11 | self.url = f'{HOST}/api/v1/urls' 12 | 13 | @step('Delete short URL') 14 | def delete_short_url(self, short_url: str) -> Response: 15 | with step('Get short URL hash'): 16 | short_url_hash = short_url.rsplit('/', 1)[-1] 17 | 18 | with step('Send request to delete short URL'): 19 | path = f'{self.url}/{short_url_hash}' 20 | response = requests.delete(url=path) 21 | log_request(response) 22 | return response 23 | -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/FRAMEWORK/assertion/__init__.py -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_content_type.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, contains_string 2 | from requests import Response 3 | 4 | 5 | def assert_content_type(response: Response, expected_content_type: str) -> None: 6 | """Asserts that the Content-Type of the response matches the expected Content-Type. 7 | 8 | Args: 9 | response: The response object from the API call. 10 | expected_content_type: The expected Content-Type string. 11 | """ 12 | content_type = response.headers.get("Content-Type", "") 13 | assert_that( 14 | content_type, 15 | contains_string(expected_content_type), 16 | reason=f"Expected Content-Type '{expected_content_type}', found: '{content_type}'", 17 | ) -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_response_header_value.py: -------------------------------------------------------------------------------- 1 | from allure import step 2 | from hamcrest import assert_that, is_ 3 | from requests import Response 4 | 5 | 6 | def assert_response_header_value(response: Response, header: str, expected_header_value: str) -> None: 7 | with step(f'Verify that header "{header}" has value "{expected_header_value}"'): 8 | actual_header_value = response.headers.get('Location') 9 | assert_that(actual_header_value, is_(expected_header_value), 10 | reason=f'The "{header}" header has not value "{expected_header_value}"') 11 | -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_response_message.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, contains_string 2 | from requests import Response 3 | 4 | 5 | def assert_message_in_response(response: Response, expected_message: str) -> None: 6 | """Asserts that the message in the response body matches the expected message. 7 | 8 | Args: 9 | response: The response object from the API call. 10 | expected_message: The expected message string. 11 | """ 12 | actual_message = response.json().get("errorMessage", "") 13 | assert_that( 14 | actual_message, 15 | contains_string(expected_message), 16 | reason=f"Expected response contains '{expected_message}', found: '{actual_message}'", 17 | ) 18 | -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_status_code.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, is_ 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 | -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_unique_short_url.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | from allure import step 4 | 5 | 6 | @step('Verify that created short url is unique') 7 | def is_unique_short_url(created_short_url: str, short_url_list: Sequence[str]) -> None: 8 | count = short_url_list.count(created_short_url) 9 | assert count == 1, f'Created short url appears {count} times in the list, expected exactly once.' 10 | -------------------------------------------------------------------------------- /API/FRAMEWORK/assertion/assert_user_in_mongodb.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from allure import step 4 | from dotenv import load_dotenv 5 | from hamcrest import assert_that, is_ 6 | 7 | from API.FRAMEWORK.mongodb.MongoDB import MongoDB 8 | from configs import MONGODB_DATABASE, MONGODB_COLLECTION_USER 9 | 10 | 11 | # Load secret config from a .env file: 12 | load_dotenv() 13 | mongodb_uri = os.environ.get('MONGODB_URI') 14 | 15 | 16 | @step('Verify that user is in the MongoDB') 17 | def is_user_in_mongodb(email: str) -> None: 18 | with step('Create MongoDB client'): 19 | mongodb_client = MongoDB(mongodb_uri, MONGODB_DATABASE, MONGODB_COLLECTION_USER) 20 | with step(f'Count users with email {email}'): 21 | count_users = mongodb_client.count_users(email) 22 | with step('Verify that count users is 1'): 23 | assert_that( 24 | count_users, 25 | is_(1), 26 | reason='User was not found in MongoDB' 27 | ) 28 | -------------------------------------------------------------------------------- /API/FRAMEWORK/mongodb/MongoDB.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import certifi 4 | from pymongo import MongoClient, UpdateOne 5 | from allure import step 6 | 7 | 8 | class MongoDB: 9 | def __init__(self, connection_string: str, db_name: str, db_collection: str) -> None: 10 | self.connection_string = connection_string 11 | self.db_name = db_name 12 | self.db_collection = db_collection 13 | 14 | # Connect to MongoDB cluster with SSL certificate verification 15 | self.mongodb_client = MongoClient(self.connection_string, tls=True, tlsCAFile=certifi.where()) 16 | 17 | def close_connection(self) -> None: 18 | """Close the MongoDB connection.""" 19 | self.mongodb_client.close() 20 | 21 | @step('Delete created short URL from MongoDB') 22 | def delete_created_short_url(self, created_short_url: str) -> int: 23 | with step(f'Get {self.db_name}'): 24 | shorty_url_db = self.mongodb_client[self.db_name] 25 | 26 | with step(f'Get {self.db_collection} collection'): 27 | url_mappings_collection = shorty_url_db[self.db_collection] 28 | 29 | with step('Delete created short URL'): 30 | query = {"shortUrl": created_short_url} 31 | result = url_mappings_collection.delete_one(query) 32 | return result.deleted_count 33 | 34 | @step('Count users by email in MongoDB') 35 | def count_users(self, email: str) -> int: 36 | with step(f'Get {self.db_name}'): 37 | shorty_url_db = self.mongodb_client[self.db_name] 38 | 39 | with step(f'Get {self.db_collection} collection'): 40 | user_details_collection = shorty_url_db[self.db_collection] 41 | 42 | with step('Count users'): 43 | query = {"email": email} 44 | count = user_details_collection.count_documents(query) 45 | return count 46 | 47 | @step('Delete user from MongoDB') 48 | def delete_user(self, email: str) -> int: 49 | with step(f'Get {self.db_name}'): 50 | shorty_url_db = self.mongodb_client[self.db_name] 51 | 52 | with step(f'Get {self.db_collection} collection'): 53 | user_details_collection = shorty_url_db[self.db_collection] 54 | 55 | with step('Delete user'): 56 | query = {"email": email} 57 | result = user_details_collection.delete_one(query) 58 | return result.deleted_count 59 | 60 | def get_all_short_url(self) -> list: 61 | """Get all short URLs from the collection.""" 62 | with step(f'Get all of short URL in collection {self.db_name}.{self.db_collection}'): 63 | with step(f'Get {self.db_name}'): 64 | shorty_url_db = self.mongodb_client[self.db_name] 65 | 66 | with step(f'Get {self.db_collection} collection'): 67 | url_mappings_collection = shorty_url_db[self.db_collection] 68 | 69 | with step('Get all documents from collection with field shortUrl'): 70 | documents = url_mappings_collection.find({}, {'shortUrl': 1}) 71 | 72 | with step('Get all values of shortUrl fields'): 73 | short_url_list = [document['shortUrl'] for document in documents if 'shortUrl' in document] 74 | return short_url_list 75 | 76 | @step('Map two short URLs to each other bidirectionally in MongoDB') 77 | def map_short_urls_bidirectional(self, short_url1: str, short_url2: str) -> None: 78 | with step(f'Get {self.db_name} database'): 79 | db = self.mongodb_client[self.db_name] 80 | 81 | with step(f'Get {self.db_collection} collection'): 82 | url_mappings_collection = db[self.db_collection] 83 | 84 | with step('Update documents to map short URLs bidirectionally'): 85 | # Use short_url1 and short_url2 directly since they are already full URLs 86 | operations = [ 87 | UpdateOne( 88 | {"shortUrl": short_url1}, # Query using the 'shortUrl' field 89 | {"$set": {"originalUrl": short_url2}} # Set 'originalUrl' to the other full URL 90 | ), 91 | UpdateOne( 92 | {"shortUrl": short_url2}, 93 | {"$set": {"originalUrl": short_url1}} 94 | ) 95 | ] 96 | 97 | # Execute the bulk write operation 98 | result = url_mappings_collection.bulk_write(operations) 99 | 100 | # Check if both updates were successful 101 | if result.matched_count < 2: 102 | missing_urls = [] 103 | if result.matched_count == 0: 104 | missing_urls = [short_url1, short_url2] 105 | elif result.matched_count == 1: 106 | # One of the URLs was not found 107 | missing_urls = [short_url1] if result.modified_count == 0 else [short_url2] 108 | raise ValueError(f"Mapping failed: Short URLs not found: {', '.join(missing_urls)}") 109 | 110 | # Optionally, return the result 111 | return result 112 | 113 | def verify_mappings(self, short_url1: str, short_url2: str): 114 | db = self.mongodb_client[self.db_name] 115 | url_mappings_collection = db[self.db_collection] 116 | 117 | doc1 = url_mappings_collection.find_one({"shortUrl": short_url1}) 118 | doc2 = url_mappings_collection.find_one({"shortUrl": short_url2}) 119 | 120 | print(f"Document for {short_url1}: {doc1}") 121 | print(f"Document for {short_url2}: {doc2}") 122 | 123 | @step('Get information about url from MongoDB') 124 | def get_url_info(self, short_url: str, ) -> dict: 125 | with step(f'Get {self.db_name} database'): 126 | db = self.mongodb_client[self.db_name] 127 | 128 | with step(f'Get {self.db_collection} collection'): 129 | url_mappings_collection = db[self.db_collection] 130 | 131 | return url_mappings_collection.find_one({"shortUrl": short_url}) 132 | 133 | @step('Update expiration time of short URL in MongoDB') 134 | def update_expiration_time(self, short_url: str, new_expiration_date: datetime) -> None: 135 | with step(f'Get {self.db_name} database'): 136 | db = self.mongodb_client[self.db_name] 137 | 138 | with step(f'Get {self.db_collection} collection'): 139 | url_mappings_collection = db[self.db_collection] 140 | 141 | with step('Update expiration time of short URL'): 142 | result = url_mappings_collection.update_one( 143 | {"shortUrl": short_url}, 144 | {"$set": {"expirationDate": new_expiration_date}} 145 | ) 146 | 147 | if result.matched_count == 0: 148 | raise ValueError(f"Failed to update expiration date for {short_url}.") 149 | 150 | if result.modified_count == 0: 151 | raise ValueError(f"Failed to update expiration date for {short_url}.") @ step( 152 | 'Update expiration time of short URL in MongoDB') 153 | 154 | def update_created_at_time(self, short_url: str, new_created_at: datetime) -> None: 155 | with step(f'Get {self.db_name} database'): 156 | db = self.mongodb_client[self.db_name] 157 | 158 | with step(f'Get {self.db_collection} collection'): 159 | url_mappings_collection = db[self.db_collection] 160 | 161 | with step('Update expiration time of short URL'): 162 | result = url_mappings_collection.update_one( 163 | {"shortUrl": short_url}, 164 | {"$set": {"createdAt": new_created_at}} 165 | ) 166 | 167 | if result.matched_count == 0: 168 | raise ValueError(f"Failed to update expiration date for {short_url}.") 169 | -------------------------------------------------------------------------------- /API/FRAMEWORK/mongodb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/FRAMEWORK/mongodb/__init__.py -------------------------------------------------------------------------------- /API/FRAMEWORK/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/FRAMEWORK/tools/__init__.py -------------------------------------------------------------------------------- /API/FRAMEWORK/tools/loggin_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 | -------------------------------------------------------------------------------- /API/FRAMEWORK/tools/redirect_to_original_url.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests import Response 3 | from allure import step 4 | 5 | from API.FRAMEWORK.tools.loggin_allure import log_request 6 | 7 | 8 | @step('Redirect to original url') 9 | def redirect_to_original_url(short_url: str, query_params: str = '') -> Response: 10 | url = short_url + query_params 11 | response = requests.get(url, allow_redirects=False) 12 | log_request(response) 13 | return response 14 | -------------------------------------------------------------------------------- /API/FRAMEWORK/tools/take_hash_from_url.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | 4 | def get_url_hash(short_url: str) -> str: 5 | """ 6 | Extracts the hash code from the given short URL. 7 | 8 | Args: 9 | short_url (str): The short URL from which to extract the hash. 10 | 11 | Returns: 12 | str: The hash code extracted from the short URL. 13 | """ 14 | parsed_url = urlparse(short_url) 15 | path = parsed_url.path # e.g., '/url/3UxbHC' 16 | return path.rstrip('/').split('/')[-1] 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /API/FRAMEWORK/tools/verification_time_expiration.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from dateutil import parser 3 | from hamcrest import less_than_or_equal_to 4 | from hamcrest.core import assert_that 5 | 6 | 7 | def verify_expiration_date(url_info_from_mongodb, expected_days): 8 | """ 9 | Verifies that the 'expirationDate' in the document is exactly 'expected_days' days from 'createdAt'. 10 | Handles date strings in ISO 8601 format with timezone information or datetime objects. 11 | 12 | Args: 13 | url_info_from_mongodb (dict): The MongoDB document containing 'createdAt' and 'expirationDate' fields. 14 | expected_days (int): The expected number of days between 'createdAt' and 'expirationDate'. 15 | 16 | Raises: 17 | AssertionError: If any of the assertions fail. 18 | """ 19 | # Extract 'createdAt' and 'expirationDate' from the document 20 | created_at_value = url_info_from_mongodb.get('createdAt') 21 | expiration_date_value = url_info_from_mongodb.get('expirationDate') 22 | 23 | # Check if both dates are present 24 | if not created_at_value or not expiration_date_value: 25 | print("Document is missing 'createdAt' or 'expirationDate' fields.") 26 | return False 27 | 28 | # Parse or assign the date values 29 | try: 30 | # Handle 'createdAt' 31 | if isinstance(created_at_value, datetime): 32 | created_at = created_at_value 33 | elif isinstance(created_at_value, str): 34 | created_at = parser.isoparse(created_at_value) 35 | else: 36 | print(f"Unsupported type for 'createdAt': {type(created_at_value)}") 37 | return False 38 | 39 | # Handle 'expirationDate' 40 | if isinstance(expiration_date_value, datetime): 41 | expiration_date = expiration_date_value 42 | elif isinstance(expiration_date_value, str): 43 | expiration_date = parser.isoparse(expiration_date_value) 44 | else: 45 | print(f"Unsupported type for 'expirationDate': {type(expiration_date_value)}") 46 | return False 47 | 48 | except Exception as e: 49 | print(f"Error parsing date values: {e}") 50 | return False 51 | 52 | # Calculate the expected expiration date 53 | expected_expiration_date = created_at + timedelta(days=expected_days) 54 | 55 | # Calculate the difference in seconds between the actual and expected expiration dates 56 | time_difference = abs((expiration_date - expected_expiration_date).total_seconds()) 57 | 58 | # Allow for a small margin of error (1 second) 59 | assert_that( 60 | time_difference, 61 | less_than_or_equal_to(1), 62 | f"Expiration date is not correctly set. Expected: {expected_expiration_date}, Actual: {expiration_date}" 63 | ) 64 | -------------------------------------------------------------------------------- /API/TEST/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/TEST/__init__.py -------------------------------------------------------------------------------- /API/TEST/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from allure import step 5 | from dotenv import load_dotenv 6 | from hamcrest import assert_that, greater_than 7 | 8 | from API.FRAMEWORK.api_endpoints.api_auth import AuthAPI 9 | from API.FRAMEWORK.api_endpoints.api_short_link import ShorteningLinkAPI 10 | from API.FRAMEWORK.mongodb.MongoDB import MongoDB 11 | from configs import MONGODB_DATABASE, MONGODB_COLLECTION_URL, MONGODB_COLLECTION_USER 12 | 13 | # Load secret config from a .env file: 14 | load_dotenv() 15 | mongodb_uri = os.environ.get('MONGODB_URI') 16 | 17 | if not mongodb_uri: 18 | raise ValueError("MONGO_URI not found in environment variables.") 19 | 20 | 21 | class MongoDbContext: 22 | def __init__(self, mongodb_client: 'MongoDB'): 23 | self.mongodb_client = mongodb_client 24 | self.created_short_urls = [] # List to track created short URLs 25 | 26 | 27 | @pytest.fixture() 28 | def mongodb_fixture() -> 'MongoDbContext': 29 | with step('Create MongoDB client'): 30 | mongodb_client = MongoDB(mongodb_uri, MONGODB_DATABASE, MONGODB_COLLECTION_URL) 31 | context = MongoDbContext(mongodb_client) 32 | yield context 33 | 34 | # Clean up after test execution 35 | for short_url in context.created_short_urls: 36 | deleted_count = context.mongodb_client.delete_created_short_url(short_url) 37 | with step(f'Verify that the created short URL {short_url} was deleted from MongoDB'): 38 | assert_that( 39 | deleted_count, 40 | greater_than(0), 41 | reason=f'Created short URL {short_url} was not deleted from MongoDB' 42 | ) 43 | 44 | with step('Close MongoDB connection'): 45 | context.mongodb_client.close_connection() 46 | 47 | 48 | @pytest.fixture(scope="function") 49 | def create_short_url(request): 50 | mongodb_client = None # Initialize mongodb_client to None 51 | created_short_url = None # Initialize created_short_url to None 52 | with step("Send POST request to create short url"): 53 | if isinstance(request.param, dict): 54 | original_url = request.param.get('original_url') 55 | days_count = request.param.get('days_count') # Optional parameter 56 | else: 57 | original_url = request.param 58 | days_count = None 59 | 60 | response = ShorteningLinkAPI().shorten_link(url=original_url, days_count=days_count) 61 | 62 | if response.status_code == 200: 63 | created_short_url = response.json()["shortUrl"] 64 | 65 | yield { 66 | 'response': response, 67 | 'created_short_url': created_short_url, 68 | 'days_count': days_count, 69 | 'original_url': original_url 70 | } 71 | 72 | if response.status_code == 200: 73 | with step('Create MongoDB client'): 74 | mongodb_client = MongoDB(mongodb_uri, MONGODB_DATABASE, MONGODB_COLLECTION_URL) 75 | 76 | deleted_count = mongodb_client.delete_created_short_url(created_short_url) 77 | with step(f'Verify that the created short URL {created_short_url} was deleted from MongoDB'): 78 | assert_that( 79 | deleted_count, 80 | greater_than(0), 81 | reason=f'Created short URL {created_short_url} was not deleted from MongoDB' 82 | ) 83 | 84 | # Ensure the MongoDB client is closed if it was created 85 | if mongodb_client is not None: 86 | with step('Close MongoDB connection'): 87 | mongodb_client.close_connection() 88 | 89 | 90 | @pytest.fixture() 91 | def sign_up_fixture(request) -> dict: 92 | user_data = request.param 93 | with step("Create Auth API client"): 94 | auth_api = AuthAPI() 95 | response = auth_api.sign_up(*user_data) 96 | 97 | yield {"response": response, "user_data": user_data} 98 | 99 | # if the user has been created, delete it from MongoDB 100 | if response.status_code in (200, 201): 101 | with step('Create MongoDB client'): 102 | mongodb_client = MongoDB(mongodb_uri, MONGODB_DATABASE, MONGODB_COLLECTION_USER) 103 | 104 | email = user_data[2] 105 | deleted_count = mongodb_client.delete_user(email) 106 | 107 | with step(f'Verify that the created user {user_data[0]} {user_data[1]} was deleted from MongoDB'): 108 | assert_that( 109 | deleted_count, 110 | greater_than(0), 111 | reason=f'Created user {user_data[0]} {user_data[1]} was not deleted from MongoDB' 112 | ) 113 | 114 | with step('Close MongoDB connection'): 115 | mongodb_client.close_connection() 116 | -------------------------------------------------------------------------------- /API/TEST/test_create_short_url.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | import pytest 5 | from allure import step 6 | from hamcrest import assert_that, is_, is_not 7 | 8 | from API.DATA.loopback_url import LOOPBACK_URL 9 | from API.FRAMEWORK.api_endpoints.api_short_link import ShorteningLinkAPI 10 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 11 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 12 | 13 | 14 | @allure.feature("Short URL generation") 15 | @allure.link("https://short-link.zufargroup.com/webjars/swagger-ui/index.html#/URL%20Shortening/shortenUrl", 16 | name="Swagger") 17 | class TestCreateShortUrl: 18 | @allure.severity(allure.severity_level.CRITICAL) 19 | @allure.title("Test create short url, happy path") 20 | @allure.description( 21 | """WHEN user send POST request to create short url 22 | THEN status HTTP CODE = 200 and response body contains short url""" 23 | ) 24 | def test_create_short_url(self): 25 | with step("Send POST request to create short url"): 26 | original_url = "https://www.google.com" 27 | response = ShorteningLinkAPI().shorten_link(original_url) 28 | 29 | with step("Verify status code"): 30 | assert_status_code(response, 200) 31 | 32 | with step("Verify content-type"): 33 | assert_content_type(response, "application/json") 34 | 35 | with step("Verify short url"): 36 | assert response.json()["shortUrl"], is_not(None) 37 | 38 | @allure.severity(allure.severity_level.CRITICAL) 39 | @allure.title("Test recreation short url") 40 | @allure.description( 41 | """WHEN user send POST request to create short url for the URL already shortened before 42 | THEN status HTTP CODE = 200 and response body contains short url with the same short url""" 43 | ) 44 | @pytest.mark.parametrize('create_short_url', [f'https://ya{time.time()}.ru'], indirect=True) 45 | def test_recreate_short_url(self, create_short_url): 46 | with step("Send firstPOST request to create short url"): 47 | created_short_url = create_short_url["created_short_url"] 48 | original_url = create_short_url["original_url"] 49 | 50 | with step("Send second POST request to create short url for the origin URL already shortened"): 51 | response_recreate_short_url = ShorteningLinkAPI().shorten_link(original_url) 52 | recreated_short_url = response_recreate_short_url.json()["shortUrl"] 53 | 54 | with step( 55 | "Verify that created short url after second POST request for origin URL is the same as was created before"): 56 | assert_that(recreated_short_url, is_(created_short_url), 57 | reason="Recreated short url is not the same as was created before") 58 | 59 | @allure.severity(allure.severity_level.CRITICAL) 60 | @allure.title("Test recreation short url, happy path") 61 | @allure.description( 62 | """WHEN user send POST request to create short url for the URL that points to a loopback address 63 | THEN status HTTP CODE = 400 and return an error message indicating that the URL cannot point 64 | to a loopback address.""" 65 | ) 66 | @pytest.mark.parametrize('original_url, error_message', LOOPBACK_URL) 67 | def test_create_short_url_for_loopback_url(self, original_url, error_message): 68 | with step("Send firstPOST request to create short url for the URL that points to a loopback address"): 69 | url = original_url 70 | response = ShorteningLinkAPI().shorten_link(url) 71 | 72 | with step("Verify status code"): 73 | assert_status_code(response, 400) 74 | 75 | with step("Verify content-type"): 76 | assert_content_type(response, "application/json") 77 | 78 | with step("Verify error message"): 79 | assert response.json()["errorMessage"], is_(error_message) 80 | -------------------------------------------------------------------------------- /API/TEST/test_create_short_url_negative.py: -------------------------------------------------------------------------------- 1 | import allure 2 | from allure import step 3 | from hamcrest import assert_that, is_ 4 | import pytest 5 | 6 | from API.DATA.original_url_invalid import ORIGINAL_URL_INVALID 7 | from API.FRAMEWORK.api_endpoints.api_short_link import ShorteningLinkAPI 8 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 9 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 10 | 11 | 12 | @allure.feature("Short URL generation") 13 | @allure.link("https://short-link.zufargroup.com/webjars/swagger-ui/index.html#/URL%20Shortening/shortenUrl", 14 | name="Swagger") 15 | @allure.severity(allure.severity_level.CRITICAL) 16 | @allure.title("Negative test create short url") 17 | @allure.description( 18 | """WHEN user send POST request to create short url 19 | AND original URL is not valid 20 | THEN status HTTP CODE = 400 and response body contains corresponding error message""" 21 | ) 22 | @pytest.mark.parametrize('original_url, error_message', ORIGINAL_URL_INVALID) 23 | def test_create_short_url_negative(original_url, error_message): 24 | with step("Send POST request to create short url"): 25 | response = ShorteningLinkAPI().shorten_link(original_url) 26 | 27 | with step("Verify status code"): 28 | assert_status_code(response, 400) 29 | 30 | with step("Verify content-type"): 31 | assert_content_type(response, "application/json") 32 | 33 | with step("Verify error message"): 34 | assert_that(response.json()["errorMessage"], is_(error_message), 35 | reason="Error message is not valid") 36 | -------------------------------------------------------------------------------- /API/TEST/test_expiration_time.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import timedelta 3 | 4 | import allure 5 | import pytest 6 | from hamcrest import assert_that 7 | 8 | from API.FRAMEWORK.api_endpoints.api_url import UrlAPI 9 | from API.FRAMEWORK.assertion.assert_response_message import assert_message_in_response 10 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 11 | from API.FRAMEWORK.tools.redirect_to_original_url import redirect_to_original_url 12 | from API.FRAMEWORK.tools.take_hash_from_url import get_url_hash 13 | from API.FRAMEWORK.tools.verification_time_expiration import verify_expiration_date 14 | 15 | 16 | @allure.feature("Expiration Time") 17 | class TestExpirationTime: 18 | @allure.severity(allure.severity_level.MINOR) 19 | @allure.feature("Expiration Time") 20 | @allure.link("https://short-link.zufargroup.com/api/v1/swagger-ui/index.html", 21 | name="Swagger") 22 | @allure.link( 23 | "https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16023559/4.+Expiration+Time#4.1-Default-Expiration-Time-(Implemented)", 24 | name="FR4.1") 25 | @allure.title("Verify that default expiration time of created short url is 365 days from the moment of creation") 26 | @allure.link( 27 | "https://team-bov4.testit.software/projects/1/tests/114?isolatedSection=5f9d72fd-d528-4513-9086-6e067c9a2999", 28 | name="Test IT Test-Case #114") 29 | @allure.description( 30 | """ 31 | GIVEN unauthorized user 32 | WHEN unauthorized user send POST request to create short url without specifying an expiration date 33 | THEN default expiration time of created short url is 365 days from the moment of creation""" 34 | ) 35 | @pytest.mark.parametrize('create_short_url', [f'https://ya{time.time()}.ru'], indirect=True) 36 | def test_default_expiration_time(self, create_short_url, mongodb_fixture): 37 | created_short_url = create_short_url["created_short_url"] 38 | short_url_info = mongodb_fixture.mongodb_client.get_url_info(created_short_url) 39 | verify_expiration_date(url_info_from_mongodb=short_url_info, expected_days=365) 40 | 41 | @allure.severity(allure.severity_level.MINOR) 42 | @allure.feature("Expiration Time") 43 | @allure.link("https://short-link.zufargroup.com/api/v1/swagger-ui/index.html", 44 | name="Swagger") 45 | @allure.link( 46 | "https://team-bov4.testit.software/projects/1/tests?isolatedSection=5f9d72fd-d528-4513-9086-6e067c9a2999", 47 | name="Test IT Test-Case #124, 125, 117, 122,121") 48 | @allure.link( 49 | "https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16023559/4.+Expiration+Time#4.2-Custom-Expiration-Time-(Implemented)", 50 | name="FR4.2") 51 | @allure.title("Verify custom expiration time of created short url") 52 | @allure.description( 53 | """ 54 | GIVEN unauthorized user 55 | WHEN unauthorized user send POST request to create short url with specifying an expiration date 56 | THEN expiration time of created short url is equal to the specified date from the moment of creation""" 57 | ) 58 | @pytest.mark.parametrize( 59 | 'create_short_url', 60 | [ 61 | ({'original_url': f'https://ya{time.time()}.ru', 'days_count': "30"}), 62 | ({'original_url': f'https://ya{time.time()}.ru', 'days_count': "364"}), 63 | ({'original_url': f'https://ya{time.time()}.ru', 'days_count': "2"}), 64 | ({'original_url': f'https://ya{time.time()}.ru', 'days_count': "1"}), 65 | ({'original_url': f'https://ya{time.time()}.ru', 'days_count': "365"}), 66 | 67 | ], 68 | indirect=True 69 | ) 70 | def test_custom_expiration_time(self, create_short_url, mongodb_fixture): 71 | created_short_url = create_short_url["created_short_url"] 72 | days_count = create_short_url["days_count"] 73 | expected_expiration_days = int(days_count) 74 | short_url_info = mongodb_fixture.mongodb_client.get_url_info(created_short_url) 75 | verify_expiration_date(url_info_from_mongodb=short_url_info, expected_days=expected_expiration_days) 76 | 77 | @allure.severity(allure.severity_level.MINOR) 78 | @allure.feature("Expiration Time") 79 | @allure.link("https://short-link.zufargroup.com/api/v1/swagger-ui/index.html", 80 | name="Swagger") 81 | @allure.link("https://team-bov4.testit.software/projects/1/tests", 82 | name="Test IT Test-Case #118, 119, 126") 83 | @allure.link( 84 | "https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16023559/4.+Expiration+Time#4.3-Time-Limits-for-Custom-Expiration-(Implemented)", 85 | name="FR4.3") 86 | @allure.title("Verify custom expiration time of created short url that is out of range") 87 | @allure.description( 88 | """ 89 | GIVEN unauthorized user 90 | WHEN unauthorized user send POST request to create short url with specifying an expiration date that is out of range 91 | THEN status HTTP CODE = 400 and response body contains error message""" 92 | ) 93 | @pytest.mark.parametrize( 94 | 'create_short_url, expected_status_code, expected_error_message', 95 | [ 96 | ( 97 | { 98 | 'original_url': f'https://ya{time.time()}.ru', 99 | 'days_count': "0", 100 | }, 101 | 400, 102 | 'Days count must be at least 1 day(s).' 103 | ), 104 | ( 105 | { 106 | 'original_url': f'https://ya{time.time()}.ru', 107 | 'days_count': "390", 108 | }, 109 | 400, 110 | 'Days count must not exceed 365 day(s).' 111 | ), 112 | ( 113 | { 114 | 'original_url': f'https://ya{time.time()}.ru', 115 | 'days_count': "366", 116 | }, 117 | 400, 118 | 'Days count must not exceed 365 day(s).' 119 | ) 120 | ], 121 | indirect=['create_short_url'] 122 | ) 123 | def test_custom_expiration_time_out_of_range(self, create_short_url, mongodb_fixture, 124 | expected_status_code, expected_error_message): 125 | response = create_short_url["response"] 126 | assert_status_code(response=response, expected_status_code=expected_status_code) 127 | assert_message_in_response(response=response, expected_message=expected_error_message) 128 | 129 | @pytest.mark.xfail(reason="Not implemented logic to handle invalid dates", run=True) 130 | @allure.feature("Expiration Time") 131 | @allure.link("https://short-link.zufargroup.com/api/v1/swagger-ui/index.html", 132 | name="Swagger") 133 | @allure.link("https://team-bov4.testit.software/projects/1/tests", 134 | name="Test IT Test-Case #120,123") 135 | @allure.severity(allure.severity_level.MINOR) 136 | @allure.title("Verify expiration time with invalid date") 137 | @allure.description( 138 | """ 139 | GIVEN unauthorized user 140 | WHEN unauthorized user send POST request to create short url with invalid expiration date that is out of range 141 | THEN status HTTP CODE = 400 and response body contains error message""" 142 | ) 143 | @pytest.mark.parametrize( 144 | 'create_short_url, expected_status_code, expected_error_message', 145 | [ 146 | ( 147 | { 148 | 'original_url': f'https://ya{time.time()}.ru', 149 | 'days_count': "7.5", 150 | }, 151 | 400, 152 | ' ' 153 | ), 154 | ( 155 | { 156 | 'original_url': f'https://ya{time.time()}.ru', 157 | 'days_count': "-1", 158 | }, 159 | 400, 160 | 'Days count must be at least 1 day(s).' 161 | ), 162 | ( 163 | { 164 | 'original_url': f'https://ya{time.time()}.ru', 165 | 'days_count': "hello", 166 | }, 167 | 400, 168 | ' ' 169 | ), 170 | ( 171 | { 172 | 'original_url': f'https://ya{time.time()}.ru', 173 | 'days_count': "%", 174 | }, 175 | 400, 176 | ' ' 177 | ) 178 | ], 179 | indirect=['create_short_url'] 180 | ) 181 | def test_expiration_time_invalid_type(self, create_short_url, mongodb_fixture, 182 | expected_status_code, expected_error_message): 183 | response = create_short_url["response"] 184 | assert_status_code(response=response, expected_status_code=expected_status_code) 185 | assert_message_in_response(response=response, expected_message=expected_error_message) 186 | 187 | @pytest.mark.xfail(reason="Bug, HTTP=302, allows redirection to original url with no error message", run=True) 188 | @allure.feature("Expiration Time") 189 | @allure.link("https://short-link.zufargroup.com/api/v1/swagger-ui/index.html", 190 | name="Swagger") 191 | @allure.link("https://team-bov4.testit.software/projects/1/tests", 192 | name="Test IT Test-Case #116") 193 | @allure.link( 194 | "https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16023559/4.+Expiration+Time#4.4-Expired-Short-URLs-(Implemented)", 195 | name="FR4.4") 196 | @allure.severity(allure.severity_level.MINOR) 197 | @allure.title("Verify get short url after expiration time ") 198 | @pytest.mark.parametrize("create_short_url", [( 199 | { 200 | 'original_url': f'https://www.linkedin.com/jobs/collections/recommended/{time.time()}', 201 | 'days_count': "1", 202 | })], indirect=True) 203 | @allure.description( 204 | """ 205 | GIVEN unauthorized user 206 | WHEN unauthorized user send GET request short url with expiration TIME 207 | THEN status HTTP CODE = 404 and response body contains error message""" 208 | ) 209 | def test_verify_get_short_url_after_expiration(self, create_short_url, mongodb_fixture): 210 | created_short_url = create_short_url["created_short_url"] 211 | get_hash_from_url = get_url_hash(short_url=created_short_url) 212 | get_url_info = mongodb_fixture.mongodb_client.get_url_info(created_short_url) 213 | amount_of_days_to_reduce = 3 214 | new_expiration_date = get_url_info['expirationDate'] - timedelta(days=amount_of_days_to_reduce) 215 | mongodb_fixture.mongodb_client.update_expiration_time( 216 | short_url=created_short_url, 217 | new_expiration_date=new_expiration_date 218 | ) 219 | 220 | new_created_at_date = get_url_info['createdAt'] - timedelta(days=amount_of_days_to_reduce) 221 | mongodb_fixture.mongodb_client.update_created_at_time( 222 | short_url=created_short_url, 223 | new_created_at=new_created_at_date 224 | ) 225 | 226 | updated_url_info = mongodb_fixture.mongodb_client.get_url_info(created_short_url) 227 | assert_that(updated_url_info['expirationDate'] == new_expiration_date, "Expiration date was not updated.") 228 | assert_that(updated_url_info['createdAt'] == new_created_at_date, "createdAt date was not updated.") 229 | 230 | response = redirect_to_original_url(short_url=created_short_url) 231 | assert_status_code(response=response, expected_status_code=404) 232 | expected_error_message = f'{{"errorMessage":"Original URL is absent for urlHash=\'{get_hash_from_url}\'"}}' 233 | assert_message_in_response(response=response, expected_message=expected_error_message) 234 | -------------------------------------------------------------------------------- /API/TEST/test_loop_redirection.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import pytest 3 | from allure import step 4 | 5 | 6 | from API.FRAMEWORK.api_endpoints.api_short_link import ShorteningLinkAPI 7 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 8 | from API.FRAMEWORK.tools.redirect_to_original_url import redirect_to_original_url 9 | 10 | 11 | @allure.feature("2. Link Redirection") 12 | class TestRedirection: 13 | @allure.severity(allure.severity_level.CRITICAL) 14 | @allure.title("Verify that loops in URL redirection are avoided") 15 | @allure.description( 16 | """WHEN user sends a GET request to the created short URL#1 that is mapped to another short URL#2 17 | THEN the response HTTP code is 400 18 | AND the response body contains the corresponding error message""" 19 | ) 20 | @pytest.mark.xfail(reason='Bug?', run=True) 21 | def test_loop_redirection(self, mongodb_fixture): 22 | with step("Create short url #1"): 23 | original_url_1 = 'https://example8.com' 24 | response_1 = ShorteningLinkAPI().shorten_link(original_url_1) 25 | short_url_1 = response_1.json()['shortUrl'] 26 | 27 | with step("Create short url #2"): 28 | original_url_2 = 'https://example9.com' 29 | response_2 = ShorteningLinkAPI().shorten_link(original_url_2) 30 | short_url_2 = response_2.json()['shortUrl'] 31 | 32 | with step("Mapping short urls to each other to create a loop"): 33 | # Register the created short URLs for cleanup 34 | mongodb_fixture.created_short_urls.extend([short_url_1, short_url_2]) 35 | 36 | # Map short URLs to each other to create a loop 37 | mongodb_fixture.mongodb_client.map_short_urls_bidirectional(short_url1=short_url_1, 38 | short_url2=short_url_2, 39 | ) 40 | with step("Get short url #1"): 41 | response = redirect_to_original_url(short_url_1) 42 | 43 | with step("Verify status code"): 44 | assert_status_code(response, 400) 45 | 46 | with step("Verify error message"): 47 | assert response.json()['error'] == 'URL cannot point to a loopback address' 48 | -------------------------------------------------------------------------------- /API/TEST/test_redirection_to_original_url.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | import pytest 5 | from allure import step 6 | 7 | from API.FRAMEWORK.assertion.assert_response_header_value import assert_response_header_value 8 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 9 | from API.FRAMEWORK.tools.redirect_to_original_url import redirect_to_original_url 10 | 11 | 12 | @allure.feature("2. Link Redirection") 13 | @allure.link("https://short-link.zufargroup.com/webjars/swagger-ui/index.html#/URL%20Shortening/shortenUrl", 14 | name="Swagger!!!Ссылка нерабочая, т.к. не работает сваггер") 15 | class TestRedirection: 16 | @allure.severity(allure.severity_level.BLOCKER) 17 | @allure.title("Verify successful redirection to the original URL by a valid short URL") 18 | @allure.description( 19 | """WHEN user send GET request to the created short URL 20 | THEN response HTTP code is 302 21 | AND response 'Location' header contains the original URL""" 22 | ) 23 | @allure.link("https://team-bov4.testit.software/projects/1/tests/67", 24 | name="Test IT Test-Case #67") 25 | @pytest.mark.parametrize('create_short_url', [f'https://ya{time.time()}.ru'], indirect=True) 26 | def test_redirection(self, create_short_url): 27 | with step("Create short url"): 28 | created_short_url = create_short_url["created_short_url"] 29 | original_url = create_short_url["original_url"] 30 | 31 | with step("Redirect to original url"): 32 | redirect_response = redirect_to_original_url(created_short_url) 33 | 34 | with step("Verify status code"): 35 | assert_status_code(redirect_response, 302) 36 | assert_response_header_value(redirect_response, 'Location', original_url) 37 | 38 | @allure.severity(allure.severity_level.NORMAL) 39 | @allure.title("Verify successful redirection to the original URL by a valid short URL with query params") 40 | @allure.description( 41 | """WHEN user send GET request to the created short URL with query params 42 | THEN response HTTP code is 302 43 | AND response 'Location' header contains the original URL with the same query params""" 44 | ) 45 | @allure.link("https://team-bov4.testit.software/projects/1/tests/69", 46 | name="Test IT Test-Case #69") 47 | @pytest.mark.parametrize('create_short_url', [f'https://ya{time.time()}.ru'], indirect=True) 48 | @pytest.mark.xfail(reason='This feature is not implemented', run=True) 49 | def test_redirection_with_query_params(self, create_short_url): 50 | with step("Create short url"): 51 | created_short_url = create_short_url["created_short_url"] 52 | original_url = create_short_url["original_url"] 53 | query_params = '?test1=value1&test2=value2' 54 | 55 | with step("Redirect to original url"): 56 | redirect_response = redirect_to_original_url(created_short_url, query_params) 57 | 58 | with step("Verify status code"): 59 | assert_status_code(redirect_response, 302) 60 | 61 | assert_response_header_value(redirect_response, 'Location', original_url + query_params) 62 | -------------------------------------------------------------------------------- /API/TEST/test_sign_in_negative.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import pytest 3 | import time 4 | from allure import step 5 | 6 | from API.DATA.email_invalid import EMAIL_INVALID 7 | from API.DATA.password_invalid import PASSWORD_INVALID 8 | 9 | from API.FRAMEWORK.api_endpoints.api_auth import AuthAPI 10 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 11 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 12 | from API.FRAMEWORK.assertion.assert_response_message import assert_message_in_response 13 | 14 | 15 | @allure.feature("6. Sign in (User Authentication)") 16 | @allure.severity(allure.severity_level.CRITICAL) 17 | class TestSignInNegative: 18 | @allure.title("Check authorization using email not existing in the system") 19 | @allure.description( 20 | """ 21 | GIVEN an email that does not exist, 22 | WHEN the user attempts to sign in with that email, 23 | THEN the system should reject the request 24 | AND return an error message indicating that the email is not found. 25 | """ 26 | ) 27 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/18251777/6." 28 | "+Sign+in+User+Authentication#6.1-User-Email-Does-Not-Exists-in-the-System-(Implemented)"), 29 | name="FR6.1") 30 | @allure.link("https://team-bov4.testit.software/projects/1/tests/133", name="Test IT Test-Case #133") 31 | def test_email_not_exist(self): 32 | email = f'mail{time.time()}.yandex.ru' 33 | password = 'Password134' 34 | 35 | auth_api = AuthAPI() 36 | response = auth_api.sign_in(email, password) 37 | 38 | with step("Verify status code is 401"): 39 | assert_status_code(response, 401) 40 | 41 | with step("Verify content-type is 'application/json'"): 42 | assert_content_type(response, "application/json") 43 | 44 | with step("Verify response message is 'Invalid email or password'"): 45 | assert_message_in_response(response, "Invalid email or password") 46 | 47 | @allure.title("Check authorization with an empty email field") 48 | @allure.description( 49 | """ 50 | GIVEN a user submits a sign-in request with an empty email, 51 | WHEN the request is processed, 52 | THEN the system should reject the request 53 | AND return an error message with HTTP status code 400 Bad Request indicating that the email is required. 54 | """ 55 | ) 56 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/18251777/6." 57 | "+Sign+in+User+Authentication#6.2.1-Empty-Email"), name="FR6.2.1") 58 | @allure.link("https://team-bov4.testit.software/projects/1/tests/131", name="Test IT Test-Case #131") 59 | def test_email_empty(self): 60 | email = '' 61 | password = 'Password134' 62 | 63 | auth_api = AuthAPI() 64 | response = auth_api.sign_in(email, password) 65 | 66 | with step("Verify status code is 400"): 67 | assert_status_code(response, 400) 68 | 69 | with step("Verify content-type is 'application/json'"): 70 | assert_content_type(response, "application/json") 71 | 72 | with step("Verify response message is 'Email must not be empty'"): 73 | assert_message_in_response(response, "Email must not be empty") 74 | 75 | @allure.title("Check authorization with an invalid email field") 76 | @allure.description( 77 | """ 78 | GIVEN a user submits an email that is invalid (too long, too short, or incorrect format), 79 | WHEN the request is processed, 80 | THEN the system should reject the request 81 | AND return an error message with HTTP status code 401 Unauthorized 82 | indicating that the email or password is invalid. 83 | """ 84 | ) 85 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/18251777/6." 86 | "+Sign+in+User+Authentication#6.2.2-Invalid-Email-(Implemented)"), name="FR6.2.2") 87 | @allure.link("https://team-bov4.testit.software/projects/1/tests/134", name="Test IT Test-Case #134") 88 | @pytest.mark.parametrize('email_invalid', EMAIL_INVALID) 89 | def test_email_invalid(self, email_invalid: str): 90 | email = email_invalid 91 | password = 'Password134' 92 | 93 | auth_api = AuthAPI() 94 | response = auth_api.sign_in(email, password) 95 | 96 | with step("Verify status code is 401"): 97 | assert_status_code(response, 401) 98 | 99 | with step("Verify content-type is 'application/json'"): 100 | assert_content_type(response, "application/json") 101 | 102 | with step("Verify response message is 'Invalid email or password'"): 103 | assert_message_in_response(response, "Invalid email or password") 104 | 105 | @allure.title("Check authorization with an empty password field") 106 | @allure.description( 107 | """ 108 | GIVEN a user submits a sign-in request without a password, 109 | WHEN the request is processed, 110 | THEN the system should reject the request 111 | AND return an error message with HTTP status code 400 Bad Request indicating that the password is required. 112 | """ 113 | ) 114 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/18251777/6." 115 | "+Sign+in+User+Authentication#6.3.1-Empty-Password"), name="FR6.3.1") 116 | @allure.link("https://team-bov4.testit.software/projects/1/tests/132", name="Test IT Test-Case #132") 117 | def test_password_empty(self): 118 | email = '123test@gmail.com' 119 | password = '' 120 | 121 | auth_api = AuthAPI() 122 | response = auth_api.sign_in(email, password) 123 | 124 | with step("Verify status code is 400"): 125 | assert_status_code(response, 400) 126 | 127 | with step("Verify content-type is 'application/json'"): 128 | assert_content_type(response, "application/json") 129 | 130 | with step("Verify response message is 'Password must not be empty'"): 131 | assert_message_in_response(response, "Password must not be empty") 132 | 133 | @allure.title("Check authorization with an invalid password field") 134 | @allure.description( 135 | """ 136 | GIVEN a user submits a password that is invalid (too long, too short, or incorrect), 137 | WHEN the request is processed, 138 | THEN the system should reject the request 139 | AND return an error message with HTTP status code 401 Unauthorized 140 | indicating that the email or password is invalid. 141 | """ 142 | ) 143 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/18251777/6." 144 | "+Sign+in+User+Authentication#6.3.2-Invalid-Password-(Implemented)"), name="FR6.3.2") 145 | @allure.link("https://team-bov4.testit.software/projects/1/tests/135", name="Test IT Test-Case #135") 146 | @pytest.mark.parametrize('password_invalid', PASSWORD_INVALID) 147 | def test_password_invalid(self, password_invalid: str): 148 | email = '123test@gmail.com' 149 | password = password_invalid 150 | 151 | auth_api = AuthAPI() 152 | response = auth_api.sign_in(email, password) 153 | 154 | with step("Verify status code is 401"): 155 | assert_status_code(response, 401) 156 | 157 | with step("Verify content-type is 'application/json'"): 158 | assert_content_type(response, "application/json") 159 | 160 | with step("Verify response message is 'Invalid email or password'"): 161 | assert_message_in_response(response, "Invalid email or password") 162 | -------------------------------------------------------------------------------- /API/TEST/test_sign_in_positive.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import pytest 3 | from hamcrest import assert_that, is_not 4 | 5 | from API.DATA.user_valid import USER_TO_CREATE 6 | from API.FRAMEWORK.api_endpoints.api_auth import AuthAPI 7 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 8 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 9 | 10 | 11 | @allure.feature("6. Sign in (User Authentication)") 12 | @allure.severity(allure.severity_level.CRITICAL) 13 | class TestSignInNegative: 14 | @allure.title("Check authorization using happy path") 15 | @allure.description( 16 | """ 17 | GIVEN user is registered in the system, 18 | When the user submits a sign-in request with valid email and password, 19 | THEN HTTP STATUS CODE = 200 and 20 | AND the system should return a JWT access token and a refresh token""" 21 | ) 22 | @allure.link("https://shorty-url.atlassian.net/wiki/x/AYAWAQ", 23 | name="FR6.1") 24 | @allure.link("https://team-bov4.testit.software/projects/1/tests", name="Test IT Test-Case #130") 25 | @pytest.mark.parametrize('sign_up_fixture', USER_TO_CREATE, indirect=True) 26 | def test_authorization_happy_path(self, sign_up_fixture): 27 | user_authorization = sign_up_fixture['user_data'] 28 | email = user_authorization[2] 29 | password = user_authorization[3] 30 | response_sign_in = AuthAPI().sign_in(email=email, password=password) 31 | assert_status_code(response=response_sign_in, expected_status_code=200) 32 | assert_content_type(response=response_sign_in, expected_content_type="application/json") 33 | token = response_sign_in.json().get("accessToken") 34 | refresh_token = response_sign_in.json().get("refreshToken") 35 | assert_that(token, is_not(None), reason="No access token found") 36 | assert_that(refresh_token, is_not(None), reason="No refresh token found") 37 | -------------------------------------------------------------------------------- /API/TEST/test_sign_up.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import pytest 3 | from allure import step 4 | from hamcrest import assert_that, is_not 5 | 6 | from API.DATA.user_valid import USER_VALID 7 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 8 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 9 | from API.FRAMEWORK.assertion.assert_user_in_mongodb import is_user_in_mongodb 10 | 11 | 12 | @allure.feature("5. Sign up (User Registration)") 13 | @allure.severity(allure.severity_level.CRITICAL) 14 | @allure.title("Verify that user can register") 15 | @allure.description( 16 | """GIVEN a valid sign-up request, 17 | WHEN the user submits the sign-up request with valid details, 18 | THEN the system should create a new user, 19 | AND return a JWT access token and a refresh token""" 20 | ) 21 | @allure.link("https://shorty-url.atlassian.net/wiki/x/EgD4", name="FR") 22 | # @allure.link("https://TestIT", name="Test IT Test-Case #") 23 | @pytest.mark.parametrize('sign_up_fixture', USER_VALID, indirect=True) 24 | def test_sign_up(sign_up_fixture): 25 | response = sign_up_fixture['response'] 26 | 27 | with step("Verify status code"): 28 | assert_status_code(response, 200) 29 | 30 | with step("Verify content-type"): 31 | assert_content_type(response, "application/json") 32 | 33 | with step("Verify accessToken in the response body"): 34 | access_token = response.json()['accessToken'] 35 | assert_that( 36 | access_token, 37 | is_not(None), 38 | reason='There is not accessToken field in the response body' 39 | ) 40 | 41 | with step("Verify refreshToken in the response body"): 42 | refresh_token = response.json()['refreshToken'] 43 | assert_that( 44 | refresh_token, 45 | is_not(None), 46 | reason='There is not refreshToken field in the response body' 47 | ) 48 | 49 | email = sign_up_fixture["user_data"][2] 50 | is_user_in_mongodb(email) 51 | -------------------------------------------------------------------------------- /API/TEST/test_sign_up_negative.py: -------------------------------------------------------------------------------- 1 | import allure 2 | import pytest 3 | from allure import step 4 | 5 | from API.DATA.user_valid import USER_TO_CREATE 6 | from API.DATA.user_invalid import (USER_EMAIL_EMPTY, USER_EMAIL_INVALID, USER_EMAIL_LONG, 7 | USER_PASSWORD_EMPTY, USER_PASSWORD_SHORT, USER_PASSWORD_WITHOUT_UPPERCASE, 8 | USER_PASSWORD_WITHOUT_LOWERCASE, USER_PASSWORD_WITHOUT_DIGITS, 9 | USER_PASSWORD_WITHOUT_SPEC_CHAR, USER_PASSWORD_WITH_SPACES, USER_PASSWORD_LONG, 10 | USER_PASSWORD_INVALID, 11 | USER_FIRST_NAME_EMPTY, USER_FIRST_NAME_INVALID, USER_FIRST_NAME_LONG, 12 | USER_LAST_NAME_EMPTY, USER_LAST_NAME_INVALID, USER_LAST_NAME_LONG, 13 | USER_COUNTRY_EMPTY, USER_COUNTRY_INVALID, USER_COUNTRY_LONG, 14 | USER_AGE_INVALID, USER_AGE_NOT_NUMBER) 15 | 16 | from API.FRAMEWORK.api_endpoints.api_auth import AuthAPI 17 | from API.FRAMEWORK.assertion.assert_status_code import assert_status_code 18 | from API.FRAMEWORK.assertion.assert_content_type import assert_content_type 19 | from API.FRAMEWORK.assertion.assert_response_message import assert_message_in_response 20 | 21 | 22 | @allure.feature("5. Sign up (User Registration)") 23 | @allure.severity(allure.severity_level.CRITICAL) 24 | class TestSignUpNegative: 25 | @allure.title("Verify that user can not register with not unique email") 26 | @allure.description( 27 | """ 28 | GIVEN an email that is already in use, 29 | WHEN the user attempts to register with that email, 30 | THEN the system should reject the request 31 | AND return an error message indicating that the email is already in use. 32 | """ 33 | ) 34 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 35 | "+Sign+up+User+Registration#5.1-Unique-Email-Validation-(Implemented)"), name="FR5.1") 36 | @allure.link("https://team-bov4.testit.software/projects/1/tests/95", name="Test IT Test-Case #95") 37 | @pytest.mark.xfail(reason="Bug is not fixed: https://shorty-url.atlassian.net/browse/SHORTY-83", run=True) 38 | @pytest.mark.parametrize('sign_up_fixture', USER_TO_CREATE, indirect=True) 39 | def test_email_unique_validation(self, sign_up_fixture): 40 | auth_api = AuthAPI() 41 | response = auth_api.sign_up(*USER_TO_CREATE[0]) 42 | 43 | with step("Verify status code is 400"): 44 | assert_status_code(response, 400) 45 | 46 | with step("Verify content-type is 'application/json'"): 47 | assert_content_type(response, "application/json") 48 | 49 | with step("Verify response message is 'Email is already in use'"): 50 | assert_message_in_response(response, "Email is already in use") 51 | 52 | @allure.title("Verify that user can not register with empty email") 53 | @allure.description( 54 | """ 55 | WHEN the user attempts to register with empty email, 56 | THEN the system should reject the request, 57 | AND return an error message indicating that the email must not be empty. 58 | """ 59 | ) 60 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 61 | "+Sign+up+User+Registration#5.2-Email-Constraints-Validation-(Implemented)"), name="FR5.2") 62 | @allure.link("https://team-bov4.testit.software/projects/1/tests/96", name="Test IT Test-Case #96") 63 | @pytest.mark.parametrize('sign_up_fixture', USER_EMAIL_EMPTY, indirect=True) 64 | def test_email_empty(self, sign_up_fixture): 65 | response = sign_up_fixture["response"] 66 | 67 | with step("Verify status code is 400"): 68 | assert_status_code(response, 400) 69 | 70 | with step("Verify content-type is 'application/json'"): 71 | assert_content_type(response, "application/json") 72 | 73 | with step("Verify response message is 'Email must not be empty'"): 74 | assert_message_in_response(response, "Email must not be empty") 75 | 76 | @allure.title("Verify that user can not register with invalid email") 77 | @allure.description( 78 | """ 79 | WHEN the user attempts to register with invalid email, 80 | THEN the system should reject the request, 81 | AND return an error message indicating that the email is invalid. 82 | """ 83 | ) 84 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 85 | "+Sign+up+User+Registration#5.2-Email-Constraints-Validation-(Implemented)"), name="FR5.2") 86 | @allure.link("https://team-bov4.testit.software/projects/1/tests/97", name="Test IT Test-Case #97") 87 | @pytest.mark.parametrize('sign_up_fixture', USER_EMAIL_INVALID, indirect=True) 88 | def test_email_invalid(self, sign_up_fixture): 89 | response = sign_up_fixture["response"] 90 | 91 | with step("Verify status code is 400"): 92 | assert_status_code(response, 400) 93 | 94 | with step("Verify content-type is 'application/json'"): 95 | assert_content_type(response, "application/json") 96 | 97 | with step("Verify response message is 'Email format is invalid'"): 98 | assert_message_in_response(response, "Email format is invalid") 99 | 100 | @allure.title("Verify that user can not register with too long email") 101 | @allure.description( 102 | """ 103 | WHEN the user attempts to register with too long email, 104 | THEN the system should reject the request, 105 | AND return an error message indicating that the email is too long. 106 | """ 107 | ) 108 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 109 | "+Sign+up+User+Registration#5.2-Email-Constraints-Validation-(Implemented)"), name="FR5.2") 110 | @allure.link("https://team-bov4.testit.software/projects/1/tests/98", name="Test IT Test-Case #98") 111 | @pytest.mark.parametrize('sign_up_fixture', USER_EMAIL_LONG, indirect=True) 112 | def test_email_long(self, sign_up_fixture): 113 | response = sign_up_fixture["response"] 114 | 115 | with step("Verify status code is 400"): 116 | assert_status_code(response, 400) 117 | 118 | with step("Verify content-type is 'application/json'"): 119 | assert_content_type(response, "application/json") 120 | 121 | with step("Verify response message is 'Email format is too long'"): 122 | assert_message_in_response(response, "Email is too long") 123 | 124 | @allure.title("Verify that user can not register with empty password") 125 | @allure.description( 126 | """ 127 | WHEN the user attempts to register with empty password, 128 | THEN the system should reject the request, 129 | AND return an error message indicating that the password must not be empty. 130 | """ 131 | ) 132 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 133 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 134 | @allure.link("https://team-bov4.testit.software/projects/1/tests/100", name="Test IT Test-Case #100") 135 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_EMPTY, indirect=True) 136 | def test_password_empty(self, sign_up_fixture): 137 | response = sign_up_fixture["response"] 138 | 139 | with step("Verify status code is 400"): 140 | assert_status_code(response, 400) 141 | 142 | with step("Verify content-type is 'application/json'"): 143 | assert_content_type(response, "application/json") 144 | 145 | with step("Verify response message is 'Password must not be empty'"): 146 | assert_message_in_response(response, "Password must not be empty") 147 | 148 | @allure.title("Verify that user can not register with too short password") 149 | @allure.description( 150 | """ 151 | WHEN the user attempts to register with too short password, 152 | THEN the system should reject the request, 153 | AND return an error message indicating that the password must be at least 8 characters long. 154 | """ 155 | ) 156 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 157 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 158 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 159 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_SHORT, indirect=True) 160 | def test_password_short(self, sign_up_fixture): 161 | response = sign_up_fixture["response"] 162 | 163 | with step("Verify status code is 400"): 164 | assert_status_code(response, 400) 165 | 166 | with step("Verify content-type is 'application/json'"): 167 | assert_content_type(response, "application/json") 168 | 169 | with step("Verify response message is 'Password must be at least 8 characters long'"): 170 | assert_message_in_response(response, "Password must be at least 8 characters long") 171 | 172 | @allure.title("Verify that user can not register with password without uppercase letter") 173 | @allure.description( 174 | """ 175 | WHEN the user attempts to register with password without uppercase letter, 176 | THEN the system should reject the request, 177 | AND return an error message indicating that the password must contain at least one uppercase letter. 178 | """ 179 | ) 180 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 181 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 182 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 183 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_WITHOUT_UPPERCASE, indirect=True) 184 | def test_password_without_uppercase(self, sign_up_fixture): 185 | response = sign_up_fixture["response"] 186 | 187 | with step("Verify status code is 400"): 188 | assert_status_code(response, 400) 189 | 190 | with step("Verify content-type is 'application/json'"): 191 | assert_content_type(response, "application/json") 192 | 193 | with step("Verify response message is 'Password must contain at least one uppercase letter'"): 194 | assert_message_in_response(response, "Password must contain at least one uppercase letter") 195 | 196 | @allure.title("Verify that user can not register with password without lowercase letter") 197 | @allure.description( 198 | """ 199 | WHEN the user attempts to register with password without lowercase letter, 200 | THEN the system should reject the request, 201 | AND return an error message indicating that the password must contain at least one lowercase letter. 202 | """ 203 | ) 204 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 205 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 206 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 207 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_WITHOUT_LOWERCASE, indirect=True) 208 | def test_password_without_lowercase(self, sign_up_fixture): 209 | response = sign_up_fixture["response"] 210 | 211 | with step("Verify status code is 400"): 212 | assert_status_code(response, 400) 213 | 214 | with step("Verify content-type is 'application/json'"): 215 | assert_content_type(response, "application/json") 216 | 217 | with step("Verify response message is 'Password must contain at least one lowercase letter'"): 218 | assert_message_in_response(response, "Password must contain at least one lowercase letter") 219 | 220 | @allure.title("Verify that user can not register with password without digits") 221 | @allure.description( 222 | """ 223 | WHEN the user attempts to register with password without digits, 224 | THEN the system should reject the request, 225 | AND return an error message indicating that the password must contain at least one digit. 226 | """ 227 | ) 228 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 229 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 230 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 231 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_WITHOUT_DIGITS, indirect=True) 232 | def test_password_without_digits(self, sign_up_fixture): 233 | response = sign_up_fixture["response"] 234 | 235 | with step("Verify status code is 400"): 236 | assert_status_code(response, 400) 237 | 238 | with step("Verify content-type is 'application/json'"): 239 | assert_content_type(response, "application/json") 240 | 241 | with step("Verify response message is 'Password must contain at least one digit'"): 242 | assert_message_in_response(response, "Password must contain at least one digit") 243 | 244 | @allure.title("Verify that user can not register with password without special characters") 245 | @allure.description( 246 | """ 247 | WHEN the user attempts to register with password without special characters, 248 | THEN the system should reject the request, 249 | AND return an error message indicating that the password must contain at least one special character. 250 | """ 251 | ) 252 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 253 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 254 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 255 | @pytest.mark.xfail(reason="Bug is not fixed: https://shorty-url.atlassian.net/browse/SHORTY-85", run=True) 256 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_WITHOUT_SPEC_CHAR, indirect=True) 257 | def test_password_without_spec_char(self, sign_up_fixture): 258 | response = sign_up_fixture["response"] 259 | 260 | with step("Verify status code is 400"): 261 | assert_status_code(response, 400) 262 | 263 | with step("Verify content-type is 'application/json'"): 264 | assert_content_type(response, "application/json") 265 | 266 | with step("Verify response message is 'Password must contain at least one special character'"): 267 | assert_message_in_response(response, "Password must contain at least one special character") 268 | 269 | @allure.title("Verify that user can not register with password containing spaces") 270 | @allure.description( 271 | """ 272 | WHEN the user attempts to register with password containing spaces, 273 | THEN the system should reject the request, 274 | AND return an error message indicating that the password must not contain spaces. 275 | """ 276 | ) 277 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 278 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 279 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 280 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_WITH_SPACES, indirect=True) 281 | def test_password_with_spaces(self, sign_up_fixture): 282 | response = sign_up_fixture["response"] 283 | 284 | with step("Verify status code is 400"): 285 | assert_status_code(response, 400) 286 | 287 | with step("Verify content-type is 'application/json'"): 288 | assert_content_type(response, "application/json") 289 | 290 | with step("Verify response message is 'Password must not contain spaces'"): 291 | assert_message_in_response(response, "Password must not contain spaces") 292 | 293 | @allure.title("Verify that user can not register with too long password") 294 | @allure.description( 295 | """ 296 | WHEN the user attempts to register with too long password, 297 | THEN the system should reject the request, 298 | AND return an error message indicating that the password is too long. 299 | """ 300 | ) 301 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 302 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 303 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 304 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_LONG, indirect=True) 305 | def test_password_long(self, sign_up_fixture): 306 | response = sign_up_fixture["response"] 307 | 308 | with step("Verify status code is 400"): 309 | assert_status_code(response, 400) 310 | 311 | with step("Verify content-type is 'application/json'"): 312 | assert_content_type(response, "application/json") 313 | 314 | with step("Verify response message is 'Password is too long'"): 315 | assert_message_in_response(response, "Password is too long") 316 | 317 | @allure.title("Verify that user can not register with invalid password") 318 | @allure.description( 319 | """ 320 | WHEN the user attempts to register with invalid password, 321 | THEN the system should reject the request, 322 | AND return an error message indicating that the password contains invalid characters. 323 | """ 324 | ) 325 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 326 | "+Sign+up+User+Registration#5.3-Password-Constraints-Validation-(Implemented)"), name="FR5.3") 327 | @allure.link("https://team-bov4.testit.software/projects/1/tests/102", name="Test IT Test-Case #102") 328 | @pytest.mark.xfail(reason="Bug is not fixed: ", run=True) 329 | @pytest.mark.parametrize('sign_up_fixture', USER_PASSWORD_INVALID, indirect=True) 330 | def test_password_invalid(self, sign_up_fixture): 331 | response = sign_up_fixture["response"] 332 | 333 | with step("Verify status code is 400"): 334 | assert_status_code(response, 400) 335 | 336 | with step("Verify content-type is 'application/json'"): 337 | assert_content_type(response, "application/json") 338 | 339 | with step("Verify response message is 'Password contains invalid characters'"): 340 | assert_message_in_response(response, "Password contains invalid characters") 341 | 342 | @allure.title("Verify that user can not register with empty first name") 343 | @allure.description( 344 | """ 345 | WHEN the user attempts to register with empty first name, 346 | THEN the system should reject the request, 347 | AND return an error message indicating that the first name must not be empty. 348 | """ 349 | ) 350 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 351 | "+Sign+up+User+Registration#5.4-First-Name-Constraints-Validation-(Implemented)"), name="FR5.4") 352 | @allure.link("https://team-bov4.testit.software/projects/1/tests/105", name="Test IT Test-Case #105") 353 | @pytest.mark.parametrize('sign_up_fixture', USER_FIRST_NAME_EMPTY, indirect=True) 354 | def test_first_name_empty(self, sign_up_fixture): 355 | response = sign_up_fixture["response"] 356 | 357 | with step("Verify status code is 400"): 358 | assert_status_code(response, 400) 359 | 360 | with step("Verify content-type is 'application/json'"): 361 | assert_content_type(response, "application/json") 362 | 363 | with step("Verify response message is 'First name must not be empty'"): 364 | assert_message_in_response(response, "First name must not be empty") 365 | 366 | @allure.title("Verify that user can not register with invalid first name") 367 | @allure.description( 368 | """ 369 | WHEN the user attempts to register with invalid first name, 370 | THEN the system should reject the request, 371 | AND return an error message indicating that the first name contains invalid characters. 372 | """ 373 | ) 374 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 375 | "+Sign+up+User+Registration#5.4-First-Name-Constraints-Validation-(Implemented)"), name="FR5.4") 376 | @allure.link("https://team-bov4.testit.software/projects/1/tests/107", name="Test IT Test-Case #107") 377 | @pytest.mark.parametrize('sign_up_fixture', USER_FIRST_NAME_INVALID, indirect=True) 378 | def test_first_name_invalid(self, sign_up_fixture): 379 | response = sign_up_fixture["response"] 380 | 381 | with step("Verify status code is 400"): 382 | assert_status_code(response, 400) 383 | 384 | with step("Verify content-type is 'application/json'"): 385 | assert_content_type(response, "application/json") 386 | 387 | with step("Verify response message is 'First name contains invalid characters'"): 388 | assert_message_in_response(response, "First name contains invalid characters") 389 | 390 | @allure.title("Verify that user can not register with too long first name") 391 | @allure.description( 392 | """ 393 | WHEN the user attempts to register with too long first name, 394 | THEN the system should reject the request, 395 | AND return an error message indicating that the first name is too long. 396 | """ 397 | ) 398 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 399 | "+Sign+up+User+Registration#5.4-First-Name-Constraints-Validation-(Implemented)"), name="FR5.4") 400 | @allure.link("https://team-bov4.testit.software/projects/1/tests/107", name="Test IT Test-Case #107") 401 | @pytest.mark.parametrize('sign_up_fixture', USER_FIRST_NAME_LONG, indirect=True) 402 | def test_first_name_long(self, sign_up_fixture): 403 | response = sign_up_fixture["response"] 404 | 405 | with step("Verify status code is 400"): 406 | assert_status_code(response, 400) 407 | 408 | with step("Verify content-type is 'application/json'"): 409 | assert_content_type(response, "application/json") 410 | 411 | with step("Verify response message is 'First name is too long'"): 412 | assert_message_in_response(response, "First name is too long") 413 | 414 | @allure.title("Verify that user can not register with empty last name") 415 | @allure.description( 416 | """ 417 | WHEN the user attempts to register with empty last name, 418 | THEN the system should reject the request, 419 | AND return an error message indicating that the last name must not be empty. 420 | """ 421 | ) 422 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 423 | "+Sign+up+User+Registration#5.5-Last-Name-Constraints-Validation-(Implemented)"), name="FR5.5") 424 | @allure.link("https://team-bov4.testit.software/projects/1/tests/108", name="Test IT Test-Case #108") 425 | @pytest.mark.parametrize('sign_up_fixture', USER_LAST_NAME_EMPTY, indirect=True) 426 | def test_last_name_empty(self, sign_up_fixture): 427 | response = sign_up_fixture["response"] 428 | 429 | with step("Verify status code is 400"): 430 | assert_status_code(response, 400) 431 | 432 | with step("Verify content-type is 'application/json'"): 433 | assert_content_type(response, "application/json") 434 | 435 | with step("Verify response message is 'Last name must not be empty'"): 436 | assert_message_in_response(response, "Last name must not be empty") 437 | 438 | @allure.title("Verify that user can not register with invalid last name") 439 | @allure.description( 440 | """ 441 | WHEN the user attempts to register with invalid last name, 442 | THEN the system should reject the request, 443 | AND return an error message indicating that the last name contains invalid characters. 444 | """ 445 | ) 446 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 447 | "+Sign+up+User+Registration#5.5-Last-Name-Constraints-Validation-(Implemented)"), name="FR5.5") 448 | @allure.link("https://team-bov4.testit.software/projects/1/tests/110", name="Test IT Test-Case #110") 449 | @pytest.mark.parametrize('sign_up_fixture', USER_LAST_NAME_INVALID, indirect=True) 450 | def test_last_name_invalid(self, sign_up_fixture): 451 | response = sign_up_fixture["response"] 452 | 453 | with step("Verify status code is 400"): 454 | assert_status_code(response, 400) 455 | 456 | with step("Verify content-type is 'application/json'"): 457 | assert_content_type(response, "application/json") 458 | 459 | with step("Verify response message is 'Last name contains invalid characters'"): 460 | assert_message_in_response(response, "Last name contains invalid characters") 461 | 462 | @allure.title("Verify that user can not register with too long last name") 463 | @allure.description( 464 | """ 465 | WHEN the user attempts to register with too long last name, 466 | THEN the system should reject the request, 467 | AND return an error message indicating that the last name is too long. 468 | """ 469 | ) 470 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 471 | "+Sign+up+User+Registration#5.5-Last-Name-Constraints-Validation-(Implemented)"), name="FR5.5") 472 | @allure.link("https://team-bov4.testit.software/projects/1/tests/110", name="Test IT Test-Case #110") 473 | @pytest.mark.parametrize('sign_up_fixture', USER_LAST_NAME_LONG, indirect=True) 474 | def test_last_name_long(self, sign_up_fixture): 475 | response = sign_up_fixture["response"] 476 | 477 | with step("Verify status code is 400"): 478 | assert_status_code(response, 400) 479 | 480 | with step("Verify content-type is 'application/json'"): 481 | assert_content_type(response, "application/json") 482 | 483 | with step("Verify response message is 'Last name is too long'"): 484 | assert_message_in_response(response, "Last name is too long") 485 | 486 | @allure.title("Verify that user can not register with empty country") 487 | @allure.description( 488 | """ 489 | WHEN the user attempts to register with empty country, 490 | THEN the system should reject the request, 491 | AND return an error message indicating that the country name must not be empty. 492 | """ 493 | ) 494 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 495 | "+Sign+up+User+Registration#5.6-Country-Constraints-Validation-(Implemented)"), name="FR5.6") 496 | # @allure.link("https://team-bov4.testit.software/projects/1/tests/113", name="Test IT Test-Case #113") 497 | @pytest.mark.parametrize('sign_up_fixture', USER_COUNTRY_EMPTY, indirect=True) 498 | def test_country_empty(self, sign_up_fixture): 499 | response = sign_up_fixture["response"] 500 | 501 | with step("Verify status code is 400"): 502 | assert_status_code(response, 400) 503 | 504 | with step("Verify content-type is 'application/json'"): 505 | assert_content_type(response, "application/json") 506 | 507 | with step("Verify response message is 'Country name must not be empty'"): 508 | assert_message_in_response(response, "Country name must not be empty") 509 | 510 | @allure.title("Verify that user can not register with invalid country") 511 | @allure.description( 512 | """ 513 | WHEN the user attempts to register with invalid country, 514 | THEN the system should reject the request, 515 | AND return an error message indicating that the country contains invalid characters. 516 | """ 517 | ) 518 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 519 | "+Sign+up+User+Registration#5.6-Country-Constraints-Validation-(Implemented)"), name="FR5.6") 520 | # @allure.link("https://team-bov4.testit.software/projects/1/tests/113", name="Test IT Test-Case #113") 521 | @pytest.mark.parametrize('sign_up_fixture', USER_COUNTRY_INVALID, indirect=True) 522 | def test_country_invalid(self, sign_up_fixture): 523 | response = sign_up_fixture["response"] 524 | 525 | with step("Verify status code is 400"): 526 | assert_status_code(response, 400) 527 | 528 | with step("Verify content-type is 'application/json'"): 529 | assert_content_type(response, "application/json") 530 | 531 | with step("Verify response message is 'Country name contains invalid characters'"): 532 | assert_message_in_response(response, "Country name contains invalid characters") 533 | 534 | @allure.title("Verify that user can not register with too long country") 535 | @allure.description( 536 | """ 537 | WHEN the user attempts to register with too long country, 538 | THEN the system should reject the request, 539 | AND return an error message indicating that the country name is too long. 540 | """ 541 | ) 542 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 543 | "+Sign+up+User+Registration#5.6-Country-Constraints-Validation-(Implemented)"), name="FR5.6") 544 | # @allure.link("https://team-bov4.testit.software/projects/1/tests/113", name="Test IT Test-Case #113") 545 | @pytest.mark.parametrize('sign_up_fixture', USER_COUNTRY_LONG, indirect=True) 546 | def test_country_long(self, sign_up_fixture): 547 | response = sign_up_fixture["response"] 548 | 549 | with step("Verify status code is 400"): 550 | assert_status_code(response, 400) 551 | 552 | with step("Verify content-type is 'application/json'"): 553 | assert_content_type(response, "application/json") 554 | 555 | with step("Verify response message is 'Country name is too long'"): 556 | assert_message_in_response(response, "Country name is too long") 557 | 558 | @allure.title("Verify that user can not register with invalid age") 559 | @allure.description( 560 | """ 561 | WHEN the user attempts to register with invalid age, 562 | THEN the system should reject the request, 563 | AND return an error message indicating that the age must be between 13 and 120. 564 | """ 565 | ) 566 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 567 | "+Sign+up+User+Registration#5.7-Age-Constraints-Validation-(Implemented)"), name="FR5.7") 568 | @allure.link("https://team-bov4.testit.software/projects/1/tests/111", name="Test IT Test-Case #111") 569 | @allure.link("https://team-bov4.testit.software/projects/1/tests/113", name="Test IT Test-Case #113") 570 | @pytest.mark.parametrize('sign_up_fixture', USER_AGE_INVALID, indirect=True) 571 | def test_age_invalid(self, sign_up_fixture): 572 | response = sign_up_fixture["response"] 573 | 574 | with step("Verify status code is 400"): 575 | assert_status_code(response, 400) 576 | 577 | with step("Verify content-type is 'application/json'"): 578 | assert_content_type(response, "application/json") 579 | 580 | with step("Verify response message is 'Age must be between 13 and 120'"): 581 | assert_message_in_response(response, "Age must be between 13 and 120") 582 | 583 | @allure.title("Verify that user can not register with not a number age") 584 | @allure.description( 585 | """ 586 | WHEN the user attempts to register with not a number age, 587 | THEN the system should reject the request, 588 | AND return an error message indicating that the age must be a valid integer. 589 | """ 590 | ) 591 | @allure.link(("https://shorty-url.atlassian.net/wiki/spaces/SKB/pages/16252946/5." 592 | "+Sign+up+User+Registration#5.7-Age-Constraints-Validation-(Implemented)"), name="FR5.7") 593 | @allure.link("https://team-bov4.testit.software/projects/1/tests/113", name="Test IT Test-Case #113") 594 | @pytest.mark.xfail(reason="Bug is not fixed: https://shorty-url.atlassian.net/browse/SHORTY-87", run=True) 595 | @pytest.mark.parametrize('sign_up_fixture', USER_AGE_NOT_NUMBER, indirect=True) 596 | def test_age_not_number(self, sign_up_fixture): 597 | response = sign_up_fixture["response"] 598 | 599 | with step("Verify status code is 400"): 600 | assert_status_code(response, 400) 601 | 602 | with step("Verify content-type is 'application/json'"): 603 | assert_content_type(response, "application/json") 604 | 605 | with step("Verify response message is 'Age must be a valid integer'"): 606 | assert_message_in_response(response, "Age must be a valid integer") 607 | -------------------------------------------------------------------------------- /API/TEST/test_uniqueness_short_url.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | import pytest 5 | from allure import step 6 | 7 | from API.FRAMEWORK.assertion.assert_unique_short_url import is_unique_short_url 8 | 9 | 10 | @allure.feature("Short URL generation") 11 | @allure.link("https://short-link.zufargroup.com/webjars/swagger-ui/index.html#/URL%20Shortening/shortenUrl", 12 | name="Swagger") 13 | @allure.link("https://team-bov4.testit.software/projects/1/tests/55", 14 | name="Test IT Test-Case #55") 15 | @allure.severity(allure.severity_level.BLOCKER) 16 | @allure.title("Verify that created short url is unique") 17 | @allure.description( 18 | """WHEN user send POST request to create short url 19 | THEN created short url is unique""" 20 | ) 21 | @pytest.mark.parametrize('create_short_url', [f'https://ya{time.time()}.ru'], indirect=True) 22 | def test_uniqueness_short_url(mongodb_fixture, create_short_url): 23 | short_url_list = mongodb_fixture.mongodb_client.get_all_short_url() 24 | 25 | with step('Create short url'): 26 | created_short_url = create_short_url["created_short_url"] 27 | 28 | with step('Verify that created short url is unique'): 29 | is_unique_short_url(created_short_url, short_url_list) 30 | -------------------------------------------------------------------------------- /API/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/API/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URL-Shortener-QA 2 | 3 | ## Automated tests for URL-Shortener Project 4 | 5 | ### Structure of the project 6 | 7 | ## Report 8 | (!) BE SURE TO INSTALL ALLURE -> https://allurereport.org/docs/gettingstarted/installation/ 9 | 10 | To get the Allure report on the local computer, follow these steps in root directory: 11 | ```bash 12 | pytest 13 | allure generate ./allure-results --clean -o ./allure-report 14 | allure serve ./allure-results 15 | ``` 16 | or you can manually open index.html from folder allure-report -------------------------------------------------------------------------------- /configs.py: -------------------------------------------------------------------------------- 1 | HOST = "https://short-link.zufargroup.com" 2 | 3 | MONGODB_DATABASE = 'ShortyUrlDb' 4 | MONGODB_COLLECTION_URL = 'url_mappings' 5 | MONGODB_COLLECTION_USER = 'user_details' 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --alluredir=allure-results 4 | --allure-no-capture 5 | --clean-alluredir 6 | --reruns 2 7 | --reruns-delay 5 8 | -s 9 | 10 | markers = 11 | critical: 12 | high: 13 | medium: 14 | low: 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunagatov/URL-Shortener-QA/73b15732ef07968a93ad0e8f0f107e14cfa67a79/requirements.txt --------------------------------------------------------------------------------