Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
FireFading committed Apr 14, 2023
1 parent 4fa8d95 commit 55c6df3
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 87 deletions.
8 changes: 8 additions & 0 deletions app/models/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ def upgrade_rating(self, rating: float):
self.avg_rating = rating
else:
self.avg_rating = (self.avg_rating * self.reviews_count + rating) / (self.reviews_count + 1)
self.reviews_count += 1

def downgrade_rating(self, rating: float):
if self.reviews_count > 1:
self.avg_rating = (self.avg_rating * self.reviews_count - rating) / (self.reviews_count - 1)
else:
self.avg_rating = 0
self.reviews_count -= 1


fields = [column.name for column in Product.__table__.columns]
24 changes: 23 additions & 1 deletion app/routers/rating.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async def get_product_ratings(product_id: uuid.UUID, session: AsyncSession = Dep


@router.get(
"/get/avg/{product_id}/",
"/avg/{product_id}/",
status_code=status.HTTP_200_OK,
summary="Получение среднего рейтинга продукта по guid",
)
Expand Down Expand Up @@ -65,3 +65,25 @@ async def create_new_ratings(
product.upgrade_rating(rating=create_rating.stars)
await product.update(session=session)
return {"detail": messages.RATING_CREATED}


@router.delete("/delete/{product_id}/", status_code=status.HTTP_200_OK, summary="Удаление оценки")
async def delete_rating(
product_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
authorize: AuthJWT = Depends(),
credentials: HTTPAuthorizationCredentials = Security(security),
):
authorize.jwt_required()
email = authorize.get_jwt_subject()
user = await get_user_or_404(email=email, session=session)
if not (rating := await m_Rating.get(session=session, user_id=user.guid, product_id=product_id)):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=messages.RATING_NOT_FOUND,
)
product = await get_product_or_404(guid=product_id, session=session)
product.downgrade_rating(rating=rating.stars)
await product.update(session=session)
await m_Rating.delete(session=session, instances=rating)
return {"detail": messages.RATING_DELETED}
7 changes: 3 additions & 4 deletions app/schemas/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import uuid

from app.utils.validators import validate_name, validate_password
from fastapi import HTTPException
from pydantic import BaseModel, EmailStr, validator


Expand All @@ -17,15 +16,15 @@ class CreateUser(Email, BaseModel):
password: str

@validator("password")
def validate_password(cls, password: str) -> str | HTTPException:
def validate_password(cls, password: str) -> str | ValueError:
return validate_password(password=password)


class Name(BaseModel):
name: str

@validator("name")
def validate_name(cls, name: str | None = None) -> str | None | HTTPException:
def validate_name(cls, name: str | None = None) -> str | None | ValueError:
return validate_name(name=name)


Expand All @@ -48,5 +47,5 @@ class UpdatePassword(BaseModel):
confirm_password: str

@validator("confirm_password")
def validate_password(cls, confirm_password: str) -> str | HTTPException:
def validate_password(cls, confirm_password: str) -> str | ValueError:
return validate_password(password=confirm_password)
2 changes: 1 addition & 1 deletion app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Config:


class Settings(BaseSettings):
database_url: PostgresDsn = Field(env="DATABASE_URL")
database_url: PostgresDsn | str = Field(env="DATABASE_URL")

postgres_db: str = Field(env="POSTGRES_DB")
postgres_host: str = Field(env="POSTGRES_HOST")
Expand Down
2 changes: 2 additions & 0 deletions app/utils/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class Messages:

RATING_ALREADY_EXISTS = "Продукт уже оценен данным пользователем"
RATING_CREATED = "Оценка продукта принята"
RATING_NOT_FOUND = "Оценка не найдена"
RATING_DELETED = "Оценка удалена"

ACCESS_DENIED = "Доступ запрещен"

Expand Down
4 changes: 2 additions & 2 deletions app/utils/password.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import bcrypt


def get_hashed_password(password: str) -> str:
def get_hashed_password(password: str) -> bytes:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt())


