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