diff --git a/app.py b/app.py index 51503db..5768f20 100644 --- a/app.py +++ b/app.py @@ -7,9 +7,10 @@ from presentation.like import api as like_api from presentation.ping import api as ping_api from presentation.product import api as product_api +from settings import Settings -def create_app(): +def create_app(settings: Settings): app = FastAPI() app.add_middleware( @@ -20,6 +21,8 @@ def create_app(): allow_headers=["*"], ) + app.settings = settings + app.add_exception_handler(BaseException, handle_exception) app.include_router(like_api) diff --git a/db.py b/db.py index a7b3153..9838ac1 100644 --- a/db.py +++ b/db.py @@ -16,7 +16,7 @@ def __init__(self, settings: Settings): database=settings.db_name ) self.engine = create_engine( - url=url, echo=True + url=url ) self.session = sessionmaker( autocommit=False, diff --git a/depends.py b/depends.py index 0c1a7b6..c755728 100644 --- a/depends.py +++ b/depends.py @@ -1,13 +1,13 @@ from typing import Annotated -from fastapi import Depends +from fastapi import Depends, Request from db import Database from settings import Settings -async def get_settings() -> Settings: - return Settings() +async def get_settings(request: Request) -> Settings: + return request.app.settings async def get_database( diff --git a/main.py b/main.py index 0a23b5a..dc4a8db 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ from app import create_app +from settings import Settings -app = create_app() +app = create_app(Settings()) diff --git a/presentation/cart.py b/presentation/cart.py index c396d5e..d48338d 100644 --- a/presentation/cart.py +++ b/presentation/cart.py @@ -44,7 +44,7 @@ async def update_shoppingcart( cart: CartUpdateDto, session: Annotated[Session, Depends(get_session)] ) -> CartUpdateDto: - await update_shoppingcart(cart_id, cart, session) + await cart_service.update_shoppingcart(cart_id, cart, session) return cart diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..86df228 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +# pytest.ini +[pytest] +asyncio_default_fixture_loop_scope = session diff --git a/service/cart.py b/service/cart.py index 6bc1a06..48baa2c 100644 --- a/service/cart.py +++ b/service/cart.py @@ -1,4 +1,5 @@ from sqlalchemy.orm.session import Session + from dto.cart import CartDto, CartUpdateDto from exception import BadRequestException, NotFoundException from orm.cart import Cart diff --git a/service/like.py b/service/like.py index 41772fa..07a7cbe 100644 --- a/service/like.py +++ b/service/like.py @@ -1,9 +1,10 @@ +from sqlalchemy.orm.session import Session + from const import LikeStatus from dto.like import LikeDto from orm.like import Like -from sqlalchemy.orm.session import Session from repository.like import LikeRepository diff --git a/settings.py b/settings.py index 4a12474..45edea8 100644 --- a/settings.py +++ b/settings.py @@ -11,3 +11,8 @@ class Settings(BaseSettings): db_password: str db_host: str db_port: int + + +class TestSettings(Settings): + db_port: int = 9999 + db_name: str = "test" diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..5d4cc4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,126 @@ +import pytest +import pytest_asyncio +from sqlalchemy import URL, create_engine +from sqlalchemy.orm import sessionmaker + +from app import create_app +from const import LikeStatus +from orm.base import Base +from orm.cart import Cart +from orm.like import Like +from orm.product import Product +from orm.user import User +from settings import TestSettings +from tests import helper + + +@pytest_asyncio.fixture(scope="session") +async def settings(): + return TestSettings() + + +@pytest_asyncio.fixture(scope="session") +async def test_fastapi_app(settings): + return create_app(settings) + + +@pytest_asyncio.fixture(scope="function") +async def engine(settings): + url = URL.create( + drivername="mysql+pymysql", + username=settings.db_user, + password=settings.db_password, + host=settings.db_host, + port=settings.db_port, + database=settings.db_name + ) + eng = create_engine( + url=url + ) + Base.metadata.create_all(eng) + yield eng + Base.metadata.drop_all(eng) + + +@pytest_asyncio.fixture(scope="function") +async def session(engine): + sessionLocal = sessionmaker(engine) + return sessionLocal() + + +@pytest_asyncio.fixture(scope="function") +async def user(): + return [ + User( + id=1, + email=helper.TEST_USER_EMAIL_1, + password=helper.TEST_USER_PASSWORD_1 + ), + User( + id=2, + email=helper.TEST_USER_EMAIL_2, + password=helper.TEST_USER_PASSWORD_2 + ) + ] + + +@pytest_asyncio.fixture(scope="function") +async def product(): + return [ + Product( + id=1, + name=helper.TEST_PRODUCT_NAME_1, + image_path=helper.TEST_PRODUCT_IMAGE_PATH_1, + price=helper.TEST_PRODUCT_PRICE_1, + summary=helper.TEST_PRODUCT_SUMMARY_1 + ), + Product( + id=2, + name=helper.TEST_PRODUCT_NAME_2, + image_path=helper.TEST_PRODUCT_IMAGE_PATH_2, + price=helper.TEST_PRODUCT_PRICE_2, + summary=helper.TEST_PRODUCT_SUMMARY_2 + ) + ] + + +@pytest_asyncio.fixture(scope="function") +async def shopping_cart(): + return [ + Cart( + id=1, + user_id=1, + product_id=1, + count=5 + ) + ] + + +@pytest_asyncio.fixture(scope="function") +async def like(): + return [ + Like( + id=1, + is_like=LikeStatus.DISLIKE, + user_id=1, + product_id=1 + ) + ] + + +@pytest_asyncio.fixture(scope="function") +async def bootstrap( + session, + user, + product, + shopping_cart, + like, +): + session.bulk_save_objects(user) + session.bulk_save_objects(product) + session.bulk_save_objects(shopping_cart) + session.bulk_save_objects(like) + + session.commit() + + yield "bootstrap" diff --git a/tests/e2e/test_cart.py b/tests/e2e/test_cart.py new file mode 100644 index 0000000..f455e26 --- /dev/null +++ b/tests/e2e/test_cart.py @@ -0,0 +1,131 @@ +import pytest +from fastapi import status +from httpx import ASGITransport, AsyncClient + +from tests import helper + + +@pytest.mark.asyncio +async def test_list_shoppingcart_returns_200( + test_fastapi_app, bootstrap, shopping_cart +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.get("/cart/1") + + assert response.status_code == status.HTTP_200_OK + + result = response.json() + assert len(result["products"]) == len( + [i for i in shopping_cart if i.user_id == 1] + ) + + +@pytest.mark.asyncio +async def test_add_shoppingcart_returns_201( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.post( + "/cart", + headers={ + "Content-Type": "application/json" + }, + json={ + "user_id": 1, + "product_id": 2 + } + ) + + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.asyncio +async def test_update_shoppingcart_returns_202( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.put( + "/cart/1", + headers={ + "Content-Type": "application/json" + }, + json={ + "count": 123 + } + ) + + assert response.status_code == status.HTTP_202_ACCEPTED + + +@pytest.mark.asyncio +async def test_update_shoppingcart_returns_400_when_invalid_count( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.put( + "/cart/1", + headers={ + "Content-Type": "application/json" + }, + json={ + "count": 0 + } + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.asyncio +async def test_update_shoppingcart_returns_404( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.put( + "/cart/-1", + headers={ + "Content-Type": "application/json" + }, + json={ + "count": 123 + } + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +@pytest.mark.asyncio +async def test_delete_shoppingcart_returns_204( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.delete( + "/cart/1", + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + +@pytest.mark.asyncio +async def test_delete_shoppingcart_returns_404( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.delete( + "/cart/-1", + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/tests/e2e/test_like.py b/tests/e2e/test_like.py new file mode 100644 index 0000000..3936473 --- /dev/null +++ b/tests/e2e/test_like.py @@ -0,0 +1,43 @@ +import pytest +from fastapi import status +from httpx import ASGITransport, AsyncClient + + +@pytest.mark.asyncio +async def test_like_returns_201( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.post( + "/like", + headers={ + "Content-Type": "application/json" + }, + json={ + "user_id": 2, + "product_id": 1 + } + ) + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.asyncio +async def test_dislike_returns_201( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.post( + "/dislike", + headers={ + "Content-Type": "application/json" + }, + json={ + "user_id": 2, + "product_id": 1 + } + ) + assert response.status_code == status.HTTP_201_CREATED diff --git a/tests/e2e/test_ping.py b/tests/e2e/test_ping.py new file mode 100644 index 0000000..184ab1f --- /dev/null +++ b/tests/e2e/test_ping.py @@ -0,0 +1,17 @@ + + +import pytest +from fastapi import status +from httpx import ASGITransport, AsyncClient + + +@pytest.mark.asyncio +async def test_ping( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.get("/ping") + + assert response.status_code == status.HTTP_200_OK diff --git a/tests/e2e/test_product.py b/tests/e2e/test_product.py new file mode 100644 index 0000000..cac51e0 --- /dev/null +++ b/tests/e2e/test_product.py @@ -0,0 +1,62 @@ +import pytest +from fastapi import status +from httpx import ASGITransport, AsyncClient + +from tests import helper + + +@pytest.mark.asyncio +async def test_show_product_detail_returns_200( + test_fastapi_app, bootstrap +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.get("/product/0") + + assert response.status_code == status.HTTP_200_OK + + result = response.json() + + assert result["name"] == helper.TEST_PRODUCT_NAME_1 + assert result["price"] == helper.TEST_PRODUCT_PRICE_1 + assert result["summary"] == helper.TEST_PRODUCT_SUMMARY_1 + assert result["image_path"] == helper.TEST_PRODUCT_IMAGE_PATH_1 + + +@pytest.mark.asyncio +async def test_list_product_returns_200( + test_fastapi_app, bootstrap, product +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.get("/product") + + assert response.status_code == status.HTTP_200_OK + + result = response.json() + + assert len(result["products"]) == len(product) + + +@pytest.mark.asyncio +async def test_add_product_returns_201( + test_fastapi_app, bootstrap, product +): + async with AsyncClient( + transport=ASGITransport(app=test_fastapi_app), base_url="http://test" + ) as client: + response = await client.post( + "/product", + headers={ + "Content-Type": "application/json" + }, + json={ + "name": "product_name", + "image_path": "product_image_path", + "price": 50000, + } + ) + + assert response.status_code == status.HTTP_201_CREATED diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..5168844 --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,15 @@ +TEST_USER_EMAIL_1 = "dba28eae-74c6-4e1d-89a2-8ea9d3ca761d" +TEST_USER_PASSWORD_1 = "7ab117a5-ae3d-486c-ad2e-0b652bf5db3a" + +TEST_USER_EMAIL_2 = "08fc8412-d377-4ace-a717-fab0c48b9a1b" +TEST_USER_PASSWORD_2 = "67e4fdbc-7b99-4f1b-bbf9-2e6d46842b08" + +TEST_PRODUCT_NAME_1 = "6cac3385-32ff-4e9d-802b-422710f51d0a" +TEST_PRODUCT_IMAGE_PATH_1 = "7c7cc382-bf9f-49de-aa28-fa23e1c519e4" +TEST_PRODUCT_PRICE_1 = 10000 +TEST_PRODUCT_SUMMARY_1 = "5270c12b-63f0-4fca-a00d-aaf30b3e955b" + +TEST_PRODUCT_NAME_2 = "17681418-01f2-4934-97d7-3e4efdf75ae5" +TEST_PRODUCT_IMAGE_PATH_2 = "ee6d3c95-c0cd-400d-bb2b-3fc38b7e96b1" +TEST_PRODUCT_PRICE_2 = 50000 +TEST_PRODUCT_SUMMARY_2 = "73a1fb37-e862-4280-88b7-4925d684b7ba"