diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..c381224 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,33 @@ +name: CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install uv flake8 pytest isort + uv pip install -r requirements.txt + + - name: Run isort + run: isort . --check-only --diff + + - name: Run linter + run: flake8 . + + - name: Run tests + run: python -m pytest \ No newline at end of file diff --git a/celerybeat-schedule.db b/celerybeat-schedule.db index 29ed372..ee020cb 100644 Binary files a/celerybeat-schedule.db and b/celerybeat-schedule.db differ diff --git a/mini/api/endpoints/users.py b/mini/api/endpoints/users.py index 23b4d59..3e08cdb 100644 --- a/mini/api/endpoints/users.py +++ b/mini/api/endpoints/users.py @@ -1,9 +1,7 @@ -from typing import Any, Dict, List +from fastapi import APIRouter, Body, Depends, Path, Security +from fastapi.responses import JSONResponse +from typing import List -from fastapi import APIRouter, Body, Depends, Path, Request, Security -from pydantic import BaseModel - -from config.config import config from config.container import container from mini.api.security import verify_api_key from mini.core.schema.tables import Room, User @@ -11,7 +9,6 @@ router = APIRouter() - @router.post("/users", response_model=User) async def create_user( id: str = Body(..., title="UUID that Supabase Auth created on the frontend"), @@ -21,102 +18,35 @@ async def create_user( ) -> User: return user_service.create_user(id=id, phone_number=phone_number) - -@router.get("/users/{user_id}/rooms") +@router.get("/users/{user_id}/rooms", response_model=List[Room]) def get_user_rooms( user_id: str = Path(..., title="The ID of the user to get"), user_service: UserService = Depends(lambda: container.get_user_service()), api_key: str = Security(verify_api_key), -) -> List[Room] | None: - """Get all rooms that a user is in""" +) -> List[Room]: return user_service.get_user_rooms(user_id) - @router.get("/users/{user_id}", response_model=User) async def get_user( user_id: str = Path(..., title="The ID of the user to get"), user_service: UserService = Depends(lambda: container.get_user_service()), api_key: str = Security(verify_api_key), ) -> User: - """Get a user by ID. Returns the retrieved User object""" return user_service.get_user(user_id) - @router.patch("/users/{user_id}", response_model=User) async def update_user( user_id: str = Path(..., title="The ID of the user to update"), - user_update: Dict[str, Any] = Body(..., title="The fields to update"), + user_update: dict = Body(..., title="The fields to update"), user_service: UserService = Depends(lambda: container.get_user_service()), api_key: str = Security(verify_api_key), ) -> User: - """Update a user. Returns updated User object""" return user_service.update_user(user_id=user_id, user_params=user_update) - @router.delete("/users/{user_id}") async def delete_user( user_id: str = Path(..., title="The ID of the user to delete"), user_service: UserService = Depends(lambda: container.get_user_service()), api_key: str = Security(verify_api_key), -) -> None: - """Delete a user. Returns the id of the deleted user.""" - return user_service.delete_user(user_id) - - -async def test_user_endpoints(): - TEST_ID = "946f4f9d-1111-495e-b59d-5f3704deb11b" # Change as needed - - async with httpx.AsyncClient(base_url="http://127.0.0.1:8000") as client: - # Test create_user - user_data = { - "id": "946f4f9d-1111-495e-b59d-5f3704deb11b", - "phone_number": "+13143209682", - } - - response = await client.post( - f"/users", - json=user_data, - headers={"Authorization": f"Bearer {config.BACKEND_API_KEY}"}, - ) - print( - f"POST /users - Status: {response.status_code}, Response: {response.json()}" - ) - - # # Test get_user - # response = await client.get( - # f"/users/{TEST_ID}", - # headers={"Authorization": f"Bearer {config.BACKEND_API_KEY}"}, - # ) - # print( - # f"GET /users/{TEST_ID} - Status: {response.status_code}, Response: {response.json()}" - # ) - - # # # Test update_user - # user_data = {"subscription_status": "inactive"} - # response = await client.patch( - # f"/users/{TEST_ID}", - # json=user_data, - # headers={"Authorization": f"Bearer {config.BACKEND_API_KEY}"}, - # ) - # print( - # f"PATCH /users/{TEST_ID} - Status: {response.status_code}, Response: {response.json()}" - # ) - - # # # Test delete_user - # response = await client.delete( - # f"/users/{TEST_ID}", - # headers={"Authorization": f"Bearer {config.BACKEND_API_KEY}"}, - # ) - # print( - # f"DELETE /users/{TEST_ID} - Status: {response.status_code}, Response: {response.json()}" - # ) - - -if __name__ == "__main__": - import asyncio - - import httpx - - from config.config import config - - asyncio.run(test_user_endpoints()) +) -> JSONResponse: + return user_service.delete_user(user_id) \ No newline at end of file diff --git a/mini/service/payment_service.py b/mini/service/payment_service.py index 126d582..986ed66 100644 --- a/mini/service/payment_service.py +++ b/mini/service/payment_service.py @@ -8,6 +8,7 @@ from mini.core.schema.tables import Subscription, Tables, User from mini.manager.database import DatabaseManager from mini.manager.payment import CheckoutManager, CustomerManager, SubscriptionManager +from mini.manager.messaging import discord_manager from .base import Service @@ -119,6 +120,11 @@ async def _handle_checkout_session_completed( condition_value=user_id, ) + discord_manager.send_message_to_channel( + message=f"Subscription created by user {user_id}.", + channel=config.DISCORD_CONFIG.website_activity_webhook_url, + ) + except Exception as e: logger.error(f"Error retrieving subscription information: {e}") raise e diff --git a/mini/service/user_service.py b/mini/service/user_service.py index 313d990..d8ff8dd 100644 --- a/mini/service/user_service.py +++ b/mini/service/user_service.py @@ -1,10 +1,10 @@ from typing import Any, Dict, List -from config.config import config from fastapi import HTTPException from fastapi.responses import JSONResponse from postgrest.exceptions import APIError +from config.config import config from mini.core.schema.subscription import SubscriptionStatus from mini.core.schema.tables import Room, Tables, User from mini.manager.database import DatabaseManager @@ -12,9 +12,6 @@ from mini.manager.messaging.discord import discord_manager from mini.service.base import Service -import requests - - class UserService(Service): def __init__( self, database_manager: DatabaseManager, customer_manager: CustomerManager @@ -43,7 +40,6 @@ def create_user(self, id: str, phone_number: str) -> User: raise HTTPException( status_code=409, detail="User with this phone number already exists" ) - raise HTTPException( status_code=500, detail=f"Error creating user: {str(e)}" ) @@ -61,7 +57,6 @@ def update_user(self, user_id: str, user_params: Dict[str, Any]) -> User: if not existing_user: raise HTTPException(status_code=404, detail="User not found") - # Only update fields that are provided and not None update_data = {k: v for k, v in user_params.items() if v is not None} updated_user = self.database_manager.update( @@ -74,8 +69,7 @@ def update_user(self, user_id: str, user_params: Dict[str, Any]) -> User: raise HTTPException(status_code=400, detail="Failed to update user") return updated_user - def delete_user(self, id: str) -> None: - """Delete a user. Returns the id of the deleted user.""" + def delete_user(self, id: str) -> JSONResponse: existing_user = self.database_manager.get_row( Tables.USERS, {Tables.USERS__id: id} ) @@ -87,13 +81,11 @@ def delete_user(self, id: str) -> None: ) if not deleted_user_id: raise HTTPException(status_code=400, detail="Failed to delete user") - return JSONResponse(status_code=200, content="Succesfully deleted user") + return JSONResponse(status_code=200, content="Successfully deleted user") def get_user_rooms(self, user_id: str) -> List[Room]: - """Get all rooms that a user is in""" rooms = self.database_manager.get_multiple_rows( table_name=Tables.ROOMS, conditions={Tables.ROOMS__user_id: user_id}, ) - - return rooms + return rooms \ No newline at end of file diff --git a/tests/api/test_users.py b/tests/api/test_users.py new file mode 100644 index 0000000..89cf620 --- /dev/null +++ b/tests/api/test_users.py @@ -0,0 +1,111 @@ +import pytest +from fastapi.testclient import TestClient +from main import app +from config.config import config +import uuid + +@pytest.fixture(scope="module") +def client(): + return TestClient(app) + +@pytest.fixture(scope="module") +def api_key_headers(): + return {"Authorization": f"Bearer {config.BACKEND_API_KEY}"} + +@pytest.fixture(scope="function") +def test_user(client, api_key_headers): + # Create a unique user with a unique phone number + user_data = { + "id": str(uuid.uuid4()), + "phone_number": f"+1314320{uuid.uuid4().hex[:4]}", # Generate a unique phone number + } + response = client.post("/users", json=user_data, headers=api_key_headers) + + # Check if creation was successful + assert response.status_code == 200, f"Failed to create user: {response.json()}" + + created_user = response.json() + + # Yield the created user to the test + yield created_user + + # Cleanup: delete the user after the test + delete_response = client.delete(f"/users/{created_user['id']}", headers=api_key_headers) + + # Ensure the user deletion was successful + assert delete_response.status_code == 200, f"Failed to delete user: {delete_response.json()}" + + +def test_create_user(client, api_key_headers): + user_data = { + "id": str(uuid.uuid4()), + "phone_number": "+13143209683", + } + response = client.post("/users", json=user_data, headers=api_key_headers) + assert response.status_code == 200 + assert "id" in response.json() + assert response.json()["phone_number"] == user_data["phone_number"] + + # Clean up: delete the created user + client.delete(f"/users/{response.json()['id']}", headers=api_key_headers) + +def test_get_user(client, api_key_headers, test_user): + response = client.get(f"/users/{test_user['id']}", headers=api_key_headers) + assert response.status_code == 200 + assert response.json()["id"] == test_user['id'] + +def test_get_user_rooms(client, api_key_headers, test_user): + response = client.get(f"/users/{test_user['id']}/rooms", headers=api_key_headers) + assert response.status_code == 200 + assert isinstance(response.json(), list) + +def test_update_user(client, api_key_headers, test_user): + update_data = {"is_subscribed": "false"} + response = client.patch(f"/users/{test_user['id']}", json=update_data, headers=api_key_headers) + assert response.status_code == 200 + assert response.json()["is_subscribed"] == False + +def test_delete_user(client, api_key_headers): + # Create a user to delete + user_data = { + "id": str(uuid.uuid4()), + "phone_number": "+13143209684", + } + create_response = client.post("/users", json=user_data, headers=api_key_headers) + assert create_response.status_code == 200 + user_id = create_response.json()["id"] + + # Delete the user + delete_response = client.delete(f"/users/{user_id}", headers=api_key_headers) + assert delete_response.status_code == 200 + assert delete_response.json() == "Successfully deleted user" + + # Verify the user is deleted + get_response = client.get(f"/users/{user_id}", headers=api_key_headers) + assert get_response.status_code == 404 + +def test_get_nonexistent_user(client, api_key_headers): + nonexistent_id = str(uuid.uuid4()) + response = client.get(f"/users/{nonexistent_id}", headers=api_key_headers) + assert response.status_code == 404 + assert "User not found" in response.json()["detail"] + +def test_create_user_duplicate_phone(client, api_key_headers, test_user): + user_data = { + "id": str(uuid.uuid4()), + "phone_number": test_user["phone_number"], # Use the same phone number as test_user + } + response = client.post("/users", json=user_data, headers=api_key_headers) + assert response.status_code == 409 + assert "User with this phone number already exists" in response.json()["detail"] + +def test_api_key_required(client): + response = client.get("/users/123") + assert response.status_code == 403 + assert "Not authenticated" in response.json()["detail"] + +def test_invalid_api_key(client): + invalid_headers = {"Authorization": "Bearer invalid_key"} + response = client.get("/users/123", headers=invalid_headers) + assert response.status_code == 403 + assert "Could not validate credentials" in response.json()["detail"] \ No newline at end of file