From 77fc1d73791ea136a2427a17bc4bc847c3719efa Mon Sep 17 00:00:00 2001 From: mikemando Date: Sat, 1 Mar 2025 00:06:39 +0100 Subject: [PATCH 1/7] feat: add feature to get top rated testimonials --- .../testimonial/test_top_rated_testimonial.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/v1/testimonial/test_top_rated_testimonial.py diff --git a/tests/v1/testimonial/test_top_rated_testimonial.py b/tests/v1/testimonial/test_top_rated_testimonial.py new file mode 100644 index 000000000..09b935d83 --- /dev/null +++ b/tests/v1/testimonial/test_top_rated_testimonial.py @@ -0,0 +1,39 @@ +import pytest +from fastapi.testclient import TestClient +from main import app +from unittest.mock import MagicMock +from api.db.database import get_db +from datetime import datetime + +client = TestClient(app) + +@pytest.fixture +def db_session_mock(): + """Mock database session""" + return MagicMock() + +@pytest.fixture(autouse=True) +def override_get_db(db_session_mock): + def get_db_override(): + yield db_session_mock + + app.dependency_overrides[get_db] = get_db_override + yield + app.dependency_overrides = {} + +def test_get_top_rated_api(db_session_mock): + """Test the /top-rated API endpoint""" + mock_testimonial = MagicMock() + mock_testimonial.id = "123" + mock_testimonial.content = "Excellent service!" + mock_testimonial.ratings = 5.0 + mock_testimonial.author_id = "abc" + mock_testimonial.created_at = datetime.now() + + db_session_mock.query().order_by().offset().limit().all.return_value = [mock_testimonial] + + response = client.get("/api/v1/testimonials/top-rated", params={"page": 1, "per_page": 1}) + + assert response.status_code == 200 + assert response.json()["message"] == "Top-rated testimonials retrieved successfully." + assert len(response.json()["data"]) == 1 \ No newline at end of file From c00e70ed8deaa7ecdfe591f1010ecc99904f923d Mon Sep 17 00:00:00 2001 From: mikemando Date: Sat, 1 Mar 2025 00:54:37 +0100 Subject: [PATCH 2/7] feat: implemented route and functionality to get top rated testimonials --- api/v1/routes/testimonial.py | 23 +++++++++++++++++++++++ api/v1/services/testimonial.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/api/v1/routes/testimonial.py b/api/v1/routes/testimonial.py index 22ff49033..269fd96b8 100644 --- a/api/v1/routes/testimonial.py +++ b/api/v1/routes/testimonial.py @@ -34,6 +34,29 @@ def get_testimonials( skip=max(page,0), ) +@testimonial.get("/top-rated", status_code=200) +def get_top_rated_testimonials( + page: int = Query(1, ge=1, description="Page number"), + per_page: int = Query(10, ge=1, description="Number of testimonials per page"), + db: Session = Depends(get_db), +): + """Endpoint to fetch top-rated testimonials""" + testimonials = testimonial_service.top_rated_testimonials(db, page, per_page) + + if not testimonials: + return success_response(status_code=200, message="No testimonials found.", data=[]) + + return success_response( + status_code=200, + message="Top-rated testimonials retrieved successfully.", + data=[{ + "id": testimonial.id, + "content": testimonial.content, + "ratings": testimonial.ratings, + "author_id": testimonial.author_id, + "created_at": testimonial.created_at.isoformat() + } for testimonial in testimonials] + ) @testimonial.get("/{testimonial_id}", status_code=status.HTTP_200_OK) def get_testimonial( diff --git a/api/v1/services/testimonial.py b/api/v1/services/testimonial.py index f3d20352a..2dc2e1b84 100644 --- a/api/v1/services/testimonial.py +++ b/api/v1/services/testimonial.py @@ -4,6 +4,8 @@ from api.v1.models.testimonial import Testimonial from api.v1.models.user import User from api.v1.schemas.testimonial import CreateTestimonial +from fastapi import HTTPException, status +from sqlalchemy import desc class TestimonialService(Service): @@ -54,6 +56,25 @@ def delete_all(self, db: Session): except Exception as e: db.rollback() raise e + + def top_rated_testimonials(self, db: Session, page: int = 1, per_page: int = 10): + """ + Fetch testimonials with the highest ratings and paginates the results. + """ + if page < 1 or per_page < 1: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid pagination parameters") + + offset = (page - 1) * per_page + + testimonials = ( + db.query(Testimonial) + .order_by(desc(Testimonial.ratings)) + .offset(offset) + .limit(per_page) + .all() + ) + + return testimonials testimonial_service = TestimonialService() From 08422437a019fccfe5d911ab261a18d929779b8c Mon Sep 17 00:00:00 2001 From: mikemando Date: Sun, 2 Mar 2025 16:08:47 +0100 Subject: [PATCH 3/7] test: add unit test for zero testimonials and invalid pagination --- api/v1/routes/testimonial.py | 32 +++++++++++-------- api/v1/services/testimonial.py | 29 +++++++++-------- .../testimonial/test_top_rated_testimonial.py | 19 ++++++++++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/api/v1/routes/testimonial.py b/api/v1/routes/testimonial.py index 269fd96b8..56d7041f7 100644 --- a/api/v1/routes/testimonial.py +++ b/api/v1/routes/testimonial.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from api.v1.models.user import User from fastapi import Depends, APIRouter, status,Query -from api.utils.success_response import success_response +from api.utils.success_response import success_response, fail_response from api.v1.services.testimonial import testimonial_service from api.v1.services.user import user_service from api.v1.schemas.testimonial import CreateTestimonial @@ -41,22 +41,26 @@ def get_top_rated_testimonials( db: Session = Depends(get_db), ): """Endpoint to fetch top-rated testimonials""" - testimonials = testimonial_service.top_rated_testimonials(db, page, per_page) + try: - if not testimonials: - return success_response(status_code=200, message="No testimonials found.", data=[]) + testimonials = testimonial_service.top_rated_testimonials(db, page, per_page) + + if not testimonials: + return success_response(status_code=200, message="No testimonials found.", data=[]) - return success_response( - status_code=200, - message="Top-rated testimonials retrieved successfully.", - data=[{ - "id": testimonial.id, - "content": testimonial.content, - "ratings": testimonial.ratings, - "author_id": testimonial.author_id, - "created_at": testimonial.created_at.isoformat() - } for testimonial in testimonials] + return success_response( + status_code=200, + message="Top-rated testimonials retrieved successfully.", + data=[{ + "id": testimonial.id, + "content": testimonial.content, + "ratings": testimonial.ratings, + "author_id": testimonial.author_id, + "created_at": testimonial.created_at.isoformat() + } for testimonial in testimonials] ) + except Exception as e: + return fail_response(status_code=500, message="An error occurred.", data={"error": str(e)}) @testimonial.get("/{testimonial_id}", status_code=status.HTTP_200_OK) def get_testimonial( diff --git a/api/v1/services/testimonial.py b/api/v1/services/testimonial.py index 2dc2e1b84..2b317e4f9 100644 --- a/api/v1/services/testimonial.py +++ b/api/v1/services/testimonial.py @@ -1,11 +1,13 @@ from sqlalchemy.orm import Session from api.core.base.services import Service from api.utils.db_validators import check_model_existence +from api.utils.success_response import fail_response from api.v1.models.testimonial import Testimonial from api.v1.models.user import User from api.v1.schemas.testimonial import CreateTestimonial from fastapi import HTTPException, status from sqlalchemy import desc +from sqlalchemy.exc import SQLAlchemyError class TestimonialService(Service): @@ -61,20 +63,19 @@ def top_rated_testimonials(self, db: Session, page: int = 1, per_page: int = 10) """ Fetch testimonials with the highest ratings and paginates the results. """ - if page < 1 or per_page < 1: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid pagination parameters") - - offset = (page - 1) * per_page + try: + offset = (page - 1) * per_page - testimonials = ( - db.query(Testimonial) - .order_by(desc(Testimonial.ratings)) - .offset(offset) - .limit(per_page) - .all() - ) - - return testimonials - + testimonials = ( + db.query(Testimonial) + .order_by(desc(Testimonial.ratings)) + .offset(offset) + .limit(per_page) + .all() + ) + return testimonials + + except SQLAlchemyError as e: + return fail_response(status_code=500, message="An error occurred while fetching top-rated testimonials.") testimonial_service = TestimonialService() diff --git a/tests/v1/testimonial/test_top_rated_testimonial.py b/tests/v1/testimonial/test_top_rated_testimonial.py index 09b935d83..270c43c8e 100644 --- a/tests/v1/testimonial/test_top_rated_testimonial.py +++ b/tests/v1/testimonial/test_top_rated_testimonial.py @@ -36,4 +36,21 @@ def test_get_top_rated_api(db_session_mock): assert response.status_code == 200 assert response.json()["message"] == "Top-rated testimonials retrieved successfully." - assert len(response.json()["data"]) == 1 \ No newline at end of file + assert len(response.json()["data"]) == 1 + +def test_no_testimonials(db_session_mock): + """Test for when no testimonials exist in the database""" + db_session_mock.query().order_by().offset().limit().all.return_value = [] + + response = client.get("/api/v1/testimonials/top-rated", params={"page": 1, "per_page": 10}) + + assert response.status_code == 200 + assert response.json()["message"] == "No testimonials found." + assert response.json()["data"] == [] + +@pytest.mark.parametrize("page, per_page", [(0, 10), (-1, 10), (1, 0), (1, -5)]) +def test_invalid_pagination_params(page, per_page): + """Test for invalid pagination""" + response = client.get("/api/v1/testimonials/top-rated", params={"page": page, "per_page": per_page}) + + assert response.status_code == 422 \ No newline at end of file From b092c12915d4e2da315480f06192bd3fa12e4dce Mon Sep 17 00:00:00 2001 From: mikemando Date: Mon, 3 Mar 2025 03:57:48 +0100 Subject: [PATCH 4/7] chore: discarded helper function to get top rated testimonials --- api/v1/services/testimonial.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/api/v1/services/testimonial.py b/api/v1/services/testimonial.py index 2b317e4f9..6e38e5965 100644 --- a/api/v1/services/testimonial.py +++ b/api/v1/services/testimonial.py @@ -59,23 +59,5 @@ def delete_all(self, db: Session): db.rollback() raise e - def top_rated_testimonials(self, db: Session, page: int = 1, per_page: int = 10): - """ - Fetch testimonials with the highest ratings and paginates the results. - """ - try: - offset = (page - 1) * per_page - - testimonials = ( - db.query(Testimonial) - .order_by(desc(Testimonial.ratings)) - .offset(offset) - .limit(per_page) - .all() - ) - return testimonials - - except SQLAlchemyError as e: - return fail_response(status_code=500, message="An error occurred while fetching top-rated testimonials.") testimonial_service = TestimonialService() From 77f4a05fdc195ab798e37ae927ad9f514d99f6c0 Mon Sep 17 00:00:00 2001 From: mikemando Date: Mon, 3 Mar 2025 04:01:17 +0100 Subject: [PATCH 5/7] fix: resolved to using paginated_response method to handle endpoint request --- api/v1/routes/testimonial.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/api/v1/routes/testimonial.py b/api/v1/routes/testimonial.py index 56d7041f7..7661ba96d 100644 --- a/api/v1/routes/testimonial.py +++ b/api/v1/routes/testimonial.py @@ -5,6 +5,7 @@ from fastapi.encoders import jsonable_encoder from api.db.database import get_db from sqlalchemy.orm import Session +from sqlalchemy import desc from api.v1.models.user import User from fastapi import Depends, APIRouter, status,Query from api.utils.success_response import success_response, fail_response @@ -42,23 +43,15 @@ def get_top_rated_testimonials( ): """Endpoint to fetch top-rated testimonials""" try: + return paginated_response( + db=db, + model=Testimonial, + skip=(page - 1) * per_page, + limit=per_page, + filters={}, + order_by=desc(Testimonial.ratings) + ) - testimonials = testimonial_service.top_rated_testimonials(db, page, per_page) - - if not testimonials: - return success_response(status_code=200, message="No testimonials found.", data=[]) - - return success_response( - status_code=200, - message="Top-rated testimonials retrieved successfully.", - data=[{ - "id": testimonial.id, - "content": testimonial.content, - "ratings": testimonial.ratings, - "author_id": testimonial.author_id, - "created_at": testimonial.created_at.isoformat() - } for testimonial in testimonials] - ) except Exception as e: return fail_response(status_code=500, message="An error occurred.", data={"error": str(e)}) From 6090d7bf0d590caea3fbb80db4842c399b8adf8b Mon Sep 17 00:00:00 2001 From: mikemando Date: Mon, 3 Mar 2025 04:03:45 +0100 Subject: [PATCH 6/7] chore: allow paginated_response accept optional order_by parameter --- api/utils/pagination.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/utils/pagination.py b/api/utils/pagination.py index f10b4bf3f..84a4a2f54 100644 --- a/api/utils/pagination.py +++ b/api/utils/pagination.py @@ -12,7 +12,8 @@ def paginated_response( skip: int, limit: int, join: Optional[Any] = None, - filters: Optional[Dict[str, Any]]=None + filters: Optional[Dict[str, Any]]=None, + order_by: Optional[Any] = None ): ''' @@ -79,6 +80,9 @@ def paginated_response( query = query.filter( getattr(getattr(join, "columns"), attr).like(f"%{value}%")) + + if order_by is not None: + query = query.order_by(order_by) total = query.count() results = jsonable_encoder(query.offset(skip).limit(limit).all()) From 9650a0471111d1449103be652cb9acbbc28cd42f Mon Sep 17 00:00:00 2001 From: mikemando Date: Mon, 3 Mar 2025 04:05:31 +0100 Subject: [PATCH 7/7] test: unit test for /top-rated endpoint --- .../testimonial/test_top_rated_testimonial.py | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/tests/v1/testimonial/test_top_rated_testimonial.py b/tests/v1/testimonial/test_top_rated_testimonial.py index 270c43c8e..993200316 100644 --- a/tests/v1/testimonial/test_top_rated_testimonial.py +++ b/tests/v1/testimonial/test_top_rated_testimonial.py @@ -1,56 +1,83 @@ import pytest from fastapi.testclient import TestClient from main import app -from unittest.mock import MagicMock from api.db.database import get_db -from datetime import datetime +from unittest.mock import MagicMock client = TestClient(app) +mock_data = [ + { + "client_name": "testclientname1", + "author_id": "066a16d8-cab5-7dd3-8000-3a167556aa11", + "content": "Amazing service!", + "id": "066a6e8b-f008-7242-8000-8f090997001a", + "updated_at": "2024-07-29T02:00:00.002967+01:00", + "client_designation": "testclient", + "comments": "Highly recommended!", + "ratings": 4.8, + "created_at": "2024-07-29T01:59:00.002967+01:00" + }, + { + "client_name": "testclientname2", + "author_id": "066a16d8-cab5-7dd3-8000-3a167556ee55", + "content": "Loved the service!", + "id": "066a6e8b-f008-7242-8000-8f090997005e", + "updated_at": "2024-07-29T02:20:10.002967+01:00", + "client_designation": "testclient", + "comments": "Best experience ever!", + "ratings": 5.0, + "created_at": "2024-07-29T02:18:00.002967+01:00" + }, + { + "client_name": "testclientname3", + "author_id": "066a16d8-cab5-7dd3-8000-3a167556ff66", + "content": "Decent service", + "id": "066a6e8b-f008-7242-8000-8f090997006f", + "updated_at": "2024-07-29T02:25:40.002967+01:00", + "client_designation": "testclient", + "comments": "Could be improved", + "ratings": 4.2, + "created_at": "2024-07-29T02:23:00.002967+01:00" + }, +] + +"""Mocking The database""" @pytest.fixture def db_session_mock(): - """Mock database session""" - return MagicMock() + db_session = MagicMock() + yield db_session @pytest.fixture(autouse=True) def override_get_db(db_session_mock): def get_db_override(): yield db_session_mock - + app.dependency_overrides[get_db] = get_db_override yield app.dependency_overrides = {} -def test_get_top_rated_api(db_session_mock): - """Test the /top-rated API endpoint""" - mock_testimonial = MagicMock() - mock_testimonial.id = "123" - mock_testimonial.content = "Excellent service!" - mock_testimonial.ratings = 5.0 - mock_testimonial.author_id = "abc" - mock_testimonial.created_at = datetime.now() - - db_session_mock.query().order_by().offset().limit().all.return_value = [mock_testimonial] - - response = client.get("/api/v1/testimonials/top-rated", params={"page": 1, "per_page": 1}) - - assert response.status_code == 200 - assert response.json()["message"] == "Top-rated testimonials retrieved successfully." - assert len(response.json()["data"]) == 1 +def test_get_top_rated_testimonials(db_session_mock): + + mock_query = MagicMock() + mock_query.count.return_value = len(mock_data) + mock_query.offset.return_value.limit.return_value.all.return_value = mock_data -def test_no_testimonials(db_session_mock): - """Test for when no testimonials exist in the database""" - db_session_mock.query().order_by().offset().limit().all.return_value = [] + db_session_mock.query.return_value = mock_query + mock_query.order_by.return_value = mock_query - response = client.get("/api/v1/testimonials/top-rated", params={"page": 1, "per_page": 10}) + url = 'api/v1/testimonials/top-rated' + response = client.get(url, params={'page': 1, 'per_page': 2}) assert response.status_code == 200 - assert response.json()["message"] == "No testimonials found." - assert response.json()["data"] == [] + assert response.json()['message'] == 'Successfully fetched items' + + returned_items = response.json()['data']['items'] + assert len(returned_items) == 3 -@pytest.mark.parametrize("page, per_page", [(0, 10), (-1, 10), (1, 0), (1, -5)]) -def test_invalid_pagination_params(page, per_page): - """Test for invalid pagination""" - response = client.get("/api/v1/testimonials/top-rated", params={"page": page, "per_page": per_page}) + assert response.json()['data']['total'] == 3 + assert response.json()['data']['limit'] == 2 + assert response.json()['data']['skip'] == 0 - assert response.status_code == 422 \ No newline at end of file + db_session_mock.query.assert_called() + mock_query.order_by.assert_called() \ No newline at end of file