From 442dcd0c4969511f519fada6083a9f6d6aa4b687 Mon Sep 17 00:00:00 2001 From: SCCSMARTCODE Date: Wed, 24 Jul 2024 23:35:59 +0100 Subject: [PATCH 001/566] [FEAT] Update Customer Details API #89 --- .idea/.gitignore | 3 + .idea/hng_boilerplate_python_fastapi_web.iml | 14 + .idea/inspectionProfiles/Project_Default.xml | 736 ++++++++++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + api/v1/routes/__init__.py | 2 + api/v1/routes/customers.py | 52 ++ api/v1/schemas/customer.py | 16 + tests/v1/test_update_customer.py | 84 ++ 11 files changed, 934 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/hng_boilerplate_python_fastapi_web.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 api/v1/routes/customers.py create mode 100644 api/v1/schemas/customer.py create mode 100644 tests/v1/test_update_customer.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/hng_boilerplate_python_fastapi_web.iml b/.idea/hng_boilerplate_python_fastapi_web.iml new file mode 100644 index 000000000..8e5446ac9 --- /dev/null +++ b/.idea/hng_boilerplate_python_fastapi_web.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..930b14e81 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,736 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..105ce2da2 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..3f0fd9bf8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..6c4a82298 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/v1/routes/__init__.py b/api/v1/routes/__init__.py index 8d196f53d..a1c093c5e 100644 --- a/api/v1/routes/__init__.py +++ b/api/v1/routes/__init__.py @@ -3,6 +3,7 @@ from api.v1.routes.newsletter import newsletter from api.v1.routes.user import user from api.v1.routes.blog import blog +from api.v1.routes.customers import customers api_version_one = APIRouter(prefix="/api/v1") @@ -10,3 +11,4 @@ api_version_one.include_router(newsletter) api_version_one.include_router(user) api_version_one.include_router(blog) +api_version_one.include_router(customers) diff --git a/api/v1/routes/customers.py b/api/v1/routes/customers.py new file mode 100644 index 000000000..77d6f0096 --- /dev/null +++ b/api/v1/routes/customers.py @@ -0,0 +1,52 @@ +from fastapi import Depends, status, APIRouter, HTTPException +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session +from api.db.database import get_db +from api.v1.models.user import User +from api.v1.schemas.customer import CustomerUpdate, SuccessResponse +from api.utils.dependencies import get_super_admin +from typing import Annotated + +customers = APIRouter(prefix="/api/v1/customers", tags=["customers"]) + + +@customers.put("/{customer_id}", response_model=SuccessResponse, status_code=status.HTTP_200_OK) +def update_customer( + customer_id: str, + customer_data: CustomerUpdate, + current_user: Annotated[User, Depends(get_super_admin)], + db: Session = Depends(get_db) +): + """ + Updates customer details for a given customer ID. + - customer_id: ID of the customer to update. + - customer_data: New data for the customer. + """ + + # Fetch the customer from the database + customer = db.query(User).filter(User.id == customer_id).first() + + if not customer: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid customer ID.") + + # Update customer details + customer.first_name = customer_data.first_name + customer.last_name = customer_data.last_name + customer.email = customer_data.email + customer.phone_number = customer_data.phone + customer.address = customer_data.address + + db.commit() + + return SuccessResponse( + status_code=200, + message="Customer details updated successfully.", + data={ + "id": customer.id, + "first_name": customer.first_name, + "last_name": customer.last_name, + "email": customer.email, + "phone_number": customer.phone_number, + "address": customer.address + } + ) diff --git a/api/v1/schemas/customer.py b/api/v1/schemas/customer.py new file mode 100644 index 000000000..28fa1d157 --- /dev/null +++ b/api/v1/schemas/customer.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional + + +class CustomerUpdate(BaseModel): + first_name: Optional[str] = None + last_name: Optional[str] = None + email: Optional[EmailStr] = None + phone: Optional[str] = None + address: Optional[str] = None + + +class SuccessResponse(BaseModel): + status_code: int + message: str + data: dict diff --git a/tests/v1/test_update_customer.py b/tests/v1/test_update_customer.py new file mode 100644 index 000000000..ef87e34cf --- /dev/null +++ b/tests/v1/test_update_customer.py @@ -0,0 +1,84 @@ +import pytest +from fastapi.testclient import TestClient +from main import app +from api.v1.services.user import user_service +from api.v1.models.user import User +from sqlalchemy.orm import Session +from api.db.database import get_db + +client = TestClient(app) + + +# Mock the database session dependency +@pytest.fixture +def mock_db_session(mocker): + db_session_mock = mocker.MagicMock(spec=Session) + app.dependency_overrides[get_db] = lambda: db_session_mock + return db_session_mock + + +# Test fixtures for users and access tokens +@pytest.fixture +def test_admin(): + return User( + username="admin", + email="admin@example.com", + is_super_admin=True, + ) + + +@pytest.fixture +def test_customer(): + return User( + username="customer", + email="customer@example.com", + first_name="John", + last_name="Doe", + phone="1234567890", + address="123 Main St", + ) + + +@pytest.fixture +def access_token_admin(test_admin): + return user_service.create_access_token(data={"username": test_admin.username}) + + +# Test successful customer update +def test_update_customer_success(mock_db_session, test_customer, access_token_admin): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + headers = {'Authorization': f'Bearer {access_token_admin}'} + update_data = { + "first_name": "Jane", + "last_name": "Smith", + "phone": "0987654321", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) + assert response.status_code == 200 + assert response.json()['customer']['first_name'] == "Jane" + assert response.json()['customer']['last_name'] == "Smith" + assert response.json()['customer']['phone'] == "0987654321" + + +# Test missing fields in update +def test_update_customer_partial_success(mock_db_session, test_customer, access_token_admin): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + headers = {'Authorization': f'Bearer {access_token_admin}'} + update_data = { + "phone": "0987654321", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) + assert response.status_code == 200 + assert response.json()['customer']['phone'] == "0987654321" + assert response.json()['customer']['first_name'] == test_customer.first_name # Check that other fields remain unchanged + + +# Test unauthorized access +def test_update_customer_unauthorized(mock_db_session, test_customer): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + update_data = { + "first_name": "Jane", + "last_name": "Smith", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data) + assert response.status_code == 401 From 43f30681ec6a126d786468f21652d79b3ffb01e4 Mon Sep 17 00:00:00 2001 From: SCCSMARTCODE Date: Thu, 25 Jul 2024 11:43:40 +0100 Subject: [PATCH 002/566] Fixed issues with customer update functionality and updated tests --- api/v1/models/profile.py | 1 - api/v1/routes/customers.py | 28 +++++++++++++++++---------- tests/v1/test_update_customer.py | 33 ++++++++++++++++++++------------ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/api/v1/models/profile.py b/api/v1/models/profile.py index e6cef9ac1..852cf0e63 100644 --- a/api/v1/models/profile.py +++ b/api/v1/models/profile.py @@ -11,7 +11,6 @@ from api.v1.models.base_model import BaseTableModel - class Profile(BaseTableModel): __tablename__ = 'profiles' diff --git a/api/v1/routes/customers.py b/api/v1/routes/customers.py index 77d6f0096..7aa70446c 100644 --- a/api/v1/routes/customers.py +++ b/api/v1/routes/customers.py @@ -1,8 +1,8 @@ from fastapi import Depends, status, APIRouter, HTTPException -from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from api.db.database import get_db from api.v1.models.user import User +from api.v1.models.profile import Profile from api.v1.schemas.customer import CustomerUpdate, SuccessResponse from api.utils.dependencies import get_super_admin from typing import Annotated @@ -22,19 +22,27 @@ def update_customer( - customer_id: ID of the customer to update. - customer_data: New data for the customer. """ - - # Fetch the customer from the database + # Fetch the customer and profile from the database customer = db.query(User).filter(User.id == customer_id).first() + customer_profile = db.query(Profile).filter(Profile.user_id == customer_id).first() if not customer: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid customer ID.") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.") + + if not customer_profile: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer profile not found.") # Update customer details - customer.first_name = customer_data.first_name - customer.last_name = customer_data.last_name - customer.email = customer_data.email - customer.phone_number = customer_data.phone - customer.address = customer_data.address + if customer_data.first_name: + customer.first_name = customer_data.first_name + if customer_data.last_name: + customer.last_name = customer_data.last_name + if customer_data.email: + customer.email = customer_data.email + if customer_data.phone: + customer_profile.phone_number = customer_data.phone + if customer_data.address: + customer.address = customer_data.address db.commit() @@ -46,7 +54,7 @@ def update_customer( "first_name": customer.first_name, "last_name": customer.last_name, "email": customer.email, - "phone_number": customer.phone_number, + "phone_number": customer_profile.phone_number, "address": customer.address } ) diff --git a/tests/v1/test_update_customer.py b/tests/v1/test_update_customer.py index ef87e34cf..9d3ecc6af 100644 --- a/tests/v1/test_update_customer.py +++ b/tests/v1/test_update_customer.py @@ -3,6 +3,7 @@ from main import app from api.v1.services.user import user_service from api.v1.models.user import User +from api.v1.models.profile import Profile # Import the Profile model from sqlalchemy.orm import Session from api.db.database import get_db @@ -21,6 +22,7 @@ def mock_db_session(mocker): @pytest.fixture def test_admin(): return User( + id="admin_id", # Ensure the admin has an ID username="admin", email="admin@example.com", is_super_admin=True, @@ -29,19 +31,26 @@ def test_admin(): @pytest.fixture def test_customer(): - return User( + user = User( + id="customer_id", # Ensure the customer has an ID username="customer", email="customer@example.com", first_name="John", last_name="Doe", - phone="1234567890", - address="123 Main St", ) + profile = Profile( + user_id=user.id, # Link profile to the user + phone_number="1234567890", + bio="Customer biography", + avatar_url="http://example.com/avatar.jpg" + ) + user.profile = profile + return user @pytest.fixture def access_token_admin(test_admin): - return user_service.create_access_token(data={"username": test_admin.username}) + return user_service.create_access_token({"sub": test_admin.username}) # Test successful customer update @@ -51,13 +60,13 @@ def test_update_customer_success(mock_db_session, test_customer, access_token_ad update_data = { "first_name": "Jane", "last_name": "Smith", - "phone": "0987654321", + "phone_number": "0987654321", # Updated to match the `Profile` schema } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) assert response.status_code == 200 - assert response.json()['customer']['first_name'] == "Jane" - assert response.json()['customer']['last_name'] == "Smith" - assert response.json()['customer']['phone'] == "0987654321" + assert response.json()['data']['first_name'] == "Jane" + assert response.json()['data']['last_name'] == "Smith" + assert response.json()['data']['phone_number'] == "0987654321" # Test missing fields in update @@ -65,12 +74,12 @@ def test_update_customer_partial_success(mock_db_session, test_customer, access_ mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer headers = {'Authorization': f'Bearer {access_token_admin}'} update_data = { - "phone": "0987654321", + "phone_number": "0987654321", } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) assert response.status_code == 200 - assert response.json()['customer']['phone'] == "0987654321" - assert response.json()['customer']['first_name'] == test_customer.first_name # Check that other fields remain unchanged + assert response.json()['data']['phone_number'] == "0987654321" + assert response.json()['data']['first_name'] == test_customer.first_name # Check that other fields remain unchanged # Test unauthorized access @@ -81,4 +90,4 @@ def test_update_customer_unauthorized(mock_db_session, test_customer): "last_name": "Smith", } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data) - assert response.status_code == 401 + assert response.status_code == 401 # Expecting 401 Unauthorized From 4e7d90fa111b0ae3abfb43326b959dff3cfa3fd3 Mon Sep 17 00:00:00 2001 From: SundayMba Date: Thu, 25 Jul 2024 11:49:36 +0100 Subject: [PATCH 003/566] fix: changed orm_mode to from_attribues --- api/v1/schemas/blog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/schemas/blog.py b/api/v1/schemas/blog.py index 6bfd8754c..4962887c5 100644 --- a/api/v1/schemas/blog.py +++ b/api/v1/schemas/blog.py @@ -25,4 +25,4 @@ class BlogResponse(BaseModel): updated_at: datetime class Config: - orm_mode = True + from_attributes = True From 85c9207067166c1da860e59c15d2fd34815c7299 Mon Sep 17 00:00:00 2001 From: SCCSMARTCODE Date: Wed, 24 Jul 2024 23:35:59 +0100 Subject: [PATCH 004/566] [FEAT] Update Customer Details API #89 --- .idea/.gitignore | 3 + .idea/hng_boilerplate_python_fastapi_web.iml | 14 + .idea/inspectionProfiles/Project_Default.xml | 736 ++++++++++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + api/v1/routes/__init__.py | 2 + api/v1/routes/customers.py | 52 ++ api/v1/schemas/customer.py | 16 + tests/v1/test_update_customer.py | 84 ++ 11 files changed, 934 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/hng_boilerplate_python_fastapi_web.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 api/v1/routes/customers.py create mode 100644 api/v1/schemas/customer.py create mode 100644 tests/v1/test_update_customer.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/hng_boilerplate_python_fastapi_web.iml b/.idea/hng_boilerplate_python_fastapi_web.iml new file mode 100644 index 000000000..8e5446ac9 --- /dev/null +++ b/.idea/hng_boilerplate_python_fastapi_web.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..930b14e81 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,736 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..105ce2da2 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..3f0fd9bf8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..6c4a82298 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/v1/routes/__init__.py b/api/v1/routes/__init__.py index 10f8c976c..a060e3e3f 100644 --- a/api/v1/routes/__init__.py +++ b/api/v1/routes/__init__.py @@ -5,6 +5,7 @@ from api.v1.routes.notification import notification from api.v1.routes.testimonial import testimonial from api.v1.routes.blog import blog +from api.v1.routes.customers import customers api_version_one = APIRouter(prefix="/api/v1") @@ -15,3 +16,4 @@ api_version_one.include_router(notification) api_version_one.include_router(testimonial) api_version_one.include_router(blog) +api_version_one.include_router(customers) diff --git a/api/v1/routes/customers.py b/api/v1/routes/customers.py new file mode 100644 index 000000000..77d6f0096 --- /dev/null +++ b/api/v1/routes/customers.py @@ -0,0 +1,52 @@ +from fastapi import Depends, status, APIRouter, HTTPException +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session +from api.db.database import get_db +from api.v1.models.user import User +from api.v1.schemas.customer import CustomerUpdate, SuccessResponse +from api.utils.dependencies import get_super_admin +from typing import Annotated + +customers = APIRouter(prefix="/api/v1/customers", tags=["customers"]) + + +@customers.put("/{customer_id}", response_model=SuccessResponse, status_code=status.HTTP_200_OK) +def update_customer( + customer_id: str, + customer_data: CustomerUpdate, + current_user: Annotated[User, Depends(get_super_admin)], + db: Session = Depends(get_db) +): + """ + Updates customer details for a given customer ID. + - customer_id: ID of the customer to update. + - customer_data: New data for the customer. + """ + + # Fetch the customer from the database + customer = db.query(User).filter(User.id == customer_id).first() + + if not customer: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid customer ID.") + + # Update customer details + customer.first_name = customer_data.first_name + customer.last_name = customer_data.last_name + customer.email = customer_data.email + customer.phone_number = customer_data.phone + customer.address = customer_data.address + + db.commit() + + return SuccessResponse( + status_code=200, + message="Customer details updated successfully.", + data={ + "id": customer.id, + "first_name": customer.first_name, + "last_name": customer.last_name, + "email": customer.email, + "phone_number": customer.phone_number, + "address": customer.address + } + ) diff --git a/api/v1/schemas/customer.py b/api/v1/schemas/customer.py new file mode 100644 index 000000000..28fa1d157 --- /dev/null +++ b/api/v1/schemas/customer.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional + + +class CustomerUpdate(BaseModel): + first_name: Optional[str] = None + last_name: Optional[str] = None + email: Optional[EmailStr] = None + phone: Optional[str] = None + address: Optional[str] = None + + +class SuccessResponse(BaseModel): + status_code: int + message: str + data: dict diff --git a/tests/v1/test_update_customer.py b/tests/v1/test_update_customer.py new file mode 100644 index 000000000..ef87e34cf --- /dev/null +++ b/tests/v1/test_update_customer.py @@ -0,0 +1,84 @@ +import pytest +from fastapi.testclient import TestClient +from main import app +from api.v1.services.user import user_service +from api.v1.models.user import User +from sqlalchemy.orm import Session +from api.db.database import get_db + +client = TestClient(app) + + +# Mock the database session dependency +@pytest.fixture +def mock_db_session(mocker): + db_session_mock = mocker.MagicMock(spec=Session) + app.dependency_overrides[get_db] = lambda: db_session_mock + return db_session_mock + + +# Test fixtures for users and access tokens +@pytest.fixture +def test_admin(): + return User( + username="admin", + email="admin@example.com", + is_super_admin=True, + ) + + +@pytest.fixture +def test_customer(): + return User( + username="customer", + email="customer@example.com", + first_name="John", + last_name="Doe", + phone="1234567890", + address="123 Main St", + ) + + +@pytest.fixture +def access_token_admin(test_admin): + return user_service.create_access_token(data={"username": test_admin.username}) + + +# Test successful customer update +def test_update_customer_success(mock_db_session, test_customer, access_token_admin): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + headers = {'Authorization': f'Bearer {access_token_admin}'} + update_data = { + "first_name": "Jane", + "last_name": "Smith", + "phone": "0987654321", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) + assert response.status_code == 200 + assert response.json()['customer']['first_name'] == "Jane" + assert response.json()['customer']['last_name'] == "Smith" + assert response.json()['customer']['phone'] == "0987654321" + + +# Test missing fields in update +def test_update_customer_partial_success(mock_db_session, test_customer, access_token_admin): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + headers = {'Authorization': f'Bearer {access_token_admin}'} + update_data = { + "phone": "0987654321", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) + assert response.status_code == 200 + assert response.json()['customer']['phone'] == "0987654321" + assert response.json()['customer']['first_name'] == test_customer.first_name # Check that other fields remain unchanged + + +# Test unauthorized access +def test_update_customer_unauthorized(mock_db_session, test_customer): + mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer + update_data = { + "first_name": "Jane", + "last_name": "Smith", + } + response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data) + assert response.status_code == 401 From 878359004514784d771eb9fa32d83372242801f7 Mon Sep 17 00:00:00 2001 From: SCCSMARTCODE Date: Thu, 25 Jul 2024 11:43:40 +0100 Subject: [PATCH 005/566] Fixed issues with customer update functionality and updated tests --- api/v1/models/profile.py | 1 - api/v1/routes/customers.py | 28 +++++++++++++++++---------- tests/v1/test_update_customer.py | 33 ++++++++++++++++++++------------ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/api/v1/models/profile.py b/api/v1/models/profile.py index e6cef9ac1..852cf0e63 100644 --- a/api/v1/models/profile.py +++ b/api/v1/models/profile.py @@ -11,7 +11,6 @@ from api.v1.models.base_model import BaseTableModel - class Profile(BaseTableModel): __tablename__ = 'profiles' diff --git a/api/v1/routes/customers.py b/api/v1/routes/customers.py index 77d6f0096..7aa70446c 100644 --- a/api/v1/routes/customers.py +++ b/api/v1/routes/customers.py @@ -1,8 +1,8 @@ from fastapi import Depends, status, APIRouter, HTTPException -from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from api.db.database import get_db from api.v1.models.user import User +from api.v1.models.profile import Profile from api.v1.schemas.customer import CustomerUpdate, SuccessResponse from api.utils.dependencies import get_super_admin from typing import Annotated @@ -22,19 +22,27 @@ def update_customer( - customer_id: ID of the customer to update. - customer_data: New data for the customer. """ - - # Fetch the customer from the database + # Fetch the customer and profile from the database customer = db.query(User).filter(User.id == customer_id).first() + customer_profile = db.query(Profile).filter(Profile.user_id == customer_id).first() if not customer: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid customer ID.") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.") + + if not customer_profile: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer profile not found.") # Update customer details - customer.first_name = customer_data.first_name - customer.last_name = customer_data.last_name - customer.email = customer_data.email - customer.phone_number = customer_data.phone - customer.address = customer_data.address + if customer_data.first_name: + customer.first_name = customer_data.first_name + if customer_data.last_name: + customer.last_name = customer_data.last_name + if customer_data.email: + customer.email = customer_data.email + if customer_data.phone: + customer_profile.phone_number = customer_data.phone + if customer_data.address: + customer.address = customer_data.address db.commit() @@ -46,7 +54,7 @@ def update_customer( "first_name": customer.first_name, "last_name": customer.last_name, "email": customer.email, - "phone_number": customer.phone_number, + "phone_number": customer_profile.phone_number, "address": customer.address } ) diff --git a/tests/v1/test_update_customer.py b/tests/v1/test_update_customer.py index ef87e34cf..9d3ecc6af 100644 --- a/tests/v1/test_update_customer.py +++ b/tests/v1/test_update_customer.py @@ -3,6 +3,7 @@ from main import app from api.v1.services.user import user_service from api.v1.models.user import User +from api.v1.models.profile import Profile # Import the Profile model from sqlalchemy.orm import Session from api.db.database import get_db @@ -21,6 +22,7 @@ def mock_db_session(mocker): @pytest.fixture def test_admin(): return User( + id="admin_id", # Ensure the admin has an ID username="admin", email="admin@example.com", is_super_admin=True, @@ -29,19 +31,26 @@ def test_admin(): @pytest.fixture def test_customer(): - return User( + user = User( + id="customer_id", # Ensure the customer has an ID username="customer", email="customer@example.com", first_name="John", last_name="Doe", - phone="1234567890", - address="123 Main St", ) + profile = Profile( + user_id=user.id, # Link profile to the user + phone_number="1234567890", + bio="Customer biography", + avatar_url="http://example.com/avatar.jpg" + ) + user.profile = profile + return user @pytest.fixture def access_token_admin(test_admin): - return user_service.create_access_token(data={"username": test_admin.username}) + return user_service.create_access_token({"sub": test_admin.username}) # Test successful customer update @@ -51,13 +60,13 @@ def test_update_customer_success(mock_db_session, test_customer, access_token_ad update_data = { "first_name": "Jane", "last_name": "Smith", - "phone": "0987654321", + "phone_number": "0987654321", # Updated to match the `Profile` schema } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) assert response.status_code == 200 - assert response.json()['customer']['first_name'] == "Jane" - assert response.json()['customer']['last_name'] == "Smith" - assert response.json()['customer']['phone'] == "0987654321" + assert response.json()['data']['first_name'] == "Jane" + assert response.json()['data']['last_name'] == "Smith" + assert response.json()['data']['phone_number'] == "0987654321" # Test missing fields in update @@ -65,12 +74,12 @@ def test_update_customer_partial_success(mock_db_session, test_customer, access_ mock_db_session.query.return_value.filter.return_value.first.return_value = test_customer headers = {'Authorization': f'Bearer {access_token_admin}'} update_data = { - "phone": "0987654321", + "phone_number": "0987654321", } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data, headers=headers) assert response.status_code == 200 - assert response.json()['customer']['phone'] == "0987654321" - assert response.json()['customer']['first_name'] == test_customer.first_name # Check that other fields remain unchanged + assert response.json()['data']['phone_number'] == "0987654321" + assert response.json()['data']['first_name'] == test_customer.first_name # Check that other fields remain unchanged # Test unauthorized access @@ -81,4 +90,4 @@ def test_update_customer_unauthorized(mock_db_session, test_customer): "last_name": "Smith", } response = client.put(f"/api/v1/customers/{test_customer.id}", json=update_data) - assert response.status_code == 401 + assert response.status_code == 401 # Expecting 401 Unauthorized From d1ffae63bfdfe6dd220e38080b599b8a0add6658 Mon Sep 17 00:00:00 2001 From: mariams58 Date: Thu, 25 Jul 2024 16:13:57 +0100 Subject: [PATCH 006/566] feat: create magic link --- api/utils/send_mail.py | 30 +++++++++++++++++++ api/v1/routes/auth.py | 19 +++++++++++- api/v1/schemas/user.py | 10 +++++++ tests/v1/user_deactivation_test.py | 46 ++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 api/utils/send_mail.py diff --git a/api/utils/send_mail.py b/api/utils/send_mail.py new file mode 100644 index 000000000..a55bbf4de --- /dev/null +++ b/api/utils/send_mail.py @@ -0,0 +1,30 @@ +# app/email_utils.py +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from api.utils.settings import settings + +def send_magic_link(email: str, token: str): + '''Sends magic-kink to user email''' + sender_email = settings.MAIL_USERNAME + receiver_email = email + password = settings.MAIL_PASSWORD + + message = MIMEMultipart("alternative") + message["Subject"] = "Your Magic Link" + message["From"] = sender_email + message["To"] = receiver_email + + text = f"Use the following link to log in: http://localhost:8000/magic-link?token={token}" + html = f"