def verify_password(password: str, hashed_password: str) -> bool:
def verify_password(password: str, hashed_password: bytes) -> bool:
return bcrypt.checkpw(password.encode(), hashed_password)
29 changes: 21 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from tests.settings import SQLALCHEMY_DATABASE_URL, Urls, User, test_product
from tests.settings import Urls, create_product_schema, login_credentials_schema, rating, settings

engine = create_async_engine(
SQLALCHEMY_DATABASE_URL,
settings.database_url,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)

Session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


@pytest_asyncio.fixture()
Expand All @@ -33,15 +33,15 @@ async def app() -> AsyncGenerator:
async def db_session(app: FastAPI) -> AsyncGenerator:
connection = await engine.connect()
transaction = await connection.begin()
session = Session(bind=connection)
session = async_session(bind=connection)
yield session
await session.close()
await transaction.rollback()
await connection.close()


@pytest_asyncio.fixture
async def client(app: FastAPI, db_session: Session) -> AsyncGenerator | TestClient:
async def client(app: FastAPI, db_session: async_session) -> AsyncGenerator | TestClient:
async def _get_test_db():
yield db_session

Expand All @@ -53,13 +53,13 @@ async def _get_test_db():
@pytest_asyncio.fixture
async def register_user(client: AsyncGenerator | TestClient, mocker: MockerFixture) -> AsyncGenerator:
mocker.patch("app.routers.users.send_mail", return_value=True)
response = client.post(Urls.REGISTER, json={"email": User.EMAIL, "password": User.PASSWORD})
response = client.post(Urls.REGISTER, json=login_credentials_schema)
assert response.status_code == status.HTTP_201_CREATED


@pytest_asyncio.fixture
async def auth_client(register_user, client: AsyncGenerator | TestClient) -> AsyncGenerator | TestClient:
response = client.post(Urls.LOGIN, json={"email": User.EMAIL, "password": User.PASSWORD})
response = client.post(Urls.LOGIN, json=login_credentials_schema)
assert response.status_code == status.HTTP_200_OK
access_token = response.json().get("access_token")
client.headers.update({"Authorization": f"Bearer {access_token}"})
Expand All @@ -70,5 +70,18 @@ async def auth_client(register_user, client: AsyncGenerator | TestClient) -> Asy
async def create_product(
auth_client: AsyncGenerator | TestClient,
) -> AsyncGenerator | TestClient:
response = auth_client.post(Urls.CREATE_PRODUCT, json=test_product)
response = auth_client.post(Urls.CREATE_PRODUCT, json=create_product_schema)
assert response.status_code == status.HTTP_201_CREATED


@pytest_asyncio.fixture
async def create_rating(auth_client: AsyncGenerator | TestClient, create_product) -> AsyncGenerator | TestClient:
response = auth_client.post(Urls.CREATE_RATING, json=rating)
assert response.status_code == status.HTTP_201_CREATED


# @pytest_asyncio.fixture
# async def get_product(auth_client: AsyncGenerator | TestClient, create_product) -> AsyncGenerator | TestClient:
# response = auth_client.get(Urls.GET_PRODUCTS)
# assert response.status_code == status.HTTP_200_OK
# result = response.json()[0]
91 changes: 57 additions & 34 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from dataclasses import dataclass
from datetime import datetime

Expand All @@ -8,49 +9,58 @@

load_dotenv(dotenv_path="../")

SQLALCHEMY_DATABASE_URL = "sqlite+aiosqlite://"


@dataclass
class Urls:
LOGIN = "/accounts/login/"
REGISTER = "/accounts/register/"
LOGOUT = "/accounts/logout/"
USER_INFO = "/accounts/profile/"
LOGIN: str = "/accounts/login/"
REGISTER: str = "/accounts/register/"
LOGOUT: str = "/accounts/logout/"
USER_INFO: str = "/accounts/profile/"

