From c7b19419d7ca7a64649ef217adf1ed0d767802a3 Mon Sep 17 00:00:00 2001 From: Andreas Ellewsen <ae@uio.no> Date: Wed, 17 Nov 2021 09:43:03 +0100 Subject: [PATCH] Add more tests for gregui Adding tests for a serializer prompted moving of view tests to their own subdirectory. --- gregui/tests/api/serializers/__init__.py | 0 gregui/tests/api/serializers/test_role.py | 209 ++++++++++++++++++ gregui/tests/api/views/__init__.py | 0 .../tests/api/{ => views}/test_invitation.py | 0 .../api/{ => views}/test_invite_guest.py | 0 gregui/tests/api/{ => views}/test_person.py | 0 gregui/tests/api/views/test_role.py | 139 ++++++++++++ gregui/tests/api/views/test_userinfo.py | 78 +++++++ gregui/tests/conftest.py | 24 ++ gregui/urls.py | 2 +- 10 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 gregui/tests/api/serializers/__init__.py create mode 100644 gregui/tests/api/serializers/test_role.py create mode 100644 gregui/tests/api/views/__init__.py rename gregui/tests/api/{ => views}/test_invitation.py (100%) rename gregui/tests/api/{ => views}/test_invite_guest.py (100%) rename gregui/tests/api/{ => views}/test_person.py (100%) create mode 100644 gregui/tests/api/views/test_role.py create mode 100644 gregui/tests/api/views/test_userinfo.py diff --git a/gregui/tests/api/serializers/__init__.py b/gregui/tests/api/serializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gregui/tests/api/serializers/test_role.py b/gregui/tests/api/serializers/test_role.py new file mode 100644 index 00000000..b36c2b7c --- /dev/null +++ b/gregui/tests/api/serializers/test_role.py @@ -0,0 +1,209 @@ +import datetime +import re + +import pytest +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from gregui.api.serializers.role import RoleSerializerUi + + +@pytest.mark.django_db +def test_minimum_ok(role, sponsor_foo): + """The minimum amount of fields works""" + ser = RoleSerializerUi( + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": None, + "end_date": (timezone.now() + datetime.timedelta(days=10)).date(), + }, + context={"sponsor": sponsor_foo}, + ) + assert ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_start_date_past_fail(role, sponsor_foo): + """Should fail because of start_date in the past""" + ser = RoleSerializerUi( + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": (timezone.now() - datetime.timedelta(days=10)).date(), + "end_date": (timezone.now() + datetime.timedelta(days=10)).date(), + }, + context={"sponsor": sponsor_foo}, + ) + with pytest.raises( + ValidationError, + match=re.escape( + "{'start_date': [ErrorDetail(string='Start date cannot be in the past', code='invalid')]}" + ), + ): + ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_end_date_past_fail(role, sponsor_foo): + """Should fail because of end_date in the past""" + ser = RoleSerializerUi( + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today(), + "end_date": (timezone.now() - datetime.timedelta(days=10)).date(), + }, + context={"sponsor": sponsor_foo}, + ) + with pytest.raises( + ValidationError, + match=re.escape( + "{'end_date': [ErrorDetail(string='End date cannot be in the past', code='invalid')]}" + ), + ): + ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_end_date_expired_role_fail(role, sponsor_foo): + """New end date fail because role has ended""" + # Expire the role to ensure failure + role.end_date = datetime.date.today() - datetime.timedelta(days=10) + role.save() + # Try to change it + ser = RoleSerializerUi( + instance=role, + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today(), + "end_date": (timezone.now() + datetime.timedelta(days=10)).date(), + }, + context={"sponsor": sponsor_foo}, + ) + # Verify that a validation error is raised + with pytest.raises( + ValidationError, + match=re.escape( + "{'end_date': [ErrorDetail(string='Role has ended, cannot change end date', code='invalid')]}" + ), + ): + ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_wrong_sponsor(role, sponsor_foo, sponsor_bar): + """Touching another sponsor's roles does not work""" + # Try to touch sponsor_foo's guest role as sponsor_bar + ser = RoleSerializerUi( + instance=role, + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today(), + "end_date": (timezone.now() + datetime.timedelta(days=10)).date(), + }, + context={"sponsor": sponsor_bar}, + ) + # Verify that a validation error is raised + with pytest.raises( + ValidationError, + match=re.escape( + "{'non_field_errors': [ErrorDetail(string='You can only edit your own roles.', code='invalid')]}" + ), + ): + ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_too_future_end_date(role, sponsor_foo): + """Setting the end date further than max_days of role_type fails""" + max_future = timezone.now().date() + datetime.timedelta(days=role.type.max_days) + ser = RoleSerializerUi( + instance=role, + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today(), + "end_date": max_future + datetime.timedelta(days=1), + }, + context={"sponsor": sponsor_foo}, + ) + # Verify that a validation error is raised + with pytest.raises( + ValidationError, + match=re.escape( + "".join( + [ + "{'non_field_errors': [ErrorDetail(string=", + f"'New end date too far into the future for this type. Must be before {max_future}.'", + ", code='invalid')]}", + ] + ) + ), + ): + ser.is_valid(raise_exception=True) + + +@pytest.mark.django_db +def test_end_before_start(role, sponsor_foo): + """Setting the end date before start date not allowed""" + # Existing instance + ser = RoleSerializerUi( + instance=role, + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today() + datetime.timedelta(days=1), + "end_date": datetime.date.today(), + }, + context={"sponsor": sponsor_foo}, + ) + # Verify that a validation error is raised + with pytest.raises( + ValidationError, + match=re.escape( + "".join( + [ + "{'non_field_errors': [ErrorDetail(string=", + "'End date cannot be before start date.'", + ", code='invalid')]}", + ] + ) + ), + ): + ser.is_valid(raise_exception=True) + + # New instance + ser = RoleSerializerUi( + data={ + "person": role.person.id, + "orgunit": role.orgunit.id, + "type": role.type.id, + "start_date": datetime.date.today() + datetime.timedelta(days=1), + "end_date": datetime.date.today(), + }, + context={"sponsor": sponsor_foo}, + ) + # Verify that a validation error is raised + with pytest.raises( + ValidationError, + match=re.escape( + "".join( + [ + "{'non_field_errors': [ErrorDetail(string=", + "'End date cannot be before start date.'", + ", code='invalid')]}", + ] + ) + ), + ): + ser.is_valid(raise_exception=True) diff --git a/gregui/tests/api/views/__init__.py b/gregui/tests/api/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gregui/tests/api/test_invitation.py b/gregui/tests/api/views/test_invitation.py similarity index 100% rename from gregui/tests/api/test_invitation.py rename to gregui/tests/api/views/test_invitation.py diff --git a/gregui/tests/api/test_invite_guest.py b/gregui/tests/api/views/test_invite_guest.py similarity index 100% rename from gregui/tests/api/test_invite_guest.py rename to gregui/tests/api/views/test_invite_guest.py diff --git a/gregui/tests/api/test_person.py b/gregui/tests/api/views/test_person.py similarity index 100% rename from gregui/tests/api/test_person.py rename to gregui/tests/api/views/test_person.py diff --git a/gregui/tests/api/views/test_role.py b/gregui/tests/api/views/test_role.py new file mode 100644 index 00000000..3b740f9e --- /dev/null +++ b/gregui/tests/api/views/test_role.py @@ -0,0 +1,139 @@ +import datetime +import pytest +from django.utils import timezone +from rest_framework.reverse import reverse +from rest_framework import status + +from greg.models import OrganizationalUnit + + +@pytest.mark.django_db +def test_role_anon_post(client): + """Should get 403 forbidden since we are not a sponsor""" + resp = client.post(reverse("gregui-v1:role-list"), data={}) + assert resp.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_role_anon_patch(client): + """Should get 403 forbidden since we are not a sponsor""" + resp = client.patch(reverse("gregui-v1:role-detail", kwargs={"pk": 1}), data={}) + assert resp.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_role_sponsor_post_ok(client, log_in, user_sponsor, role): + """Should work since we are a sponsor at this unit""" + log_in(user_sponsor) + resp = client.post( + reverse("gregui-v1:role-list"), + data={ + "person": role.person.id, + "type": role.type.id, + "orgunit": role.orgunit.id, + "start_date": "", + "end_date": (timezone.now() + datetime.timedelta(days=10)).strftime( + "%Y-%m-%d" + ), + }, + ) + assert resp.content == b"" + assert resp.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +def test_role_sponsor_post_no_data_fail(client, log_in, user_sponsor): + """Should fail since we did not provide any role data""" + log_in(user_sponsor) + resp = client.post(reverse("gregui-v1:role-list"), data={}) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_role_sponsor_post_fail(client, log_in, user_sponsor, role): + """Should fail since we are not a sponsor at this unit.""" + # Unit the sponsor is not connected to so that we fail + ou = OrganizationalUnit.objects.create(name_nb="foo", name_en="foo_en") + + log_in(user_sponsor) + resp = client.post( + reverse("gregui-v1:role-list"), + data={ + "person": role.person.id, + "type": role.type.id, + "orgunit": ou.id, + "start_date": "", + "end_date": (timezone.now() + datetime.timedelta(days=10)).strftime( + "%Y-%m-%d" + ), + }, + ) + assert ( + resp.content + == b'{"orgunit":["A sponsor can only make changes to roles at units they are sponsors for."]}' + ) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_role_sponsor_patch_ok( + client, + log_in, + user_sponsor, + role, +): + """Should work since we are the sponsor owning the role.""" + log_in(user_sponsor) + new_date = (timezone.now() + datetime.timedelta(days=10)).date() + + resp = client.patch( + reverse("gregui-v1:role-detail", kwargs={"pk": role.id}), + data={ + "end_date": new_date.strftime("%Y-%m-%d"), + }, + ) + assert resp.status_code == status.HTTP_200_OK + # Verify the new date was set + role.refresh_from_db() + assert role.end_date == new_date + + +@pytest.mark.django_db +def test_role_sponsor_patch_fail(client, log_in, user_sponsor, role): + """Should fail since we are not a sponsor at this unit.""" + + # Unit the sponsor is not connected to so that we fail + ou = OrganizationalUnit.objects.create(name_nb="foo", name_en="foo_en") + + log_in(user_sponsor) + resp = client.patch( + reverse("gregui-v1:role-detail", kwargs={"pk": role.id}), + data={ + "orgunit": ou.id, + }, + ) + assert ( + resp.content + == b'{"orgunit":["A sponsor can only make changes to roles at units they are sponsors for."]}' + ) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_role_sponsor_patch_fail_unknown_role( + client, + log_in, + user_sponsor, +): + """Should fail since the role does not exist""" + log_in(user_sponsor) + new_date = (timezone.now() + datetime.timedelta(days=10)).date() + + resp = client.patch( + reverse("gregui-v1:role-detail", kwargs={"pk": 1}), + data={ + "end_date": new_date.strftime("%Y-%m-%d"), + }, + ) + assert resp.content == b"" + assert resp.status_code == status.HTTP_400_BAD_REQUEST diff --git a/gregui/tests/api/views/test_userinfo.py b/gregui/tests/api/views/test_userinfo.py new file mode 100644 index 00000000..c5caaa30 --- /dev/null +++ b/gregui/tests/api/views/test_userinfo.py @@ -0,0 +1,78 @@ +import pytest +from rest_framework.reverse import reverse +from rest_framework import status + + +@pytest.mark.django_db +def test_userinfo_anon_get(client): + """Anonymous people should be forbidden.""" + response = client.get(reverse("api-userinfo")) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_userinfo_invited_get(client, invitation_link): + """Invited guests should get info about themself and the role.""" + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + response = client.get(reverse("api-userinfo")) + assert response.status_code == status.HTTP_200_OK + assert response.json() == { + "feide_id": None, + "sponsor_id": None, + "person_id": 1, + "first_name": "Foo", + "last_name": "Bar", + "email": "foo@example.org", + "mobile_phone": None, + "fnr": None, + "passport": None, + "roles": [ + { + "id": 1, + "ou_nb": "Foo NB", + "ou_en": "Foo EN", + "name_nb": "Role Foo NB", + "name_en": "Role Foo EN", + "start_date": None, + "end_date": "2050-10-15", + "sponsor": {"first_name": "Sponsor", "last_name": "Bar"}, + } + ], + } + + +@pytest.mark.django_db +def test_userinfo_sponsor_get(client, log_in, user_sponsor): + """Sponsors should get info about themselves""" + log_in(user_sponsor) + + response = client.get(reverse("api-userinfo")) + assert response.status_code == status.HTTP_200_OK + assert response.json() == { + "feide_id": "", + "person_id": None, + "roles": [], + "sponsor_id": 1, + } + + +@pytest.mark.django_db +def test_userinfo_guest_get(client, log_in, user_person): + """Logged in guests should get info about themself""" + log_in(user_person) + response = client.get(reverse("api-userinfo")) + assert response.status_code == status.HTTP_200_OK + assert response.json() == { + "feide_id": "", + "sponsor_id": None, + "person_id": 1, + "roles": [], + "first_name": "Foo", + "last_name": "Bar", + "email": "foo@bar.com", + "mobile_phone": None, + "fnr": "123456*****", + "passport": None, + } diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index b7f6e8f6..3132ea8c 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -109,6 +109,13 @@ def sponsor_foo( return create_sponsor(**sponsor_foo_data, unit=unit_foo) +@pytest.fixture +def sponsor_bar(unit_foo: OrganizationalUnit, create_sponsor) -> Sponsor: + return create_sponsor( + feide_id="bar@example.com", first_name="Bar", last_name="Baz", unit=unit_foo + ) + + @pytest.fixture def create_user() -> Callable[[str, str, str, str], UserModel]: user_model = get_user_model() @@ -144,6 +151,23 @@ def user_sponsor(sponsor_foo: Sponsor, create_user) -> User: return user_model.objects.get(id=user.id) +@pytest.fixture +def user_person(person_foo: Sponsor, create_user) -> User: + user_model = get_user_model() + + # Create a user and link him to a sponsor + user = create_user( + username="test_person", + email="person@example.org", + first_name="Test", + last_name="Person", + ) + GregUserProfile.objects.create(user=user, person=person_foo) + + # This user is a sponsor for unit_foo + return user_model.objects.get(id=user.id) + + @pytest.fixture def create_greg_user_profile() -> Callable[ [UserModel, Optional[Person], Optional[Sponsor]], GregUserProfile diff --git a/gregui/urls.py b/gregui/urls.py index cc6fa884..c8a47cbf 100644 --- a/gregui/urls.py +++ b/gregui/urls.py @@ -19,7 +19,7 @@ urlpatterns: List[URLResolver] = [ path("api/ui/v1/session/", views.SessionView.as_view(), name="api-session"), path("api/ui/v1/testmail/", views.send_test_email, name="api-testmail"), path("api/ui/v1/whoami/", views.WhoAmIView.as_view(), name="api-whoami"), - path("api/ui/v1/userinfo/", UserInfoView.as_view()), # type: ignore + path("api/ui/v1/userinfo/", UserInfoView.as_view(), name="api-userinfo"), # type: ignore path("api/ui/v1/ous/", OusView.as_view()), path("api/ui/v1/guests/", GuestInfoView.as_view()), ] -- GitLab