diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 52e2e60586dc46a45bef0533e695eed0b300ee1d..aa2dd0698a66b70d5cd73020ae3f8e19ccf0fdff 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -91,7 +91,7 @@ class InvitationView(CreateAPIView, DestroyAPIView): logger.warning( f"Attempting to delete invitation for already registered guest with person ID {person_id}" ) - return Response(status.HTTP_400_BAD_REQUEST) + return Response(status=status.HTTP_400_BAD_REQUEST) # Delete the person. The delete will cascade and all roles, identities and invitations will be removed. # It is OK to do this here since the person has not gone through the registration, so it is not @@ -237,6 +237,7 @@ class InvitedGuestView(GenericAPIView): person = invite_link.invitation.role.person data = request.data + fnr = data.get("person") and data["person"].get("fnr") # If there is a Feide ID registered with the guest, assume that the name is also coming from there feide_id = self._get_identity_or_none(person, Identity.IdentityType.FEIDE_ID) @@ -248,7 +249,7 @@ class InvitedGuestView(GenericAPIView): ): return Response(status=status.HTTP_400_BAD_REQUEST) - if self._verified_fnr_already_exists(person) and "fnr" in data: + if self._verified_fnr_already_exists(person) and fnr: # The user should not be allowed to change a verified fnr return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py index 8972a4679b727a0b96df890f422f50ee15e79de7..50ed292122d6e3b9fe71e715c8b9510475ebe926 100644 --- a/gregui/api/views/person.py +++ b/gregui/api/views/person.py @@ -56,7 +56,9 @@ class PersonView(APIView): def patch(self, request, id): person = Person.objects.get(id=id) # For now only the e-mail is allowed to be updated - email = request.data["email"] + email = request.data.get("email") + if not email: + return Response(status=status.HTTP_400_BAD_REQUEST) validate_email(email) # The following line will raise an exception if the e-mail is not valid create_identity_or_update(Identity.IdentityType.PRIVATE_EMAIL, email, person) diff --git a/gregui/tests/api/test_invitation.py b/gregui/tests/api/test_invitation.py index c63738805c13cf3d6fd726b2d7b415b30e9c4e55..a94f4daf9d98afefbb22e9429ec2e2dd3ddc5f8b 100644 --- a/gregui/tests/api/test_invitation.py +++ b/gregui/tests/api/test_invitation.py @@ -1,3 +1,5 @@ +import datetime +from django.utils import timezone import pytest from rest_framework import status @@ -8,14 +10,14 @@ from greg.models import InvitationLink, Person, Identity @pytest.mark.django_db -def test_get_invite(client): +def test_post_invite(client): """Forbid access with bad invitation link uuid""" response = client.post(reverse("gregui-v1:invite-verify"), data={"uuid": "baduuid"}) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.django_db -def test_get_invite_ok(client, invitation_link): +def test_post_invite_ok(client, invitation_link): """Access okay with valid invitation link""" response = client.post( reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid} @@ -24,7 +26,7 @@ def test_get_invite_ok(client, invitation_link): @pytest.mark.django_db -def test_get_invite_expired(client, invitation_link_expired): +def test_post_invite_expired(client, invitation_link_expired): """Forbid access with expired invite link""" response = client.post( reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link_expired.uuid} @@ -32,8 +34,22 @@ def test_get_invite_expired(client, invitation_link_expired): assert response.status_code == status.HTTP_403_FORBIDDEN +@pytest.mark.django_db +def test_post_missing_invite_id(client): + """Forbid access if no id provided.""" + response = client.post(reverse("gregui-v1:invite-verify")) + assert response.status_code == status.HTTP_403_FORBIDDEN + + @pytest.mark.django_db def test_get_invited_info_no_session(client, invitation_link): + """Forbid access if invite expired after session was created.""" + # get a session + client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid}) + # expire the invitation link + invitation_link.expire = timezone.now() - datetime.timedelta(days=1) + invitation_link.save() + # fail to get info because session has expired response = client.get(reverse("gregui-v1:invited-info")) assert response.status_code == status.HTTP_403_FORBIDDEN @@ -336,3 +352,41 @@ def test_name_update_allowed_if_feide_identity_is_not_present( person.refresh_from_db() assert person.first_name == "Someone" assert person.last_name == "Test" + + +@pytest.mark.django_db +def test_post_info_fail_fnr_already_verified(client, invited_person_verified_nin): + """Ensure that an automatically verified fnr cannot be changed""" + person, invitation_link = invited_person_verified_nin + fnr = "10093720895" + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + url = reverse("gregui-v1:invited-info") + data = {"person": {"mobile_phone": "+4797543992", "fnr": fnr}} + response = client.post(url, data, format="json") + + # Verify rejection + assert response.status_code == status.HTTP_400_BAD_REQUEST + # Verify fnr was not changed + person.refresh_from_db() + assert person.fnr.value != fnr + + +@pytest.mark.django_db +def test_post_info_fail_unknown_field(client, invited_person_verified_nin): + """Ensure that including an unknown field gives bad request""" + _, invitation_link = invited_person_verified_nin + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + url = reverse("gregui-v1:invited-info") + data = {"person": {"mobile_phone": "+4797543992", "badfield": "foo"}} + response = client.post(url, data, format="json") + + # Verify rejection + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/gregui/tests/api/test_invite_guest.py b/gregui/tests/api/test_invite_guest.py index c3c4d42646bfe240ed5ad8bd6fe1b2a0d2309ff3..5a7f8ebfb5c081fe23c2c71206eb009eeda71324 100644 --- a/gregui/tests/api/test_invite_guest.py +++ b/gregui/tests/api/test_invite_guest.py @@ -82,6 +82,30 @@ def test_invite_cancel(client, invitation_link, invitation, role, log_in, user_s assert InvitationLink.objects.filter(invitation__id=invitation.id).count() == 0 +@pytest.mark.django_db +def test_fail_delete_confirmed_invitation( + client, role, log_in, user_sponsor, sponsor_foo, invitation, invitation_link +): + log_in(user_sponsor) + + role = Role.objects.get(pk=role.id) + pe = Person.objects.get(pk=role.person_id) + pe.registration_completed_date = datetime.date.today() + pe.save() + assert pe.registration_completed_date + assert pe.is_registered + + url = reverse("gregui-v1:invitation") + response = client.delete(f"{url}?person_id={str(pe.id)}") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + # The role, invitation and connected links should still exist + assert Role.objects.filter(id=role.id).count() == 1 + assert Invitation.objects.filter(id=invitation.id).count() == 1 + assert InvitationLink.objects.filter(invitation__id=invitation.id).count() == 1 + + @pytest.mark.django_db def test_invite_resend_existing_invite_active( client, @@ -162,3 +186,74 @@ def test_invite_resend_existing_invite_not_active( InvitationLink.objects.get( invitation__role__person_id=person_invited.id, expire__gt=timezone.now() ) + + +@pytest.mark.django_db +def test_invite_resend_person_no_invites_fail( + client, log_in, user_sponsor, person_invited +): + """Resending an invite for a person without invites should return bad request.""" + log_in(user_sponsor) + + url = reverse("gregui-v1:invite-resend", kwargs={"person_id": person_invited.id}) + response = client.patch(url) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_invite_resend_person_multiple_links_send_all( + client, log_in, user_sponsor, invited_person, registration_template, mocker +): + """Resending an invite for a person with multiple working links should resend all of them.""" + send_invite_mock_function = mocker.patch( + "gregui.api.views.invitation.send_invite_mail" + ) + log_in(user_sponsor) + person, invitation = invited_person + invitation = Invitation.objects.filter(role__person_id=person.id).first() + InvitationLink.objects.create( + invitation=invitation, expire=timezone.now() + datetime.timedelta(days=1) + ) + InvitationLink.objects.create( + invitation=invitation, expire=timezone.now() + datetime.timedelta(days=1) + ) + + invitation_links_for_person = InvitationLink.objects.filter( + invitation__role__person_id=person.id + ) + assert invitation_links_for_person.count() == 3 + url = reverse("gregui-v1:invite-resend", kwargs={"person_id": person.id}) + response = client.patch(url) + assert response.status_code == status.HTTP_200_OK + assert send_invite_mock_function.call_count == 3 + + +@pytest.mark.django_db +def test_invite_resend_person_multiple_expired_links_send_all( + client, log_in, user_sponsor, invited_person, registration_template, mocker +): + """ + Resending invites for a person with multiple invitations where all links are + expired should resend all of them. + """ + send_invite_mock_function = mocker.patch( + "gregui.api.views.invitation.send_invite_mail" + ) + + log_in(user_sponsor) + # Expire invitation link + person, invitation = invited_person + invitation = Invitation.objects.filter(role__person_id=person.id).first() + link = InvitationLink.objects.get(invitation=invitation) + link.expire = timezone.now() - datetime.timedelta(days=1) + link.save() + # Make another invitation with an expired link + inv2 = Invitation.objects.create(role=invitation.role) + InvitationLink.objects.create( + invitation=inv2, expire=timezone.now() - datetime.timedelta(days=1) + ) + # Resend request should trigger creation of new links for both invitations + url = reverse("gregui-v1:invite-resend", kwargs={"person_id": person.id}) + response = client.patch(url) + assert response.status_code == status.HTTP_200_OK + assert send_invite_mock_function.call_count == 2 diff --git a/gregui/tests/api/test_person.py b/gregui/tests/api/test_person.py new file mode 100644 index 0000000000000000000000000000000000000000..4afb1f771111df9a3266ae98622b9b05d934a824 --- /dev/null +++ b/gregui/tests/api/test_person.py @@ -0,0 +1,44 @@ +import pytest +from rest_framework import status +from rest_framework.reverse import reverse + + +@pytest.mark.django_db +def test_get_person_fail(client): + """Anonymous user cannot get person info""" + url = reverse("gregui-v1:person-get", kwargs={"id": 1}) + response = client.get(url) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_get_person(client, log_in, user_sponsor, invited_person): + """Logged in sponsor can get person info""" + person, _ = invited_person + url = reverse("gregui-v1:person-get", kwargs={"id": person.id}) + log_in(user_sponsor) + response = client.get(url) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_patch_person_no_data_fail(client, log_in, user_sponsor, invited_person): + """No data in patch should fail""" + person, _ = invited_person + url = reverse("gregui-v1:person-get", kwargs={"id": person.id}) + log_in(user_sponsor) + response = client.patch(url) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_patch_person_new_email_ok(client, log_in, user_sponsor, invited_person): + """Logged in sponsor can update email address of person""" + person, _ = invited_person + url = reverse("gregui-v1:person-get", kwargs={"id": person.id}) + log_in(user_sponsor) + assert person.private_email.value == "foo@example.org" + response = client.patch(url, data={"email": "new@example.com"}) + assert response.status_code == status.HTTP_200_OK + person.refresh_from_db() + assert person.private_email.value == "new@example.com" diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index 53b04a306be928c28584d09270abf5d5bbf34a83..b7f6e8f6c8ce4d793ae5789361df4c7f3d49c3aa 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -385,6 +385,40 @@ def invited_person_no_ids( ) +@pytest.fixture +def invited_person_verified_nin( + create_person, + create_role, + create_invitation, + create_invitation_link, + sponsor_foo, + unit_foo, + role_type_foo, +) -> Tuple[Person, InvitationLink]: + """ + Invited person, with a verified NIN. + """ + person = create_person( + first_name="Victor", + last_name="Verified", + email="foo@bar2.com", + nin="12345678912", + ) + fnr = person.identities.get(type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER) + fnr.verified = Identity.Verified.AUTOMATIC + fnr.save() + + role = create_role( + person=person, sponsor=sponsor_foo, unit=unit_foo, role_type=role_type_foo + ) + + invitation = create_invitation(role=role) + invitation_link = create_invitation_link(invitation=invitation) + return Person.objects.get(id=person.id), InvitationLink.objects.get( + id=invitation_link.id + ) + + @pytest.fixture def log_in(client) -> Callable[[UserModel], APIClient]: def _log_in(user):