diff --git a/api/v1/routes/api_status.py b/api/v1/routes/api_status.py index baf670124..473cb60da 100644 --- a/api/v1/routes/api_status.py +++ b/api/v1/routes/api_status.py @@ -1,6 +1,6 @@ from typing import Annotated from api.db.database import get_db -from api.v1.schemas.api_status import APIStatusPost +from api.v1.schemas.api_status import APIStatusPost, APIStatusUpdate from api.v1.services.api_status import APIStatusService from api.utils.success_response import success_response from fastapi import APIRouter, Depends, status @@ -32,3 +32,15 @@ async def post_api_status( data=new_status, status_code=status.HTTP_201_CREATED ) +@api_status.put('/{api_group}', response_model=success_response, status_code=200) +async def update_api_status( + api_group: str, + schema: APIStatusUpdate, + db: Annotated[Session, Depends(get_db)] +): + updated_status = APIStatusService.update(db, api_group, schema) + return success_response( + message='API Status updated successfully', + data=updated_status, + status_code=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/api/v1/schemas/api_status.py b/api/v1/schemas/api_status.py index 31b1fb468..f88da2fda 100644 --- a/api/v1/schemas/api_status.py +++ b/api/v1/schemas/api_status.py @@ -1,6 +1,5 @@ from decimal import Decimal from pydantic import BaseModel, Field, PositiveInt, PositiveFloat, ConfigDict, StringConstraints - from typing import List, Optional from datetime import datetime @@ -22,7 +21,7 @@ class APIStatusPost(BaseModel): api_group: str status: str response_time: Optional[Decimal] = None - details: str + details: str | None = None class Config: from_attributes = True @@ -44,7 +43,7 @@ class APIStatusUpdate(BaseModel): created_at (datetime): The date and time when the API status was created. """ - api_group: str = Field(..., alias="apiGroup") + api_group: Optional[str] = Field(None, alias="apiGroup") status: str last_checked: Optional[datetime] = None response_time: Optional[Decimal] = None diff --git a/api/v1/services/api_status.py b/api/v1/services/api_status.py index a46c288c4..1039e7c6f 100644 --- a/api/v1/services/api_status.py +++ b/api/v1/services/api_status.py @@ -2,7 +2,7 @@ from api.core.base.services import Service from sqlalchemy.orm import Session from api.v1.models.api_status import APIStatus -from api.v1.schemas.api_status import APIStatusPost +from api.v1.schemas.api_status import APIStatusPost, APIStatusUpdate from fastapi import HTTPException @@ -81,20 +81,51 @@ def upsert(db: Session, schema: APIStatusPost) -> APIStatus: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="A database error occurred." ) + @staticmethod + def update(db: Session, api_group: str, schema: APIStatusUpdate) -> APIStatus: + """ + Update an existing API status record by api_group. + + Parameters: + db (Session): The SQLAlchemy database session. + api_group (str): The api_group identifier to update. + schema (APIStatusUpdate): The data model with updated API status info. + + Returns: + APIStatus: The updated API status record. + + Raises: + HTTPException: 404 if the api_group doesn't exist, 500 for db errors. + """ + try: + existing_status = db.query(APIStatus).filter(APIStatus.api_group == api_group).first() + if not existing_status: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="API Status not found" + ) + if schema.status is not None: + existing_status.status = schema.status + if schema.response_time is not None: + existing_status.response_time = Decimal(schema.response_time) + if schema.details is not None: + existing_status.details = schema.details + if schema.last_checked is not None: + existing_status.last_checked = schema.last_checked + + db.commit() + db.refresh(existing_status) + return existing_status + + except HTTException as e: + raise e + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="A database error occured." + ) - # @staticmethod - # def update(db: Session, schema: APIStatusPost) -> APIStatus: - # status = APIStatus( - # api_group=schema.api_group, - # status=schema.status, - # response_time=schema.response_time, - # details=schema.details, - # ) - # db.add(status) - # db.commit() - # db.refresh(status) - # return status - @staticmethod def delete_by_api_group(db: Session, api_group) -> APIStatus: status = db.query(APIStatus).filter(APIStatus.api_group == api_group).first() diff --git a/tests/v1/status_page/test_post_status.py b/tests/v1/status_page/test_post_status.py index fabc7919f..2cfcaa85c 100644 --- a/tests/v1/status_page/test_post_status.py +++ b/tests/v1/status_page/test_post_status.py @@ -5,19 +5,21 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session from uuid_extensions import uuid7 +from fastapi import HTTPException from api.db.database import get_db from api.v1.models.contact_us import ContactUs from api.v1.models.api_status import APIStatus +from api.v1.services.api_status import APIStatusService from main import app - @pytest.fixture def db_session_mock(): db_session = MagicMock(spec=Session) return db_session + @pytest.fixture def client(db_session_mock): app.dependency_overrides[get_db] = lambda: db_session_mock @@ -32,7 +34,19 @@ def mock_post_api_status(): api_group="Blog API", status="Down", response_time=None, - details="API not responding (HTTP 503)" + details="API not responding (HTTP 503)", + last_checked=datetime.now(timezone.utc) + ) + + +def mock_updated_api_status(): + return APIStatus( + id=str(uuid7()), + api_group="Blog API", + status="Up", + response_time=200.0, + details="API is running fine", + last_checked=datetime.now(timezone.utc) ) @@ -44,8 +58,8 @@ def test_post_api_status(mock_create, db_session_mock, client): db_session_mock.commit.return_value = None db_session_mock.refresh.return_value = None - mock_create.return_value = mock_post_api_status() - # mock_check_existing.return_value = None + mock_status = mock_post_api_status() + mock_create.return_value = mock_status response = client.post('/api/v1/api-status', json={ "api_group": "Blog API", @@ -54,7 +68,13 @@ def test_post_api_status(mock_create, db_session_mock, client): "details": "API not responding (HTTP 503)" }) - assert response.status_code == 201 + assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] == "success" + assert json_response["status_code"] == 201 + assert json_response["message"] == "API Status created successfully" + assert json_response["data"]["api_group"] == "Blog API" + @patch("api.v1.services.api_status.APIStatusService.fetch_all") def test_get_api_status(mock_fetch, db_session_mock, client): @@ -64,9 +84,121 @@ def test_get_api_status(mock_fetch, db_session_mock, client): db_session_mock.commit.return_value = None db_session_mock.refresh.return_value = None - mock_fetch.return_value = mock_post_api_status() + mock_status = mock_post_api_status() + mock_fetch.return_value = [mock_status] response = client.get('/api/v1/api-status') - print(response.json()) - assert response.status_code == 200 + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] == "success" + assert json_response["status_code"] == 200 + assert json_response["message"] == "All API Status fetched successfully" + assert json_response["data"][0]["api_group"] == "Blog API" + + +@patch("api.v1.services.api_status.APIStatusService.update") +def test_put_api_status_success(mock_update, db_session_mock, client): + """Tests the PUT /api/v1/api-status/{api_group} endpoint for successful update""" + + db_session_mock.commit.return_value = None + db_session_mock.refresh.return_value = None + + mock_status = mock_updated_api_status() + mock_update.return_value = mock_status + + update_data = { + "apiGroup": "Blog API", + "status": "Up", + "response_time": 200, + "details": "API is running fine" + } + response = client.put('/api/v1/api-status/Blog API', json=update_data) + + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] == "success" + assert json_response["status_code"] == 200 + assert json_response["message"] == "API Status updated successfully" + data = json_response["data"] + assert data["api_group"] == "Blog API" + assert data["status"] == "Up" + assert float(data["response_time"]) == 200.0 + assert data["details"] == "API is running fine" + + +@patch("api.v1.services.api_status.APIStatusService.update") +def test_put_api_status_not_found(mock_update, db_session_mock, client): + """Tests the PUT /api/v1/api-status/{api_group} endpoint for a non-existent api_group""" + + mock_update.side_effect = HTTPException( + status_code=404, detail="API Status not found" + ) + + update_data = { + "apiGroup": "Nonexistent API", + "status": "Up" + } + response = client.put('/api/v1/api-status/Nonexistent API', json=update_data) + + assert response.status_code == 404, f"Expected 404, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] is False + assert json_response["status_code"] == 404 + assert json_response["message"] == "API Status not found" + + +@patch("api.v1.services.api_status.APIStatusService.update") +def test_put_api_status_partial_update(mock_update, db_session_mock, client): + """Tests the PUT /api/v1/api-status/{api_group} endpoint for partial update""" + + db_session_mock.commit.return_value = None + db_session_mock.refresh.return_value = None + + partial_updated_status = APIStatus( + id=str(uuid7()), + api_group="Blog API", + status="Inactive", + response_time=None, + details="API not responding (HTTP 503)", + last_checked=datetime.now(timezone.utc) + ) + mock_update.return_value = partial_updated_status + + update_data = { + "status": "Inactive" + } + response = client.put('/api/v1/api-status/Blog API', json=update_data) + + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] == "success" + assert json_response["status_code"] == 200 + assert json_response["message"] == "API Status updated successfully" + data = json_response["data"] + assert data["api_group"] == "Blog API" + assert data["status"] == "Inactive" + assert data["response_time"] is None + assert data["details"] == "API not responding (HTTP 503)" + + +@patch("api.v1.services.api_status.APIStatusService.update") +def test_put_api_status_invalid_response_time(mock_update, db_session_mock, client): + """Tests the PUT /api/v1/api-status/{api_group} endpoint with invalid response_time""" + + update_data = { + "apiGroup": "Blog API", + "status": "Up", + "response_time": "invalid" + } + response = client.put('/api/v1/api-status/Blog API', json=update_data) + + assert response.status_code == 422, f"Expected 422, got {response.status_code}: {response.text}" + json_response = response.json() + assert json_response["status"] is False + assert json_response["status_code"] == 422 + assert json_response["message"] == "Invalid input" + assert len(json_response["errors"]) == 1 + assert json_response["errors"][0]["loc"] == ["body", "response_time"] + assert json_response["errors"][0]["msg"] == "Input should be a valid decimal" + assert json_response["errors"][0]["type"] == "decimal_parsing" \ No newline at end of file