From 342c61dfd47a7e80af95e1eb3d6809003232628c Mon Sep 17 00:00:00 2001 From: Arnthorny Date: Wed, 7 Aug 2024 16:54:09 +0100 Subject: [PATCH 1/4] feat: Endpoint that allows a superadmin delete an invitation from db --- api/v1/routes/invitations.py | 26 +++++- api/v1/services/invite.py | 26 +++++- tests/v1/invitation/test_delete_invitation.py | 85 +++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tests/v1/invitation/test_delete_invitation.py diff --git a/api/v1/routes/invitations.py b/api/v1/routes/invitations.py index 4265f257a..d22d16d8f 100644 --- a/api/v1/routes/invitations.py +++ b/api/v1/routes/invitations.py @@ -5,6 +5,7 @@ from api.db.database import get_db as get_session from api.v1.services import invite from api.v1.models.user import User +from api.utils.success_response import success_response from api.v1.services.user import user_service import logging @@ -42,4 +43,27 @@ async def add_user_to_organization( logging.info(f"Processing invitation ID: {invite_id}") - return invite.InviteService.add_user_to_organization(invite_id, session) \ No newline at end of file + return invite.InviteService.add_user_to_organization(invite_id, session) + +@invites.delete("/{invite_id}", status_code=200, response_model=success_response) +def delete_invite( + invite_id: str, + db: Session = Depends(get_session), + admin: User = Depends(user_service.get_current_super_admin) +): + """ Delete invite from database """ + if invite_id.strip() == "": + logging.warning(f"Invitation ID not found in path parameter.") + raise HTTPException(status_code=404, detail="Invite id is absent") + + invite_is_deleted = invite.InviteService.delete(db, invite_id) + + if not invite_is_deleted: + raise HTTPException(status_code=404, detail="Invalid invitation id") + + logging.info(f"Deleted invite. ID: {invite_id}") + + return success_response( + status_code=200, + message='Invite deleted successfully', + ) \ No newline at end of file diff --git a/api/v1/services/invite.py b/api/v1/services/invite.py index 5f57222c7..bda2e05b3 100644 --- a/api/v1/services/invite.py +++ b/api/v1/services/invite.py @@ -11,10 +11,11 @@ from api.v1.models.user import User from api.v1.models.associations import user_organization_association from api.v1.schemas import invitations +from api.core.base.services import Service from urllib.parse import urlencode -class InviteService: +class InviteService(Service): @staticmethod def create( invite: invitations.InvitationCreate, request: Request, session: Session @@ -139,5 +140,24 @@ def add_user_to_organization(invite_id: str, session: Session): status_code=500, detail="An error occurred while adding the user to the organization", ) - -invite_service = InviteService() \ No newline at end of file + @staticmethod + def delete(session: Session, id: str): + """Function to delete invite link + + Args: + session(Session): The current ORM session object. + id(str): Invite id string + + Returns: + True if delete is successful else False + + """ + invite = ( + session.query(Invitation).filter_by(id=id).first() + ) + + if invite is None: + return False + session.delete(invite) + session.commit() + return True \ No newline at end of file diff --git a/tests/v1/invitation/test_delete_invitation.py b/tests/v1/invitation/test_delete_invitation.py new file mode 100644 index 000000000..38f839947 --- /dev/null +++ b/tests/v1/invitation/test_delete_invitation.py @@ -0,0 +1,85 @@ +# Dependencies: +# pip install pytest-mock +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from main import app +from api.v1.services.user import user_service +from api.db.database import get_db +from sqlalchemy.orm import Session +from datetime import datetime +from api.v1.services.user import oauth2_scheme + + +def mock_deps(): + return MagicMock(id="user_id") + +def mock_oauth(): + return 'access_token' + +def mock_db(): + return MagicMock(spec=Session) + +@pytest.fixture +def client(): + client = TestClient(app) + yield client + +DELETE_ENDPOINT = "/api/v1/invite/invite_id" +class TestCodeUnderTest: + @classmethod + def setup_class(cls): + app.dependency_overrides[user_service.get_current_super_admin] = mock_deps + app.dependency_overrides[get_db] = mock_db + + @classmethod + def teardown_class(cls): + app.dependency_overrides = {} + + # Successfully delete invite from to the database + def test_delete_invite_success(self, client): + + response = client.delete(DELETE_ENDPOINT) + + assert response.status_code == 200 + assert response.json()['message'] == "Invite deleted successfully" + assert response.json().get('data') == None + assert response.json()['success'] == True + + + + # Invite id path parameter absent + def test_delete_invite_empty_id(self, client): + + response = client.delete("/api/v1/invite/ ") + + assert response.status_code == 404 + assert response.json()['message'] == "Invite id is absent" + + # Invalid invite id + def test_delete_invite_invalid_id(self, client): + + + with patch('api.v1.services.invite.InviteService.delete', return_value=False) as tmp: + response = client.delete(DELETE_ENDPOINT) + assert response.status_code == 404 + assert response.json()['message'] == "Invalid invitation id" + + # Handling unauthorized request + def test_delete_invite_unauth(self, client): + app.dependency_overrides = {} + + response = client.delete(DELETE_ENDPOINT) + assert response.status_code == 401 + assert response.json()['message'] == 'Not authenticated' + + # Handling forbidden request + def test_delete_invite_forbidden(self, client): + app.dependency_overrides = {} + app.dependency_overrides[get_db] = mock_db + app.dependency_overrides[oauth2_scheme] = mock_oauth + + with patch('api.v1.services.user.user_service.get_current_user', return_value=MagicMock(is_super_admin=False)) as cu: + response = client.delete(DELETE_ENDPOINT) + assert response.status_code == 403 + assert response.json()['message'] == 'You do not have permission to access this resource' From 4f9185c1b8eaf03dd9542b1b055e427b41dc1660 Mon Sep 17 00:00:00 2001 From: Arnthorny Date: Wed, 7 Aug 2024 17:13:40 +0100 Subject: [PATCH 2/4] fix: Revert line to instantiate invite_service object and add empty methods due to abstract class --- api/v1/services/invite.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/v1/services/invite.py b/api/v1/services/invite.py index bda2e05b3..87ac4d9c1 100644 --- a/api/v1/services/invite.py +++ b/api/v1/services/invite.py @@ -160,4 +160,15 @@ def delete(session: Session, id: str): return False session.delete(invite) session.commit() - return True \ No newline at end of file + return True + + def fetch(self): + pass + + def fetch_all(self): + pass + + def update(self): + pass + +invite_service = InviteService() \ No newline at end of file From 50b4974fb75183aed656d9091862405da5553fb9 Mon Sep 17 00:00:00 2001 From: Arnthorny Date: Wed, 7 Aug 2024 17:49:57 +0100 Subject: [PATCH 3/4] chore: Removed if-guard for invite_id path parameter --- api/v1/routes/invitations.py | 4 ---- tests/v1/invitation/test_delete_invitation.py | 9 --------- 2 files changed, 13 deletions(-) diff --git a/api/v1/routes/invitations.py b/api/v1/routes/invitations.py index d22d16d8f..21018a327 100644 --- a/api/v1/routes/invitations.py +++ b/api/v1/routes/invitations.py @@ -52,10 +52,6 @@ def delete_invite( admin: User = Depends(user_service.get_current_super_admin) ): """ Delete invite from database """ - if invite_id.strip() == "": - logging.warning(f"Invitation ID not found in path parameter.") - raise HTTPException(status_code=404, detail="Invite id is absent") - invite_is_deleted = invite.InviteService.delete(db, invite_id) if not invite_is_deleted: diff --git a/tests/v1/invitation/test_delete_invitation.py b/tests/v1/invitation/test_delete_invitation.py index 38f839947..62ea8bf20 100644 --- a/tests/v1/invitation/test_delete_invitation.py +++ b/tests/v1/invitation/test_delete_invitation.py @@ -47,15 +47,6 @@ def test_delete_invite_success(self, client): assert response.json()['success'] == True - - # Invite id path parameter absent - def test_delete_invite_empty_id(self, client): - - response = client.delete("/api/v1/invite/ ") - - assert response.status_code == 404 - assert response.json()['message'] == "Invite id is absent" - # Invalid invite id def test_delete_invite_invalid_id(self, client): From 8e361decf3212eb185b1883f837811870d8dc0c9 Mon Sep 17 00:00:00 2001 From: Arnthorny Date: Wed, 7 Aug 2024 17:57:45 +0100 Subject: [PATCH 4/4] chore: Changed endpoint status code from 200 OK to 204 NO Content --- api/v1/routes/invitations.py | 11 +++-------- tests/v1/invitation/test_delete_invitation.py | 6 +----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/api/v1/routes/invitations.py b/api/v1/routes/invitations.py index 21018a327..686b7d236 100644 --- a/api/v1/routes/invitations.py +++ b/api/v1/routes/invitations.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.orm import Session from urllib.parse import urlparse, parse_qs from api.v1.schemas import invitations @@ -45,7 +45,7 @@ async def add_user_to_organization( return invite.InviteService.add_user_to_organization(invite_id, session) -@invites.delete("/{invite_id}", status_code=200, response_model=success_response) +@invites.delete("/{invite_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_invite( invite_id: str, db: Session = Depends(get_session), @@ -57,9 +57,4 @@ def delete_invite( if not invite_is_deleted: raise HTTPException(status_code=404, detail="Invalid invitation id") - logging.info(f"Deleted invite. ID: {invite_id}") - - return success_response( - status_code=200, - message='Invite deleted successfully', - ) \ No newline at end of file + logging.info(f"Deleted invite. ID: {invite_id}") \ No newline at end of file diff --git a/tests/v1/invitation/test_delete_invitation.py b/tests/v1/invitation/test_delete_invitation.py index 62ea8bf20..9f01222b0 100644 --- a/tests/v1/invitation/test_delete_invitation.py +++ b/tests/v1/invitation/test_delete_invitation.py @@ -41,11 +41,7 @@ def test_delete_invite_success(self, client): response = client.delete(DELETE_ENDPOINT) - assert response.status_code == 200 - assert response.json()['message'] == "Invite deleted successfully" - assert response.json().get('data') == None - assert response.json()['success'] == True - + assert response.status_code == 204 # Invalid invite id def test_delete_invite_invalid_id(self, client):