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
This commit is contained in:
Emma Foster 2024-11-04 15:10:58 -05:00 committed by GitHub
parent 2e0dd3b987
commit 99e43c7b30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 174 additions and 30 deletions

View File

@ -35,17 +35,15 @@ class ResourceService:
def create(self, user: User, resource: Resource) -> Resource: 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. Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
resource: Resource object to add to table resource: Resource object to add to table
Returns: Returns:
Resource: Object added to table Resource: Object added to table
""" """
if resource.role != user.role or resource.group != user.group: if user.role != UserTypeEnum.ADMIN:
raise PermissionError( 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) resource_entity = ResourceEntity.from_model(resource)
@ -57,14 +55,11 @@ class ResourceService:
def get_by_id(self, user: User, id: int) -> Resource: def get_by_id(self, user: User, id: int) -> Resource:
""" """
Gets a resource based on the resource id that the user has access to Gets a resource based on the resource id that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
id: int, the id of the resource id: int, the id of the resource
Returns: Returns:
Resource Resource
Raises: Raises:
ResourceNotFoundException: If no resource is found with id ResourceNotFoundException: If no resource is found with id
""" """
@ -72,8 +67,7 @@ class ResourceService:
self._session.query(ResourceEntity) self._session.query(ResourceEntity)
.filter( .filter(
ResourceEntity.id == id, ResourceEntity.id == id,
ResourceEntity.role == user.role, ResourceEntity.program.in_(user.program),
ResourceEntity.group == user.group,
) )
.one_or_none() .one_or_none()
) )
@ -86,18 +80,15 @@ class ResourceService:
def update(self, user: User, resource: ResourceEntity) -> Resource: def update(self, user: User, resource: ResourceEntity) -> Resource:
""" """
Update the resource if the user has access Update the resource if the user has access
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
resource (ResourceEntity): Resource to update resource (ResourceEntity): Resource to update
Returns: Returns:
Resource: Updated resource object Resource: Updated resource object
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID 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( raise PermissionError(
"User does not have permission to update this resource." "User does not have permission to update this resource."
) )
@ -109,7 +100,11 @@ class ResourceService:
f"No resource found with matching id: {resource.id}" 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() self._session.commit()
return obj.to_model() return obj.to_model()
@ -117,20 +112,21 @@ class ResourceService:
def delete(self, user: User, id: int) -> None: def delete(self, user: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
id: int, a unique resource id id: int, a unique resource id
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding id 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 = ( resource = (
self._session.query(ResourceEntity) self._session.query(ResourceEntity)
.filter( .filter(
ResourceEntity.id == id, ResourceEntity.id == id,
ResourceEntity.role == user.role,
ResourceEntity.group == user.group,
) )
.one_or_none() .one_or_none()
) )
@ -144,22 +140,21 @@ class ResourceService:
def get_by_slug(self, user: User, search_string: str) -> list[Resource]: 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 Get a list of resources given a search string that the user has access to
Parameters: Parameters:
user: a valid User model representing the currently logged in User user: a valid User model representing the currently logged in User
search_string: a string to search resources by search_string: a string to search resources by
Returns: Returns:
list[Resource]: list of resources relating to the string list[Resource]: list of resources relating to the string
Raises: Raises:
ResourceNotFoundException if no resource is found with the corresponding slug ResourceNotFoundException if no resource is found with the corresponding slug
""" """
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
ResourceEntity.title.ilike(f"%{search_string}%"), ResourceEntity.name.ilike(f"%{search_string}%"),
ResourceEntity.role == user.role, ResourceEntity.program.in_(user.program)
ResourceEntity.group == user.group,
) )
entities = self._session.scalars(query).all() 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]

View File

@ -4,7 +4,7 @@ import pytest
from sqlalchemy import Engine, create_engine, text from sqlalchemy import Engine, create_engine, text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.exc import OperationalError 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 ..database import _engine_str
from ..env import getenv from ..env import getenv
@ -57,5 +57,6 @@ def setup_insert_data_fixture(session: Session):
user_test_data.insert_fake_data(session) user_test_data.insert_fake_data(session)
tag_test_data.insert_fake_data(session) tag_test_data.insert_fake_data(session)
service_test_data.insert_fake_data(session) service_test_data.insert_fake_data(session)
resource_test_data.insert_fake_data(session)
session.commit() session.commit()
yield yield

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from ...services import UserService from ...services import UserService
from ...services import TagService from ...services import TagService
from ...services import ServiceService from ...services import ServiceService
from ...services import ResourceService
@ -23,4 +24,9 @@ def tag_svc(session: Session):
@pytest.fixture() @pytest.fixture()
def service_svc(session: Session): def service_svc(session: Session):
"""This fixture is used to test the ServiceService class""" """This fixture is used to test the ServiceService class"""
return ServiceService(session) return ServiceService(session)
@pytest.fixture()
def resource_svc(session: Session):
"""This fixture is used to test the ResourceService class"""
return ResourceService(session)

View File

@ -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 == []

View File

@ -50,6 +50,24 @@ resource5 = Resource(
created_at=datetime(2023, 6, 5, 11, 30, 0), 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] resources = [resource1, resource2, resource3, resource4, resource5]
resource_1 = Resource( resource_1 = Resource(
@ -266,13 +284,11 @@ def reset_table_id_seq(
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
@ -312,4 +328,4 @@ def insert_fake_data(session: Session):
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1) reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()