From aebf4395054473e5ca4d5dd6f090313299dc326d Mon Sep 17 00:00:00 2001 From: Sofia Date: Sat, 25 May 2024 18:58:27 -0300 Subject: [PATCH] HAN 109: Tests unitarios (#23) added tests --- .github/workflows/tests.yaml | 49 +++++ Dockerfile.test | 17 ++ app/controller/Users.py | 2 +- app/{service => external}/Social.py | 0 app/external/__init__.py | 0 app/models/__init__.py | 0 app/repository/Users.py | 2 +- app/service/Users.py | 10 +- app/tests/__init__.py | 0 app/tests/test_users.py | 314 ++++++++++++++++++++++++++++ requirements.txt | 5 +- 11 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 Dockerfile.test rename app/{service => external}/Social.py (100%) create mode 100644 app/external/__init__.py create mode 100644 app/models/__init__.py create mode 100644 app/tests/__init__.py create mode 100644 app/tests/test_users.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..53958c8 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,49 @@ +name: Tests + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - '*' + +jobs: + test: + name: Tests microservice + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run tests & Coverage file + run: | + python -m venv venv + source venv/bin/activate + pytest + pytest --cache-clear --cov=app/service app/tests/ --cov-report=xml > pytest-coverage.txt + env: + DATABASE_URL: postgresql://pepe + PYTHONPATH: ./app + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + directory: ./coverage/reports/ + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true + path_to_write_report: ./coverage/codecov_report.txt + verbose: true diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..3bb66cb --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,17 @@ +FROM python:3.11-slim-buster + +WORKDIR /users + +RUN apt-get update \ + && apt-get -y install libpq-dev gcc \ + && pip install psycopg2 + +COPY requirements.txt ./ + +RUN pip install -r requirements.txt + +EXPOSE ${PORT} + +COPY . . + +CMD ["sh", "-c","DATABASE_URL=postgresql://user:1234@sql:5432/users", "PYTHONPATH=./app", "pytest"] \ No newline at end of file diff --git a/app/controller/Users.py b/app/controller/Users.py index 412608b..59b821c 100644 --- a/app/controller/Users.py +++ b/app/controller/Users.py @@ -1,7 +1,7 @@ from fastapi import status from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from service.Social import SocialService +from external.Social import SocialService from service.Users import UsersService diff --git a/app/service/Social.py b/app/external/Social.py similarity index 100% rename from app/service/Social.py rename to app/external/Social.py diff --git a/app/external/__init__.py b/app/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/repository/Users.py b/app/repository/Users.py index d3eed34..6a4ec32 100644 --- a/app/repository/Users.py +++ b/app/repository/Users.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session from os import environ from typing import Optional -from models.database import Base +from app.models.database import Base from models.users import User from models.alarms import Alarm from datetime import date, datetime diff --git a/app/service/Users.py b/app/service/Users.py index 034983b..8911616 100644 --- a/app/service/Users.py +++ b/app/service/Users.py @@ -92,7 +92,7 @@ def delete_notification(self, user_id: int, notification_id: int): self.user_repository.rollback() raise e - def _generate_nickname(self, name): + def _generate_nickname(self, name): # pragma: no cover name_without_spaces = name.replace(" ", "") uuid_max = 8 @@ -131,7 +131,7 @@ def login(self, auth_code: str): algorithm="HS256") return user, jwt_token - def _get_access_token(self, authorization_code): + def _get_access_token(self, authorization_code): # pragma: no cover token_url = "https://oauth2.googleapis.com/token" payload = { "client_id": os.environ["GOOGLE_CLIENT_ID"], @@ -146,7 +146,7 @@ def _get_access_token(self, authorization_code): else: return None - def _get_user_info(self, access_token): + def _get_user_info(self, access_token): # pragma: no cover user_info_url = "https://www.googleapis.com/oauth2/v2/userinfo" headers = {"Authorization": f"Bearer {access_token}"} params = {"fields": "id,email,name,picture,gender"} @@ -171,14 +171,14 @@ def _validate_location(self, location): return True return False - def retrieve_user_id(self, request): + def retrieve_user_id(self, request): # pragma: no cover token = self.__get_token(request.headers) payload = jwt.decode(token, os.environ["JWT_SECRET"], algorithms=["HS256"]) return int(payload.get("user_id")) - def __get_token(self, headers: dict): + def __get_token(self, headers: dict): # pragma: no cover keyName = None for key in headers.keys(): if key.lower() == TOKEN_FIELD_NAME: diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_users.py b/app/tests/test_users.py new file mode 100644 index 0000000..6bcfaa0 --- /dev/null +++ b/app/tests/test_users.py @@ -0,0 +1,314 @@ +import unittest +from datetime import date, datetime + + +import pytest +from unittest.mock import Mock, MagicMock + +from repository.Users import UsersRepository +from service.Users import UsersService +from schemas.Schemas import ( + CreateNotificationSchema, + CreateUserSchema, + UserSchema +) + +from exceptions.UserException import ( + ForbiddenUser, + InvalidData, + ResourceNotFound, + InvalidURL +) + + +@pytest.fixture +def mock_user_repository(): + return Mock() + + +@pytest.fixture +def users_service(mock_user_repository): + return UsersService(mock_user_repository) + + +def get_mock(classToMock, attributes=None): + if attributes is None: + attributes = {} + mock = MagicMock(spec=classToMock) + mock.configure_mock(**attributes) + return mock + + +john = UserSchema( + id=1, + name="John", + email="john@mail.com", + gender="male", + photo="link.com", + birthdate=date(1990, 5, 12), + location={}, + nickname="john", + biography="un tipo", + device_token="d3vic2to0k3n" +) + +alice = UserSchema( + id=2, + name="Alice", + email="alice@mail.com", + gender="female", + photo="link.com", + birthdate=date(1990, 5, 10), + location={}, + nickname="alice", + biography="una tipa", + device_token="d3vic2to0k3n" +) + +new_user = CreateUserSchema( + name="Alice", + email="alice@mail.com", + location={"lat": 20, "long": 100} +) + + +class ServiceTests(unittest.TestCase): + def test_get_user(self): + attr_db = {"get_user.return_value": john} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + user = service.get_user(1) + + self.assertEqual(user, john) + mock_db.get_user.assert_called_once_with(1) + + def test_raise_user_not_found(self): + attr_db = {"get_user.return_value": None} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(ResourceNotFound): + service.get_user(10) + + def test_get_all_users(self): + attr_db = {"get_all_users.return_value": [john, alice]} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + users = service.get_all_users() + + self.assertEqual(len(users), 2) + mock_db.get_all_users.assert_called_once() + + def test_get_users_by_id(self): + attr_db = {"get_users_by_ids.return_value": [john, alice]} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + users = service.get_users_by_ids([1, 2]) + + self.assertTrue(users.__contains__(john)) + self.assertTrue(users.__contains__(alice)) + mock_db.get_users_by_ids.assert_called_once_with([1, 2]) + + def test_create_user(self): + attr_db = { + "add.return_value": None, + "create_user.return_value": {"id": 3, "name": "Alice", + "email": "alice@mail.com"}, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + user = service.create_user(new_user.dict()) + + self.assertEqual(user["name"], "Alice") + mock_db.create_user.assert_called_once() + mock_db.rollback.assert_not_called() + + def test_create_user_rollback_if_failed(self): + attr_db = { + "create_user.side_effect": Exception(), + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(Exception): + service.create_user(new_user.dict()) + + mock_db.rollback.assert_called_once() + + def test_create_fails_if_invalid_location(self): + attr_db = {"rollback.return_value": None} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + invalid_user = CreateUserSchema(name="Alice", email="alice@mail.com", + location={"lat": 20}) + + with self.assertRaises(InvalidData): + service.create_user(invalid_user.dict()) + mock_db.add.assert_not_called() + + def test_update_user(self): + attr_db = { + "add.return_value": None, + "edit_user.return_value": None, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + service.update_user(1, {"nickname": "alice"}) + + mock_db.edit_user.assert_called_once() + mock_db.rollback.assert_not_called() + + def test_update_user_rollback_if_failed(self): + attr_db = { + "edit_user.side_effect": Exception(), + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(Exception): + service.update_user(1, {"nickname": "alice"}) + + mock_db.rollback.assert_called_once() + + def test_update_fails_if_invalid_location(self): + attr_db = {"edit_user.return_value": None} + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + invalid_update = {"photo": "invalidlink"} + + with self.assertRaises(InvalidURL): + service.update_user(1, invalid_update) + + def test_create_notification(self): + attr_db = { + "create_notification.return_value": None, + "get_notifications.return_value": [ + { + "datetime": datetime(2023, 5, 17, 10, 30), + "content": 'Alarm' + } + ], + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + service.create_notification( + 1, + CreateNotificationSchema(date_time=datetime(2023, 5, 17, 10, 30), + content="Alarm") + ) + + notifications = service.get_notifications(1) + notification = notifications.pop() + + self.assertEqual(notification["content"], "Alarm") + mock_db.create_notification.assert_called_once() + mock_db.rollback.assert_not_called() + + def test_delete_notification(self): + attr_db = { + "delete_notification.return_value": None, + "get_notification_owner.return_value": 1, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + service.delete_notification(1, 1) + + mock_db.delete_notification.assert_called_once() + mock_db.rollback.assert_not_called() + + def test_delete_notification_fails_if_not_authorized(self): + attr_db = { + "delete_notification.return_value": None, + "get_notification_owner.return_value": 2, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(ForbiddenUser): + service.delete_notification(1, 1) + + def test_delete_fails_if_notification_doesnt_exist(self): + attr_db = { + "delete_notification.return_value": None, + "get_notification_owner.return_value": None, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(ResourceNotFound): + service.delete_notification(1, 1) + + def test_update_notification(self): + attr_db = { + "edit_notification.return_value": None, + "get_notification_owner.return_value": 1, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + service.update_notification( + 1, + 1, + CreateNotificationSchema( + date_time=datetime(2023, 6, 17, 10, 30), + content="Alarm" + ) + ) + + mock_db.edit_notification.assert_called_once() + mock_db.rollback.assert_not_called() + + def test_create_notification_rollback_if_failed(self): + attr_db = { + "create_notification.side_effect": Exception(), + "get_notification_owner.return_value": 1, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(Exception): + service.create_notification( + 1, + CreateNotificationSchema( + date_time=datetime(2023, 5, 17, 10, 30), + content="Alarm") + ) + + mock_db.rollback.assert_called_once() + + def test_delete_notification_rollback_if_failed(self): + attr_db = { + "delete_notification.side_effect": Exception(), + "get_notification_owner.return_value": 1, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(Exception): + service.delete_notification(1, 1) + + mock_db.rollback.assert_called_once() + + def test_update_notification_rollback_if_failed(self): + attr_db = { + "edit_notification.side_effect": Exception(), + "get_notification_owner.return_value": 1, + "rollback.return_value": None + } + mock_db = get_mock(UsersRepository, attr_db) + service = UsersService(mock_db) + + with self.assertRaises(Exception): + service.update_notification(1, 1, {}) + + mock_db.rollback.assert_called_once() diff --git a/requirements.txt b/requirements.txt index 15fccd4..a4db993 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,7 @@ APScheduler>=3.10,<4.0 arq pytz python-jose -async-firebase \ No newline at end of file +async-firebase +pytest +mock +pytest-cov \ No newline at end of file