UPDATE_EMAIL = "/accounts/profile/update/email/"
UPDATE_PHONE = "/accounts/profile/update/phone/"
UPDATE_NAME = "/accounts/profile/update/name/"
UPDATE_EMAIL: str = "/accounts/profile/update/email/"
UPDATE_PHONE: str = "/accounts/profile/update/phone/"
UPDATE_NAME: str = "/accounts/profile/update/name/"

FORGOT_PASSWORD = "/accounts/forgot-password/"
RESET_PASSWORD = "/accounts/reset-password/"
CHANGE_PASSWORD = "/accounts/change-password/"
FORGOT_PASSWORD: str = "/accounts/forgot-password/"
RESET_PASSWORD: str = "/accounts/reset-password/"
CHANGE_PASSWORD: str = "/accounts/change-password/"

DELETE_PROFILE = "/accounts/profile/delete/"
DELETE_PROFILE: str = "/accounts/profile/delete/"

CREATE_PRODUCT = "/products/new/"
GET_PRODUCTS = "/products/get/"
DELETE_PRODUCT = "/products/delete/"
CREATE_PRODUCT: str = "/products/new/"
GET_PRODUCTS: str = "/products/get/"
DELETE_PRODUCT: str = "/products/delete/"

CREATE_RATING = "/products/ratings/new/"
GET_RATINGS = "/products/ratings/get/"
CREATE_RATING: str = "/products/ratings/new/"
GET_RATINGS: str = "/products/ratings/get/"
GET_AVG_RATING: str = "/products/ratings/avg/"
DELETE_RATING: str = "/products/ratings/delete/"


@dataclass
class User:
EMAIL = "test@mail.ru"
NEW_EMAIL = "new_test@mail.ru"
WRONG_EMAIL = "wrong_test@mail.ru"
EMAIL: str = "test@mail.ru"
NEW_EMAIL: str = "new_test@mail.ru"
WRONG_EMAIL: str = "wrong_test@mail.ru"

PHONE: str | None = None
NEW_PHONE: str = "89101111111"

PHONE = None
NEW_PHONE = "89101111111"
NAME: str | None = None
NEW_NAME: str = "UserName"

NAME = None
NEW_NAME = "UserName"
PASSWORD: str = "Abc123!@#def456$%^"
NEW_PASSWORD: str = "NewAbc123!@#def456$%^"
WRONG_PASSWORD: str = "WrongAbc123!@#def456$%^"

PASSWORD = "Abc123!@#def456$%^"
NEW_PASSWORD = "NewAbc123!@#def456$%^"
WRONG_PASSWORD = "WrongAbc123!@#def456$%^"

@dataclass
class Product:
GUID: uuid.UUID = uuid.UUID("00000000-0000-0000-0000-000000000000")
NAME: str = "test_product"
DESCRIPTION: str = "test_description"
PRODUCER: str = "test_producer"
PRICE: float = 10000.0


class BaseTestSettings(Settings):
Expand All @@ -63,13 +73,26 @@ def create_fake_token(expires_in: datetime = datetime(1999, 1, 1), email: str =


settings = BaseTestSettings(_env_file=".env.example")
zero_uuid = "00000000-0000-0000-0000-000000000000"

login_credentials_schema = {"email": User.EMAIL, "password": User.PASSWORD}

change_password_schema = {
"password": User.NEW_PASSWORD,
"confirm_password": User.NEW_PASSWORD,
}

wrong_change_password_schema = {
"password": User.NEW_PASSWORD,
"confirm_password": User.WRONG_PASSWORD,
}

test_product = {
"name": "test_product",
"description": "test_description",
"producer": "test_producer",
"price": 10000.0,
create_product_schema = {
"guid": Product.GUID,
"name": Product.NAME,
"description": Product.DESCRIPTION,
"producer": Product.PRODUCER,
"price": Product.PRICE,
}

rating = {"stars": 2, "product_id": "00000000-0000-0000-0000-000000000000"}
rating = {"stars": 2, "product_id": Product.GUID}
Loading

0 comments on commit 55c6df3

Please sign in to comment.