From 9964c363269e925978f3b37ef56e205bae6b920d Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Sun, 19 Sep 2021 18:15:38 +0300 Subject: [PATCH 1/3] Add GET and POST endpoints --- .gitignore | 3 +++ main.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .gitignore create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb80269 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/__pycache__ +/venv \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..11a52e4 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +wsd_app = FastAPI() + +user_list = [ + { + 'mail': 'abc@yandex.ru', + 'password': 'abc_pwd' + }, + { + 'mail': 'qwerty@gmail.com', + 'password': 'qwerty' + } +] + + +@wsd_app.get("/statistics/") +async def get_statistics(indicator: str): + if indicator == "num_of_users": + return {'number of users': len(user_list)} + raise HTTPException(status_code=404, detail="Don't know this indicator") + + +class User(BaseModel): + mail: str = Field(..., regex=r".+@.+\..+") + password1: str + password2: str + + +@wsd_app.post("/registration/", status_code=201) +async def create_account(user: User): + if user.mail in [d['mail'] for d in user_list]: + raise HTTPException(status_code=403, detail="This mail has been already registered") + if user.password1 != user.password2: + raise HTTPException(status_code=403, detail="Passwords don't match") + user_list.append({'mail': user.mail, 'password': user.password1}) + return "Account is created" From 19d5c5d9b806add16fb139d5902ededbbede7d55 Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Mon, 27 Sep 2021 02:25:15 +0300 Subject: [PATCH 2/3] Add task 2 solution --- .gitignore | 5 ++- README.md | 16 +++++++- databases/users.py | 39 ++++++++++++++++++ main.py | 37 +++++++++-------- models/users.py | 21 ++++++++++ tests/integration_tests.py | 58 ++++++++++++++++++++++++++ tests/unit_tests.py | 84 ++++++++++++++++++++++++++++++++++++++ utils.py | 17 ++++++++ 8 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 databases/users.py create mode 100644 models/users.py create mode 100644 tests/integration_tests.py create mode 100644 tests/unit_tests.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index bb80269..8d1da93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea -/__pycache__ -/venv \ No newline at end of file +*/__pycache__ +/venv +/.pytest_cash \ No newline at end of file diff --git a/README.md b/README.md index 68a01b0..8a3cf38 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# WebServiceDevelopment \ No newline at end of file +# WebServiceDevelopment + +Это репозиторий, в котором будут храниться решение ДЗ по курсу +разработки веб-сервисов на питоне. Домашними заданиями является работа +над проектом. Пока что в качестве проекта сделать "датаграмм" (типо +"телеграмм", но "датаграмм", потому что на курсе по Java нам предлагали +различать UDP и TCP так, что UDP -- это датаграмма, а TCP -- телеграмма). + +## Тестирование +Перед тестированием установите: +`pip install email-validator` + +Запуск юнит тестов: `python -m pytest tests/unit_tests.py` + +Запуск интеграционных тестов: `python -m pytest tests/integration_tests.py` \ No newline at end of file diff --git a/databases/users.py b/databases/users.py new file mode 100644 index 0000000..96da212 --- /dev/null +++ b/databases/users.py @@ -0,0 +1,39 @@ +from typing import Dict, Optional + + +class UserMeta: + def __init__(self, email: str, name: str): + self.email = email + self.name = name + + +__id_pwd: Dict[int, str] = {} +__id_user_meta: Dict[int, UserMeta] = {} + + +def get_user_by_auth(email: str, password: str) -> Optional[UserMeta]: + for (id, user_meta) in __id_user_meta.items(): + if user_meta.email == email: + if __id_pwd[id] != password: + return None + return user_meta + return None + + +def is_email_occupied(email: str) -> bool: + for (_, user_meta) in __id_user_meta.items(): + if user_meta.email == email: + return True + return False + + +def add_user(email: str, name: str, password: str): + if is_email_occupied(email): + raise ValueError("User with this email exists") + id = len(__id_pwd) + __id_pwd[id] = password + __id_user_meta[id] = UserMeta(email, name) + + +def get_num_of_users(): + return len(__id_user_meta) \ No newline at end of file diff --git a/main.py b/main.py index 11a52e4..fe6c1e2 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,8 @@ from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field +from models.users import UserCreateModel, UserAuthModel +from databases.users import get_user_by_auth, is_email_occupied, add_user, get_num_of_users -wsd_app = FastAPI() +datagram_app = FastAPI() user_list = [ { @@ -15,24 +16,24 @@ ] -@wsd_app.get("/statistics/") +@datagram_app.get("/statistics/") async def get_statistics(indicator: str): - if indicator == "num_of_users": - return {'number of users': len(user_list)} - raise HTTPException(status_code=404, detail="Don't know this indicator") + if indicator != "num_of_users": + raise HTTPException(status_code=404, detail="Don't know this indicator") + get_num_of_users() -class User(BaseModel): - mail: str = Field(..., regex=r".+@.+\..+") - password1: str - password2: str +@datagram_app.post("/users/registration", status_code=201) +async def register_user(user: UserCreateModel): + if is_email_occupied(user.email): + raise HTTPException(status_code=403, detail="This mail has been already registered") + add_user(user.email, user.name, user.password.get_secret_value()) + return "Registered successfully" -@wsd_app.post("/registration/", status_code=201) -async def create_account(user: User): - if user.mail in [d['mail'] for d in user_list]: - raise HTTPException(status_code=403, detail="This mail has been already registered") - if user.password1 != user.password2: - raise HTTPException(status_code=403, detail="Passwords don't match") - user_list.append({'mail': user.mail, 'password': user.password1}) - return "Account is created" +@datagram_app.post("/users/auth") +async def authorize(user_auth: UserAuthModel): + user = get_user_by_auth(user_auth.email, user_auth.password.get_secret_value()) + if user is None: + raise HTTPException(status_code=403, detail="Wrong mail or password") + return user diff --git a/models/users.py b/models/users.py new file mode 100644 index 0000000..0d13046 --- /dev/null +++ b/models/users.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, EmailStr, SecretStr, validator +from utils import validate_new_password + + +class UserAuthModel(BaseModel): + email: EmailStr + password: SecretStr + + @validator('password') + def password_acceptable(cls, v): + validate_new_password(v) + return v + + +class UserCreateModel(UserAuthModel): + name: str + + @validator('name') + def name_alphanumeric(cls, v): + assert v.isalnum(), 'must be alphanumeric' + return v diff --git a/tests/integration_tests.py b/tests/integration_tests.py new file mode 100644 index 0000000..f130405 --- /dev/null +++ b/tests/integration_tests.py @@ -0,0 +1,58 @@ +import pytest +from fastapi.testclient import TestClient + +from main import datagram_app + +client = TestClient(datagram_app) + + +@pytest.fixture() +def setup_and_teardown_db(): + from databases.users import __id_pwd, __id_user_meta + save_pwd = __id_pwd.copy() + save_meta = __id_user_meta.copy() + __id_pwd.clear() + __id_user_meta.clear() + + __id_pwd = save_pwd + __id_user_meta = save_meta + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ registration test ~~~~~~~~~~~~~~~~~~ +def test_registration_success(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + + +def test_registration_wrong_pwd_format(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "a1!"}) + assert rs.status_code == 422 + + +def test_registration_mail_occupied(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 403 + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ auth test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +def test_auth_success(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "a@yandex.ru", "password": "Aa1!"}) + assert rs.status_code == 200 + + +def test_auth_user_not_exist(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "b@yandex.ru", "password": "Aa1!"}) + assert rs.status_code == 403 + + +def test_auth_wrong_password(setup_and_teardown_db): + rs = client.post("/users/registration", json={"email": "a@yandex.ru", "name": "a", "password": "Aa1!"}) + assert rs.status_code == 201 + rs = client.post("/users/auth", json={"email": "a@yandex.ru", "password": "Ba1!"}) + assert rs.status_code == 403 diff --git a/tests/unit_tests.py b/tests/unit_tests.py new file mode 100644 index 0000000..b4c5382 --- /dev/null +++ b/tests/unit_tests.py @@ -0,0 +1,84 @@ +import pytest +from utils import validate_new_password +from databases.users import add_user, get_user_by_auth +from pydantic import SecretStr + + +# ~~~~~~~~~~~~~~~~~ validate password tests ~~~~~~~~~~~~~~~~~~~~ +def test_correct_password(): + validate_new_password(SecretStr("Ab1!")) + + +def test_miss_lowercase(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("A1!")) + assert ve.value.args[1] == r"[a-z]" + + +def test_miss_uppercase(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("b1!")) + assert ve.value.args[1] == r"[A-Z]" + + +def test_miss_digit(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab!")) + assert ve.value.args[1] == r"[0-9]" + + +def test_miss_special_char(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab1")) + assert ve.value.args[1] == r"[!@#_.]" + + +def test_wrong_char(): + with pytest.raises(ValueError) as ve: + validate_new_password(SecretStr("Ab1!$")) + assert ve.value.args[1] == "wrong char" + + +@pytest.fixture() +def setup_and_teardown_db(): + from databases.users import __id_pwd, __id_user_meta + save_pwd = __id_pwd.copy() + save_meta = __id_user_meta.copy() + __id_pwd.clear() + __id_user_meta.clear() + + __id_pwd = save_pwd + __id_user_meta = save_meta + + +# ~~~~~~~~~~~~~~~~~ add new user tests ~~~~~~~~~~~~~~~~~~~~ +def test_add_user_correct_params(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + add_user(email="b@yandex.ru", name="b", password="Bb2@") + + +def test_add_user_with_existing_name(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + add_user(email="b@yandex.ru", name="a", password="Aa1!") + + +def test_add_user_occupied_email(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + with pytest.raises(ValueError): + add_user(email="a@yandex.ru", name="b", password="Bb2@") + + +# # ~~~~~~~~~~~~~~~~~ get user meta tests ~~~~~~~~~~~~~~~~~~~~ +def test_get_user_meta_correct_params(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="a@yandex.ru", password="Aa1!") is not None + + +def test_get_user_meta_occupied_email(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="aa@yandex.ru", password="Aa1!") is None + + +def test_get_user_meta_wrong_password(setup_and_teardown_db): + add_user(email="a@yandex.ru", name="a", password="Aa1!") + assert get_user_by_auth(email="a@yandex.ru", password="Aa1!!") is None diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0d07bcd --- /dev/null +++ b/utils.py @@ -0,0 +1,17 @@ +from pydantic import SecretStr +import re + + +def validate_new_password(pwd: SecretStr): + value = pwd.get_secret_value() + if re.search(r"[a-z]", value) is None: + raise ValueError("password must contain lowercase letter", r"[a-z]") + if re.search(r"[A-Z]", value) is None: + raise ValueError("password must contain uppercase letter", r"[A-Z]") + if re.search(r"[0-9]", value) is None: + raise ValueError("password must contain digit", r"[0-9]") + if re.search(r"[!@#_.]", value) is None: + raise ValueError("password must contain special character '!', '@', '#', '_' or '.'", r"[!@#_.]") + m = re.search(r"[^a-zA-Z0-9!@#_.]", value) + if m is not None: + raise ValueError("password contains forbidden character " + m.group(0), "wrong char") From f3f2ae2c938c9afd5458226e6f45073d324523db Mon Sep 17 00:00:00 2001 From: DF5HSE Date: Tue, 5 Oct 2021 19:26:35 +0300 Subject: [PATCH 3/3] Fix mistakes in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8a3cf38..e02db3a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # WebServiceDevelopment -Это репозиторий, в котором будут храниться решение ДЗ по курсу +Это репозиторий, в котором будут храниться решения ДЗ по курсу разработки веб-сервисов на питоне. Домашними заданиями является работа -над проектом. Пока что в качестве проекта сделать "датаграмм" (типо -"телеграмм", но "датаграмм", потому что на курсе по Java нам предлагали +над проектом. Пока что в качестве проекта сделать "датаграм" (типо +"телеграм", но "датаграм", потому что на курсе по Java нам предлагали различать UDP и TCP так, что UDP -- это датаграмма, а TCP -- телеграмма). ## Тестирование