From 99e43c7b309d36e64086b6bd33100c0854ff30e8 Mon Sep 17 00:00:00 2001 From: Emma Foster <111466810+emmalynfoster@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:10:58 -0500 Subject: [PATCH] Resource Service and Tests (#41) * Implement Resource service, tests, and test data with 97% coverage. * Update slug service to return empty list and corresponding tests * Update resource update tests to grab resource by id from the db to check --- backend/services/resource.py | 45 ++++--- backend/test/conftest.py | 3 +- backend/test/services/fixtures.py | 8 +- backend/test/services/resource_test.py | 126 ++++++++++++++++++++ backend/test/services/resource_test_data.py | 22 +++- 5 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 backend/test/services/resource_test.py diff --git a/backend/services/resource.py b/backend/services/resource.py index 2648ad6..67959c7 100644 --- a/backend/services/resource.py +++ b/backend/services/resource.py @@ -35,17 +35,15 @@ class ResourceService: def create(self, user: User, resource: Resource) -> Resource: """ Creates a resource based on the input object and adds it to the table if the user has the right permissions. - Parameters: user: a valid User model representing the currently logged in User resource: Resource object to add to table - Returns: Resource: Object added to table """ - if resource.role != user.role or resource.group != user.group: + if user.role != UserTypeEnum.ADMIN: raise PermissionError( - "User does not have permission to add resources in this role or group." + "User does not have permission to add resources in this program." ) resource_entity = ResourceEntity.from_model(resource) @@ -57,14 +55,11 @@ class ResourceService: def get_by_id(self, user: User, id: int) -> Resource: """ Gets a resource based on the resource id that the user has access to - Parameters: user: a valid User model representing the currently logged in User id: int, the id of the resource - Returns: Resource - Raises: ResourceNotFoundException: If no resource is found with id """ @@ -72,8 +67,7 @@ class ResourceService: self._session.query(ResourceEntity) .filter( ResourceEntity.id == id, - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, + ResourceEntity.program.in_(user.program), ) .one_or_none() ) @@ -86,18 +80,15 @@ class ResourceService: def update(self, user: User, resource: ResourceEntity) -> Resource: """ Update the resource if the user has access - Parameters: user: a valid User model representing the currently logged in User resource (ResourceEntity): Resource to update - Returns: Resource: Updated resource object - Raises: ResourceNotFoundException: If no resource is found with the corresponding ID """ - if resource.role != user.role or resource.group != user.group: + if user.role != UserTypeEnum.ADMIN: raise PermissionError( "User does not have permission to update this resource." ) @@ -109,7 +100,11 @@ class ResourceService: f"No resource found with matching id: {resource.id}" ) - obj.update_from_model(resource) # Assuming an update method exists + obj.name = resource.name + obj.summary = resource.summary + obj.link = resource.link + obj.program = resource.program + self._session.commit() return obj.to_model() @@ -117,20 +112,21 @@ class ResourceService: def delete(self, user: User, id: int) -> None: """ Delete resource based on id that the user has access to - Parameters: user: a valid User model representing the currently logged in User id: int, a unique resource id - Raises: ResourceNotFoundException: If no resource is found with the corresponding id """ + if user.role != UserTypeEnum.ADMIN: + raise PermissionError( + "User does not have permission to delete this resource." + ) + resource = ( self._session.query(ResourceEntity) .filter( ResourceEntity.id == id, - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, ) .one_or_none() ) @@ -144,22 +140,21 @@ class ResourceService: def get_by_slug(self, user: User, search_string: str) -> list[Resource]: """ Get a list of resources given a search string that the user has access to - Parameters: user: a valid User model representing the currently logged in User search_string: a string to search resources by - Returns: list[Resource]: list of resources relating to the string - Raises: ResourceNotFoundException if no resource is found with the corresponding slug """ query = select(ResourceEntity).where( - ResourceEntity.title.ilike(f"%{search_string}%"), - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, + ResourceEntity.name.ilike(f"%{search_string}%"), + ResourceEntity.program.in_(user.program) ) entities = self._session.scalars(query).all() - return [entity.to_model() for entity in entities] + if not entities: + return [] + + return [entity.to_model() for entity in entities] \ No newline at end of file diff --git a/backend/test/conftest.py b/backend/test/conftest.py index b91a2ac..f231859 100644 --- a/backend/test/conftest.py +++ b/backend/test/conftest.py @@ -4,7 +4,7 @@ import pytest from sqlalchemy import Engine, create_engine, text from sqlalchemy.orm import Session from sqlalchemy.exc import OperationalError -from .services import user_test_data, tag_test_data, service_test_data +from .services import user_test_data, tag_test_data, service_test_data, resource_test_data from ..database import _engine_str from ..env import getenv @@ -57,5 +57,6 @@ def setup_insert_data_fixture(session: Session): user_test_data.insert_fake_data(session) tag_test_data.insert_fake_data(session) service_test_data.insert_fake_data(session) + resource_test_data.insert_fake_data(session) session.commit() yield diff --git a/backend/test/services/fixtures.py b/backend/test/services/fixtures.py index 9fb349a..213f1bf 100644 --- a/backend/test/services/fixtures.py +++ b/backend/test/services/fixtures.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from ...services import UserService from ...services import TagService from ...services import ServiceService +from ...services import ResourceService @@ -23,4 +24,9 @@ def tag_svc(session: Session): @pytest.fixture() def service_svc(session: Session): """This fixture is used to test the ServiceService class""" - return ServiceService(session) \ No newline at end of file + return ServiceService(session) + +@pytest.fixture() +def resource_svc(session: Session): + """This fixture is used to test the ResourceService class""" + return ResourceService(session) \ No newline at end of file diff --git a/backend/test/services/resource_test.py b/backend/test/services/resource_test.py new file mode 100644 index 0000000..7d9b007 --- /dev/null +++ b/backend/test/services/resource_test.py @@ -0,0 +1,126 @@ +from backend.models.user_model import User +from backend.entities.resource_entity import ResourceEntity +from ...models.enum_for_models import ProgramTypeEnum +from backend.services.resource import ResourceService +from backend.services.tag import TagService +from backend.services.exceptions import ResourceNotFoundException +from . import resource_test_data +from . import user_test_data +from .fixtures import resource_svc, user_svc, tag_svc +from backend.models.resource_model import Resource +import pytest + + +def test_get_resource_by_user_volunteer(resource_svc: ResourceService): + """ Test getting resources by a volunteer """ + resources = resource_svc.get_resource_by_user(user_test_data.volunteer) + assert len(resources) == 2 + assert isinstance(resources[0], Resource) + +def test_get_resources_admin(resource_svc: ResourceService): + """ Test getting resources by an admin """ + resources = resource_svc.get_resource_by_user(user_test_data.admin) + assert len(resources) == len(resource_test_data.resources) + assert isinstance(resources[0], Resource) + +def test_get_resources_employee(resource_svc: ResourceService): + """ Test getting by an employee """ + resources = resource_svc.get_resource_by_user(user_test_data.employee) + assert len(resources) == 5 + assert isinstance(resources[0], Resource) + +def test_create_resource_admin(resource_svc: ResourceService): + """ Test creating resources as an admin """ + resource = resource_svc.create(user_test_data.admin, resource_test_data.resource6) + assert resource.name == resource_test_data.resource6.name + assert isinstance(resource, Resource) + +def test_create_not_permitted(resource_svc: ResourceService): + """ Test creating resources without permission """ + with pytest.raises(PermissionError): + resource = resource_svc.create(user_test_data.volunteer, resource_test_data.resource1) + pytest.fail() + +def test_get_by_id(resource_svc: ResourceService): + """ Test getting a resource by id as an admin """ + test_resource = resource_test_data.resource1 + resource = resource_svc.get_by_id(user_test_data.admin, test_resource.id) + assert resource is not None + assert resource.id == test_resource.id + assert resource.name == test_resource.name + +def test_get_by_id_no_access(resource_svc: ResourceService): + """ Test getting a resourced with an id no accessible to an employee """ + test_resource = resource_test_data.resource2 + with pytest.raises(ResourceNotFoundException): + resource = resource_svc.get_by_id(user_test_data.employee, test_resource.id) + pytest.fail() + +def test_update(resource_svc: ResourceService): + """ Test updating a resource by an admin """ + updated_resource = resource_test_data.resource5_new + resource = resource_svc.update(user_test_data.admin, updated_resource) + db_resource = resource_svc.get_by_id(user_test_data.admin, resource.id) + assert resource.id == updated_resource.id + assert resource.name == updated_resource.name + assert resource.summary == updated_resource.summary + assert resource.link == updated_resource.link + assert resource.program == updated_resource.program + assert db_resource.id == updated_resource.id + assert db_resource.name == updated_resource.name + assert db_resource.summary == updated_resource.summary + assert db_resource.link == updated_resource.link + assert db_resource.program == updated_resource.program + + +def test_update_no_permission(resource_svc: ResourceService): + """ Test updating a resource without permission """ + with pytest.raises(PermissionError): + resource = resource_svc.update(user_test_data.employee, resource_test_data.resource5_new) + pytest.fail() + +def test_delete(resource_svc: ResourceService): + """ Test deleting a resource as an admin """ + resource_svc.delete(user_test_data.admin, resource_test_data.resource5.id) + resources = resource_svc.get_resource_by_user(user_test_data.admin) + assert len(resources) == len(resource_test_data.resources) - 1 + +def test_delete_no_permission(resource_svc: ResourceService): + """ Test deleting a resource with no permission """ + with pytest.raises(PermissionError): + resource = resource_svc.delete(user_test_data.employee, resource_test_data.resource5.id) + pytest.fail() + +def test_get_1_by_slug(resource_svc: ResourceService): + """ Test getting 1 resource with a specific search """ + resource_to_test = resource_test_data.resource1 + slug = "Resource 1" + resources = resource_svc.get_by_slug(user_test_data.admin, slug) + assert len(resources) == 1 + resource = resources[0] + assert resource.id == resource_to_test.id + assert resource.name == resource_to_test.name + assert resource.summary == resource_to_test.summary + assert resource.link == resource_to_test.link + assert resource.program == resource_to_test.program + +def test_get_by_slug(resource_svc: ResourceService): + """ Test a generic search to get all resources """ + slug = "Resource" + resources = resource_svc.get_by_slug(user_test_data.admin, slug) + assert len(resources) == 5 + +def test_get_by_slug_not_found(resource_svc: ResourceService): + """ Test getting a resource that does not exist """ + slug = "Not Found" + resources = resource_svc.get_by_slug(user_test_data.admin, slug) + assert len(resources) == 0 + assert resources == [] + + +def test_get_by_slug_no_permission(resource_svc: ResourceService): + """ Test getting a resource the user does not have access to """ + slug = "Resource 2" + resources = resource_svc.get_by_slug(user_test_data.employee, slug) + assert len(resources) == 0 + assert resources == [] \ No newline at end of file diff --git a/backend/test/services/resource_test_data.py b/backend/test/services/resource_test_data.py index bb39266..7634df7 100644 --- a/backend/test/services/resource_test_data.py +++ b/backend/test/services/resource_test_data.py @@ -50,6 +50,24 @@ resource5 = Resource( created_at=datetime(2023, 6, 5, 11, 30, 0), ) +resource6 = Resource( + id=6, + name="Resource 6", + summary="New Financial Resource", + link="https://example.com/resource6", + program=ProgramTypeEnum.ECONOMIC, + created_at=datetime(2024, 6, 5, 11, 30, 0), +) + +resource5_new = Resource( + id=5, + name="Resource 5", + summary = "Updated shelter and housing resources", + link="https://example.com/resource5/new", + program=ProgramTypeEnum.DOMESTIC, + created_at=datetime(2023, 6, 5, 11, 30, 0), +) + resources = [resource1, resource2, resource3, resource4, resource5] resource_1 = Resource( @@ -266,13 +284,11 @@ def reset_table_id_seq( next_id: int, ) -> None: """Reset the ID sequence of an entity table. - Args: session (Session) - A SQLAlchemy Session entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity_id_column (MappedColumn) - The ID column (should be an int column) next_id (int) - Where the next inserted, autogenerated ID should begin - Returns: None""" table = entity.__table__ @@ -312,4 +328,4 @@ def insert_fake_data(session: Session): reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1) # Commit all changes - session.commit() + session.commit() \ No newline at end of file