✨(api) allow invitations for domain management
add an endpoint to allow domain managers to invite someone on people, using their email address
This commit is contained in:
committed by
Sabrina Demagny
parent
9ee1ef5ba0
commit
2224acf12d
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Tests for DomainInvitations API endpoint in People's app mailbox_manager. Focus on "create" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager.api.client import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__create__anonymous():
|
||||
"""Anonymous users should not be able to create invitations."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation_values = serializers.DomainInvitationSerializer(
|
||||
factories.DomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_domain_invitations__create__authenticated_outsider():
|
||||
"""Users should not be permitted to send domain management invitations
|
||||
for a domain they don't manage."""
|
||||
user = core_factories.UserFactory()
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation_values = serializers.DomainInvitationSerializer(
|
||||
factories.DomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["owner", "administrator"],
|
||||
)
|
||||
def test_api_domain_invitations__admin_should_create_invites(role):
|
||||
"""Owners and administrators should be able to invite new domain managers."""
|
||||
user = core_factories.UserFactory()
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user, role=role)
|
||||
|
||||
invitation_values = serializers.DomainInvitationSerializer(
|
||||
factories.DomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
|
||||
def test_api_domain_invitations__viewers_should_not_invite_to_manage_domains():
|
||||
"""
|
||||
Domain viewers should not be able to invite new domain managers.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=domain, user=user, role=enums.MailDomainRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
invitation_values = serializers.DomainInvitationSerializer(
|
||||
factories.DomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "You are not allowed to manage invitations for this domain."
|
||||
}
|
||||
|
||||
|
||||
def test_api_domain_invitations__should_not_create_duplicate_invitations():
|
||||
"""An email should not be invited multiple times to the same domain."""
|
||||
existing_invitation = factories.DomainInvitationFactory()
|
||||
domain = existing_invitation.domain
|
||||
|
||||
# Grant privileged role on the domain to the user
|
||||
user = core_factories.UserFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=domain, user=user, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
# Create a new invitation to the same domain with the exact same email address
|
||||
duplicated_invitation = serializers.DomainInvitationSerializer(
|
||||
factories.DomainInvitationFactory.build(email=existing_invitation.email)
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
duplicated_invitation,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["__all__"] == [
|
||||
"Domain invitation with this Email address and Domain already exists."
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Tests for DomainInvitations API endpoint in People's app mailbox_manager. Focus on "list" action.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__anonymous_user_should_not_list_invitations():
|
||||
"""Anonymous users should not be able to list invitations."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
response = APIClient().get(f"/api/v1.0/mail-domains/{domain.slug}/invitations/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_domain_invitations__domain_managers_should_list_invitations():
|
||||
"""
|
||||
Authenticated user should be able to list invitations
|
||||
in domains they manage, including from other issuers.
|
||||
"""
|
||||
auth_user, other_user = core_factories.UserFactory.create_batch(2)
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=domain, user=auth_user, role=enums.MailDomainRoleChoices.ADMIN
|
||||
)
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=domain, user=other_user, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
invitation = factories.DomainInvitationFactory(
|
||||
domain=domain, role=enums.MailDomainRoleChoices.ADMIN, issuer=auth_user
|
||||
)
|
||||
other_invitations = factories.DomainInvitationFactory.create_batch(
|
||||
2, domain=domain, role=enums.MailDomainRoleChoices.VIEWER, issuer=other_user
|
||||
)
|
||||
|
||||
# expired invitations should be listed too
|
||||
# override settings to accelerate validation expiration
|
||||
settings.INVITATION_VALIDITY_DURATION = 1 # second
|
||||
expired_invitation = factories.DomainInvitationFactory(
|
||||
domain=domain, role=enums.MailDomainRoleChoices.VIEWER, issuer=auth_user
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
# invitations from other teams should not be listed
|
||||
other_domain = factories.MailDomainEnabledFactory()
|
||||
factories.DomainInvitationFactory.create_batch(
|
||||
2, domain=other_domain, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(auth_user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 4
|
||||
assert sorted(response.json()["results"], key=lambda x: x["created_at"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"created_at": i.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"email": str(i.email),
|
||||
"domain": str(domain.id),
|
||||
"role": str(i.role),
|
||||
"issuer": str(i.issuer.id),
|
||||
"is_expired": i.is_expired,
|
||||
}
|
||||
for i in [invitation, *other_invitations, expired_invitation]
|
||||
],
|
||||
key=lambda x: x["created_at"],
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Tests for DomainInvitations API endpoint. Focus on "retrieve" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__anonymous_user_should_not_retrieve_invitations():
|
||||
"""
|
||||
Anonymous user should not be able to retrieve invitations.
|
||||
"""
|
||||
|
||||
invitation = factories.DomainInvitationFactory()
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_domain_invitations__unrelated_user_should_not_retrieve_invitations():
|
||||
"""
|
||||
Authenticated unrelated users should not be able to retrieve invitations.
|
||||
"""
|
||||
auth_user = core_factories.UserFactory()
|
||||
invitation = factories.DomainInvitationFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(auth_user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 0
|
||||
|
||||
|
||||
def test_api_domain_invitations__domain_managers_should_list_invitations():
|
||||
"""
|
||||
Authenticated domain managers should be able to retrieve invitations
|
||||
whatever their role in the domain.
|
||||
"""
|
||||
auth_user = core_factories.UserFactory()
|
||||
invitation = factories.DomainInvitationFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, user=auth_user, role=enums.MailDomainRoleChoices.ADMIN
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(auth_user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(invitation.id),
|
||||
"created_at": invitation.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"email": invitation.email,
|
||||
"domain": str(invitation.domain.id),
|
||||
"role": str(invitation.role),
|
||||
"issuer": str(invitation.issuer.id),
|
||||
"is_expired": False,
|
||||
}
|
||||
@@ -3,6 +3,21 @@
|
||||
|
||||
import json
|
||||
|
||||
## USERS
|
||||
|
||||
|
||||
def response_user_created(user_sub):
|
||||
"""mimic dimail response upon succesfull user creation."""
|
||||
return json.dumps(
|
||||
{
|
||||
"name": user_sub,
|
||||
"is_admin": "false",
|
||||
"uuid": "user-uuid-on-dimail",
|
||||
"perms": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
## DOMAINS
|
||||
|
||||
CHECK_DOMAIN_BROKEN = {
|
||||
@@ -278,6 +293,15 @@ DOMAIN_SPEC = [
|
||||
|
||||
TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
|
||||
|
||||
## ALLOWS
|
||||
|
||||
|
||||
def response_allows_created(user_name, domain_name):
|
||||
"""mimic dimail response upon succesfull allows creation.
|
||||
Dimail expects a name but our names are ProConnect's uuids."""
|
||||
return json.dumps({"user": user_name, "domain": domain_name})
|
||||
|
||||
|
||||
## MAILBOXES
|
||||
|
||||
|
||||
|
||||
103
src/backend/mailbox_manager/tests/models/test_invitations.py
Normal file
103
src/backend/mailbox_manager/tests/models/test_invitations.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Unit tests for the Domain Invitation model
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import exceptions
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from freezegun import freeze_time
|
||||
from rest_framework import status
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager.tests.fixtures import dimail
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_models_domain_invitations_readonly_after_create():
|
||||
"""Existing invitations should be readonly."""
|
||||
invitation = factories.DomainInvitationFactory()
|
||||
with pytest.raises(exceptions.PermissionDenied):
|
||||
invitation.save()
|
||||
|
||||
|
||||
def test_models_domain_invitations__is_expired():
|
||||
"""
|
||||
The 'is_expired' property should return False until validity duration
|
||||
is exceeded and True afterwards.
|
||||
"""
|
||||
expired_invitation = factories.DomainInvitationFactory()
|
||||
assert expired_invitation.is_expired is False
|
||||
|
||||
settings.INVITATION_VALIDITY_DURATION = 1
|
||||
time.sleep(1)
|
||||
|
||||
assert expired_invitation.is_expired is True
|
||||
|
||||
|
||||
def test_models_domain_invitation__should_convert_invitations_to_accesses_upon_joining():
|
||||
"""
|
||||
Upon creating a new user, domain invitations linked to that email
|
||||
should be converted to accesses and then deleted.
|
||||
"""
|
||||
# Two invitations to the same mail but to different domains
|
||||
email = "future_admin@example.com"
|
||||
invitation_to_domain1 = factories.DomainInvitationFactory(
|
||||
email=email, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
invitation_to_domain2 = factories.DomainInvitationFactory(email=email)
|
||||
|
||||
# an expired invitation that should not be converted
|
||||
with freeze_time("1985-10-30"):
|
||||
expired_invitation = factories.DomainInvitationFactory(email=email)
|
||||
|
||||
# another person invited to domain2
|
||||
other_invitation = factories.DomainInvitationFactory(
|
||||
domain=invitation_to_domain2.domain
|
||||
)
|
||||
|
||||
new_user = core_factories.UserFactory.build(email=email)
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/users/"),
|
||||
body=dimail.response_user_created("sub"),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/allows/"),
|
||||
body=dimail.response_allows_created(
|
||||
"sub", invitation_to_domain1.domain.name
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
new_user = core_factories.UserFactory(email=email)
|
||||
|
||||
assert models.MailDomainAccess.objects.filter(
|
||||
domain=invitation_to_domain1.domain, user=new_user
|
||||
).exists()
|
||||
assert models.MailDomainAccess.objects.filter(
|
||||
domain=invitation_to_domain2.domain, user=new_user
|
||||
).exists()
|
||||
assert not models.DomainInvitation.objects.filter(
|
||||
domain=invitation_to_domain1.domain, email=email
|
||||
).exists() # invitation "consumed"
|
||||
assert not models.DomainInvitation.objects.filter(
|
||||
domain=invitation_to_domain2.domain, email=email
|
||||
).exists() # invitation "consumed"
|
||||
assert models.DomainInvitation.objects.filter(
|
||||
domain=expired_invitation.domain, email=email
|
||||
).exists() # expired invitation remains
|
||||
assert models.DomainInvitation.objects.filter(
|
||||
domain=invitation_to_domain2.domain, email=other_invitation.email
|
||||
).exists() # the other invitation remains
|
||||
Reference in New Issue
Block a user