diff --git a/server/app/controllers/__init__.py b/server/app/controllers/__init__.py index f7d4b9d..801af01 100644 --- a/server/app/controllers/__init__.py +++ b/server/app/controllers/__init__.py @@ -1,3 +1,9 @@ -from .controller import MainController +from app.controllers.events import EventController +from app.controllers.group import GroupController +from app.controllers.musicians import MusicianController +from app.controllers.users import UserController -controller = MainController() +user_controller = UserController() +event_controller = EventController() +group_controller = GroupController() +musicians_controller = MusicianController() diff --git a/server/app/controllers/controller.py b/server/app/controllers/controller.py index 6296541..28af7d7 100644 --- a/server/app/controllers/controller.py +++ b/server/app/controllers/controller.py @@ -3,6 +3,12 @@ from fastapi.security import HTTPAuthorizationCredentials from icecream import ic from app.admin import oauth_token +from app.controllers import ( + event_controller, + group_controller, + musicians_controller, + user_controller, +) from app.controllers.events import EventController from app.controllers.group import GroupController from app.controllers.musicians import MusicianController @@ -22,19 +28,29 @@ class MainController: All methods are asynchronous to facilitate asynchronous calls from the Router layer. token-based authentication is handled here as needed per the nature of the data being accessed. + + Testing: pass mocked sub-controllers to the constructor. """ - def __init__(self) -> None: - self.event_controller = EventController() - self.musician_controller = MusicianController() - self.user_controller = UserController() - self.group_controller = GroupController() + def __init__( + self, + event_controller=event_controller, + musicians_controller=musicians_controller, + user_controller=user_controller, + group_controller=group_controller, + oauth_token=oauth_token, + ) -> None: + self.event_controller = event_controller + self.musician_controller = musicians_controller + self.user_controller = user_controller + self.group_controller = group_controller + self.oauth_token = oauth_token async def get_musicians(self) -> list[Musician]: return self.musician_controller.get_musicians() - async def get_musician(self, id: int) -> Musician: - return self.musician_controller.get_musician(id) + async def get_musician(self, musician_id: int) -> Musician: + return self.musician_controller.get_musician(musician_id) async def update_musician( self, @@ -49,7 +65,7 @@ class MainController: status_code=status.HTTP_400_BAD_REQUEST, detail="ID in URL does not match ID in request body", ) - _, sub = oauth_token.email_and_sub(token) + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) return self.musician_controller.update_musician( musician_id=musician.id, @@ -60,42 +76,54 @@ class MainController: async def get_events(self) -> list[EventSeries]: return self.event_controller.get_all_series() - async def get_event(self, id: int) -> EventSeries: - return self.event_controller.get_one_series_by_id(id) + async def get_event(self, series_id: int) -> EventSeries: + return self.event_controller.get_one_series_by_id(series_id) async def create_event( self, series: NewEventSeries, token: HTTPAuthorizationCredentials ) -> EventSeries: - _, sub = oauth_token.email_and_sub(token) + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) return self.event_controller.create_series(series) async def add_series_poster( self, series_id: int, poster: UploadFile, token: HTTPAuthorizationCredentials ) -> EventSeries: - _, sub = oauth_token.email_and_sub(token) + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) return self.event_controller.add_series_poster(series_id, poster) - async def delete_series(self, id: int, token: HTTPAuthorizationCredentials) -> None: - _, sub = oauth_token.email_and_sub(token) + async def delete_series( + self, series_id: int, token: HTTPAuthorizationCredentials + ) -> None: + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) - self.event_controller.delete_series(id) + self.event_controller.delete_series(series_id) async def update_series( self, route_id: int, series: EventSeries, token: HTTPAuthorizationCredentials ) -> EventSeries: - _, sub = oauth_token.email_and_sub(token) + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) return self.event_controller.update_series(route_id, series) async def get_users(self) -> list[User]: return self.user_controller.get_users() - async def get_user(self, id: int) -> User: - return self.user_controller.get_user_by_id(id) + async def get_user(self, user_id: int) -> User: + return self.user_controller.get_user_by_id(user_id) async def create_user(self, token: HTTPAuthorizationCredentials) -> User: + """This method does NOT post a user to the database. + Instead, it retrieves the user's information from the OAuth token, + updates the user's sub in the db if needed, and returns the user object. + + Args: + token (HTTPAuthorizationCredentials): The OAuth token + + Returns: + User: The User object + """ return self.user_controller.create_user(token) async def get_group(self) -> Group: @@ -104,6 +132,6 @@ class MainController: async def update_group_bio( self, bio: str, token: HTTPAuthorizationCredentials ) -> Group: - _, sub = oauth_token.email_and_sub(token) + _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) return self.group_controller.update_group_bio(bio) diff --git a/server/app/controllers/users.py b/server/app/controllers/users.py index 6d19017..f28bf3d 100644 --- a/server/app/controllers/users.py +++ b/server/app/controllers/users.py @@ -116,6 +116,14 @@ class UserController(BaseController): ) def create_user(self, token: HTTPAuthorizationCredentials) -> User: + """Updates a user's sub in the database and creates a new User object. + + Args: + token (HTTPAuthorizationCredentials): The token containing the user's email and sub + + Returns: + User: A User object which is suitable for a response body + """ email, sub = oauth_token.email_and_sub(token) user: User = self.get_user_by_email(email) if user.sub is None: diff --git a/server/app/main.py b/server/app/main.py index 6bfae60..0d572da 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -3,7 +3,7 @@ from asyncio import gather from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.controllers import MainController +from app.controllers.controller import MainController from app.models.tgd import TheGrapefruitsDuo from app.routers.contact import router as contact_router from app.routers.events import router as event_router diff --git a/server/app/routers/__init__.py b/server/app/routers/__init__.py index e69de29..2982d32 100644 --- a/server/app/routers/__init__.py +++ b/server/app/routers/__init__.py @@ -0,0 +1,3 @@ +from app.controllers.controller import MainController + +controller = MainController() diff --git a/server/app/routers/events.py b/server/app/routers/events.py index 80ab053..a0da2b1 100644 --- a/server/app/routers/events.py +++ b/server/app/routers/events.py @@ -3,8 +3,8 @@ from fastapi.security import HTTPAuthorizationCredentials from icecream import ic from app.admin import oauth2_http -from app.controllers import controller from app.models.event import EventSeries, NewEventSeries +from app.routers import controller router = APIRouter( prefix="/events", diff --git a/server/app/routers/group.py b/server/app/routers/group.py index ddd6fe0..f059b6e 100644 --- a/server/app/routers/group.py +++ b/server/app/routers/group.py @@ -3,8 +3,8 @@ from fastapi.security.http import HTTPAuthorizationCredentials from icecream import ic from app.admin import oauth2_http -from app.controllers import controller from app.models.group import Group +from app.routers import controller router = APIRouter( prefix="/group", diff --git a/server/app/routers/musicians.py b/server/app/routers/musicians.py index 124c03b..265d912 100644 --- a/server/app/routers/musicians.py +++ b/server/app/routers/musicians.py @@ -3,8 +3,8 @@ from fastapi.security import HTTPAuthorizationCredentials from icecream import ic from app.admin import oauth2_http -from app.controllers import controller from app.models.musician import Musician +from app.routers import controller router = APIRouter( prefix="/musicians", diff --git a/server/app/routers/users.py b/server/app/routers/users.py index c2c1dc6..4e91ad6 100644 --- a/server/app/routers/users.py +++ b/server/app/routers/users.py @@ -2,8 +2,8 @@ from fastapi import APIRouter, Depends, status from fastapi.security import HTTPAuthorizationCredentials from app.admin import oauth2_http -from app.controllers import controller from app.models.user import User +from app.routers import controller router = APIRouter( prefix="/users", diff --git a/server/pyproject.toml b/server/pyproject.toml index ba8dfa0..6122102 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "thegrapefruitsduo" -version = "0.4.0" +version = "0.4.1" package-mode = false description = "FastAPI backend for thegrapefruitsduo.com" authors = ["Lucas Jensen "] diff --git a/server/tests/controllers/test_main_controller.py b/server/tests/controllers/test_main_controller.py new file mode 100644 index 0000000..35e97c7 --- /dev/null +++ b/server/tests/controllers/test_main_controller.py @@ -0,0 +1,168 @@ +from unittest.mock import MagicMock + +import pytest +from fastapi import HTTPException, status +from icecream import ic + +from app.controllers.controller import MainController +from app.models.event import Event, EventSeries, NewEventSeries +from app.models.group import Group +from app.models.musician import Musician +from app.models.user import User + +mock_user_controller = MagicMock() +mock_musician_controller = MagicMock() +mock_group_controller = MagicMock() +mock_event_controller = MagicMock() +mock_oauth_token = MagicMock() + +mock_oauth_token.email_and_sub = MagicMock(return_value=("email", "sub")) + +mock_token = MagicMock() + +controller = MainController( + user_controller=mock_user_controller, + musicians_controller=mock_musician_controller, + group_controller=mock_group_controller, + event_controller=mock_event_controller, + oauth_token=mock_oauth_token, # type: ignore +) + + +def test_type(): + """Tests the type of the controller object.""" + assert isinstance(controller, MainController) + + +@pytest.mark.asyncio +async def test_get_musicians(): + """Tests the get_musicians method.""" + await controller.get_musicians() + MagicMock.assert_called_once_with(mock_musician_controller.get_musicians) + + +@pytest.mark.asyncio +async def test_get_musician(): + """Tests the get_musician method.""" + musician_id = 1 + await controller.get_musician(musician_id) + MagicMock.assert_called_once_with( + mock_musician_controller.get_musician, musician_id + ) + + +@pytest.mark.asyncio +async def test_update_musician(): + """Tests the update_musician method. + Underlying controller methods are tested elsewhere. This test is to ensure the method is called correctly. + """ + musician = Musician( + id=1, name="John Doe", bio="A musician", headshot_id="headshot123" + ) + + await controller.update_musician( + musician=musician, url_param_id=1, token=mock_token + ) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called_once(mock_user_controller.get_user_by_sub) + MagicMock.assert_called_once(mock_musician_controller.update_musician) + + +@pytest.mark.asyncio +async def test_get_events(): + """Tests the get_events method.""" + await controller.get_events() + MagicMock.assert_called_once(mock_event_controller.get_all_series) + + +@pytest.mark.asyncio +async def test_get_event(): + """Tests the get_event method.""" + series_id = 1 + await controller.get_event(series_id) + MagicMock.assert_called_once_with( + mock_event_controller.get_one_series_by_id, series_id + ) + + +@pytest.mark.asyncio +async def test_create_event(): + """Tests the create_event method.""" + series = NewEventSeries(name="Test Event", description="A test event", events=[]) + await controller.create_event(series, mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.get_user_by_sub) + MagicMock.assert_called(mock_event_controller.create_series) + + +@pytest.mark.asyncio +async def test_add_series_poster(): + """Tests the add_series_poster method.""" + series_id = 1 + poster = MagicMock() + await controller.add_series_poster(series_id, poster, mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.get_user_by_sub) + MagicMock.assert_called(mock_event_controller.add_series_poster) + + +@pytest.mark.asyncio +async def test_delete_series(): + """Tests the delete_series method.""" + series_id = 1 + await controller.delete_series(series_id, mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.get_user_by_sub) + MagicMock.assert_called(mock_event_controller.delete_series) + + +@pytest.mark.asyncio +async def test_update_series(): + """Tests the update_series method.""" + series = EventSeries( + series_id=1, name="Test Event", description="A test event", events=[] + ) + await controller.update_series(1, series, mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.get_user_by_sub) + MagicMock.assert_called(mock_event_controller.update_series) + + +@pytest.mark.asyncio +async def test_get_users(): + """Tests the get_users method.""" + await controller.get_users() + MagicMock.assert_called_once(mock_user_controller.get_users) + + +@pytest.mark.asyncio +async def test_get_user(): + """Tests the get_user method.""" + user_id = 1 + await controller.get_user(user_id) + MagicMock.assert_called_once_with(mock_user_controller.get_user_by_id, user_id) + + +@pytest.mark.asyncio +async def test_create_user(): + """Tests the create_user method.""" + await controller.create_user(mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.create_user) + + +@pytest.mark.asyncio +async def test_get_group(): + """Tests the get_group method.""" + await controller.get_group() + MagicMock.assert_called_once(mock_group_controller.get_group) + + +@pytest.mark.asyncio +async def test_update_group_bio(): + """Tests the update_group_bio method.""" + bio = "A new bio" + await controller.update_group_bio(bio, mock_token) + MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) + MagicMock.assert_called(mock_user_controller.get_user_by_sub) + MagicMock.assert_called(mock_group_controller.update_group_bio)