Use the following link to log in: Magic Link

" + + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + + message.attach(part1) + message.attach(part2) + + with smtplib.SMTP(settings.MAIL_SERVER, settings.MAIL_PORT) as server: + server.starttls() + server.login(sender_email, password) + server.sendmail(sender_email, receiver_email, message.as_string()) diff --git a/api/v1/routes/auth.py b/api/v1/routes/auth.py index 20b3a163c..8a45bafa9 100644 --- a/api/v1/routes/auth.py +++ b/api/v1/routes/auth.py @@ -2,10 +2,11 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from api.utils.success_response import success_response +from api.utils.send_mail import send_magic_link from api.v1.models import User from typing import Annotated from datetime import timedelta -from api.v1.schemas.user import UserCreate +from api.v1.schemas.user import UserCreate, MagicLinkRequest from api.db.database import get_db from api.utils.dependencies import get_current_admin from api.v1.services.user import user_service @@ -136,3 +137,19 @@ def refresh_access_token(request: Request, response: Response, db: Session = Dep def read_admin_data(current_admin: Annotated[User, Depends(get_current_admin)]): return {"message": "Hello, admin!"} + +@auth.post("/request-magic-link", status_code=status.HTTP_200_OK) +def request_magic_link(request: MagicLinkRequest, response: Response, db: Session = Depends(get_db)): + user = user_service.fetch_by_email( + db=db, + email=request.email + ) + access_token = user_service.create_access_token(user_id=user.id) + send_magic_link(user.email, access_token) + + response = success_response( + status_code=200, + message=f"Magic link sent to {user.email}" + ) + return response + diff --git a/api/v1/schemas/user.py b/api/v1/schemas/user.py index 3f4268ce4..c97411ca8 100644 --- a/api/v1/schemas/user.py +++ b/api/v1/schemas/user.py @@ -62,3 +62,13 @@ class DeactivateUserSchema(BaseModel): reason: Optional[str] = None confirmation: bool + +class MagicLinkRequest(BaseModel): + '''Schema for magic link creation''' + + email: EmailStr + +class MagicLinkResponse(BaseModel): + '''Schema for magic link respone''' + + message: str \ No newline at end of file diff --git a/tests/v1/user_deactivation_test.py b/tests/v1/user_deactivation_test.py index 227ca5486..ba1feec4e 100644 --- a/tests/v1/user_deactivation_test.py +++ b/tests/v1/user_deactivation_test.py @@ -19,6 +19,7 @@ client = TestClient(app) DEACTIVATION_ENDPOINT = '/api/v1/users/deactivation' LOGIN_ENDPOINT = 'api/v1/auth/login' +MAGIC_ENDPOINT = '/api/v1/auth/request-magic-link' @pytest.fixture @@ -163,3 +164,48 @@ def test_user_inactive(mock_user_service, mock_db_session): assert user_already_deactivated.status_code == 403 assert user_already_deactivated.json().get('message') == 'User is not active' + +@pytest.mark.usefixtures("mock_db_session", "mock_user_service") +def test_request_magic_link(mock_user_service, mock_db_session): + """Test for requesting magic link""" + + # Create a mock user + mock_user = User( + id=str(uuid7()), + username="testuser1", + email="testuser1@gmail.com", + password=user_service.hash_password("Testpassword@123"), + first_name='Test', + last_name='User', + is_active=False, + is_super_admin=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) + ) + mock_db_session.query.return_value.filter.return_value.first.return_value = mock_user + + with patch("api.utils.send_mail.smtplib.SMTP") as mock_smtp: + # Configure the mock SMTP server + mock_smtp_instance = MagicMock() + mock_smtp.return_value = mock_smtp_instance + + + # Test for requesting magic link for an existing user + magic_login = client.post(MAGIC_ENDPOINT, json={ + "email": mock_user.email + }) + assert magic_login.status_code == status.HTTP_200_OK + response = magic_login.json() + #assert response.get("status_code") == status.HTTP_200_OK # check for the right response before proceeding + assert response.get("message") == f"Magic link sent to {mock_user.email}" + + # Ensure the SMTP server was called correctly + #mock_smtp_instance.send_magic_link.assert_called_once() + # Test for requesting magic link for a non-existing user + mock_db_session.query.return_value.filter.return_value.first.return_value = None + magic_login = client.post(MAGIC_ENDPOINT, json={ + "email": "notauser@gmail.com" + }) + response = magic_login.json() + assert response.get("status_code") == status.HTTP_404_NOT_FOUND # check for the right response before proceeding + assert response.get("message") == "User not found" \ No newline at end of file From 5156962fd9ab94d2ed1d5b11d1dc4e6a9b80bba8 Mon Sep 17 00:00:00 2001 From: SCCSMARTCODE Date: Thu, 25 Jul 2024 18:06:58 +0100 Subject: [PATCH 007/566] Fixed test cases and updated update_customer function --- .idea/hng_boilerplate_python_fastapi_web.iml | 3 ++ api/utils/dependencies.py | 4 ++- api/v1/models/billing_plan.py | 2 +- api/v1/routes/customers.py | 35 +++++++++++++++----- api/v1/schemas/customer.py | 4 +-- api/v1/services/user.py | 12 +++++++ tests/v1/test_update_customer.py | 10 +++--- 7 files changed, 52 insertions(+), 18 deletions(-) diff --git a/.idea/hng_boilerplate_python_fastapi_web.iml b/.idea/hng_boilerplate_python_fastapi_web.iml index 8e5446ac9..519512493 100644 --- a/.idea/hng_boilerplate_python_fastapi_web.iml +++ b/.idea/hng_boilerplate_python_fastapi_web.iml @@ -11,4 +11,7 @@