✨(models/api) allow inviting external users to a document by their email
We want to be able to share a document with a person even if this person does not have an account in impress yet. This code is ported from https://github.com/numerique-gouv/people.
This commit is contained in:
committed by
Samuel Paccoud
parent
125284456f
commit
515b686795
494
src/backend/core/tests/test_api_document_invitations.py
Normal file
494
src/backend/core/tests/test_api_document_invitations.py
Normal file
@@ -0,0 +1,494 @@
|
||||
"""
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
import random
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_document_invitations__create__anonymous():
|
||||
"""Anonymous users should not be able to create invitations."""
|
||||
document = factories.DocumentFactory()
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/documents/{document.id}/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_document_invitations__create__authenticated_outsider():
|
||||
"""Users outside of document should not be permitted to invite to document."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"inviting,invited,is_allowed",
|
||||
(
|
||||
["member", "member", False],
|
||||
["member", "administrator", False],
|
||||
["member", "owner", False],
|
||||
["administrator", "member", True],
|
||||
["administrator", "administrator", True],
|
||||
["administrator", "owner", False],
|
||||
["owner", "member", True],
|
||||
["owner", "administrator", True],
|
||||
["owner", "owner", True],
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__create__privileged_members(
|
||||
via, inviting, invited, is_allowed, mock_user_get_teams
|
||||
):
|
||||
"""
|
||||
Only owners and administrators should be able to invite new users.
|
||||
Only owners can invite owners.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=inviting)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=inviting
|
||||
)
|
||||
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": invited,
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
if is_allowed:
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert models.Invitation.objects.count() == 1
|
||||
else:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Invitation.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_document_invitations__create__issuer_and_document_override():
|
||||
"""It should not be possible to set the "document" and "issuer" fields."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
other_document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
invitation_values = {
|
||||
"document": str(other_document.id),
|
||||
"issuer": str(factories.UserFactory().id),
|
||||
"email": "guest@example.com",
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
# document and issuer automatically set
|
||||
assert response.json()["document"] == str(document.id)
|
||||
assert response.json()["issuer"] == str(user.id)
|
||||
|
||||
|
||||
def test_api_document_invitations__create__cannot_duplicate_invitation():
|
||||
"""An email should not be invited multiple times to the same document."""
|
||||
existing_invitation = factories.InvitationFactory()
|
||||
document = existing_invitation.document
|
||||
|
||||
# Grant privileged role on the Document to the user
|
||||
user = factories.UserFactory()
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document, user=user, role="administrator"
|
||||
)
|
||||
|
||||
# Create a new invitation to the same document with the exact same email address
|
||||
invitation_values = {
|
||||
"email": existing_invitation.email,
|
||||
"role": random.choice(["administrator", "member"]),
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["__all__"] == [
|
||||
"Document invitation with this Email address and Document already exists."
|
||||
]
|
||||
|
||||
|
||||
def test_api_document_invitations__create__cannot_invite_existing_users():
|
||||
"""
|
||||
It should not be possible to invite already existing users.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
existing_user = factories.UserFactory()
|
||||
|
||||
# Build an invitation to the email of an exising identity in the db
|
||||
invitation_values = {
|
||||
"email": existing_user.email,
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["email"] == [
|
||||
"This email is already associated to a registered user."
|
||||
]
|
||||
|
||||
|
||||
def test_api_document_invitations__list__anonymous_user():
|
||||
"""Anonymous users should not be able to list invitations."""
|
||||
document = factories.DocumentFactory()
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id}/invitations/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__list__authenticated(
|
||||
via, mock_user_get_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to list invitations for documents to which they are
|
||||
related, whatever the role and including invitations issued by other users.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
role = random.choice(models.RoleChoices.choices)[0]
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(
|
||||
document=document, role="administrator", issuer=user
|
||||
)
|
||||
other_invitations = factories.InvitationFactory.create_batch(
|
||||
2, document=document, role="member", issuer=other_user
|
||||
)
|
||||
|
||||
# invitations from other documents should not be listed
|
||||
other_document = factories.DocumentFactory()
|
||||
factories.InvitationFactory.create_batch(2, document=other_document, role="member")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 3
|
||||
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),
|
||||
"document": str(document.id),
|
||||
"role": i.role,
|
||||
"issuer": str(i.issuer.id),
|
||||
"is_expired": False,
|
||||
"abilities": {
|
||||
"destroy": role in ["administrator", "owner"],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
}
|
||||
for i in [invitation, *other_invitations]
|
||||
],
|
||||
key=lambda x: x["created_at"],
|
||||
)
|
||||
|
||||
|
||||
def test_api_document_invitations__list__expired_invitations_still_listed(settings):
|
||||
"""
|
||||
Expired invitations are still listed.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
users=[(user, "administrator"), (other_user, "owner")]
|
||||
)
|
||||
|
||||
# override settings to accelerate validation expiration
|
||||
settings.INVITATION_VALIDITY_DURATION = 1 # second
|
||||
expired_invitation = factories.InvitationFactory(
|
||||
document=document,
|
||||
role="member",
|
||||
issuer=user,
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 1
|
||||
assert sorted(response.json()["results"], key=lambda x: x["created_at"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(expired_invitation.id),
|
||||
"created_at": expired_invitation.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"email": str(expired_invitation.email),
|
||||
"document": str(document.id),
|
||||
"role": expired_invitation.role,
|
||||
"issuer": str(expired_invitation.issuer.id),
|
||||
"is_expired": True,
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
},
|
||||
],
|
||||
key=lambda x: x["created_at"],
|
||||
)
|
||||
|
||||
|
||||
def test_api_document_invitations__retrieve__anonymous_user():
|
||||
"""
|
||||
Anonymous users should not be able to retrieve invitations.
|
||||
"""
|
||||
|
||||
invitation = factories.InvitationFactory()
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_document_invitations__retrieve__unrelated_user():
|
||||
"""
|
||||
Authenticated unrelated users should not be able to retrieve invitations.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__retrieve__document_member(via, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated users related to the document should be able to retrieve invitations
|
||||
whatever their role in the document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory()
|
||||
role = random.choice(models.RoleChoices.choices)[0]
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role=role
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{invitation.document.id}/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,
|
||||
"document": str(invitation.document.id),
|
||||
"role": str(invitation.role),
|
||||
"issuer": str(invitation.issuer.id),
|
||||
"is_expired": False,
|
||||
"abilities": {
|
||||
"destroy": role in ["administrator", "owner"],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize(
|
||||
"method",
|
||||
["put", "patch"],
|
||||
)
|
||||
def test_api_document_invitations__update__forbidden(method, via, mock_user_get_teams):
|
||||
"""
|
||||
Update of invitations is currently forbidden.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||
if method == "put":
|
||||
response = client.put(url)
|
||||
if method == "patch":
|
||||
response = client.patch(url)
|
||||
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json()["detail"] == f'Method "{method.upper()}" not allowed.'
|
||||
|
||||
|
||||
def test_api_document_invitations__delete__anonymous():
|
||||
"""Anonymous user should not be able to delete invitations."""
|
||||
invitation = factories.InvitationFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_document_invitations__delete__authenticated_outsider():
|
||||
"""Members unrelated to a document should not be allowed to cancel invitations."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", ["owner", "administrator"])
|
||||
def test_api_document_invitations__delete__privileged_members(
|
||||
role, via, mock_user_get_teams
|
||||
):
|
||||
"""Privileged member should be able to cancel invitation."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__delete__members(via, mock_user_get_teams):
|
||||
"""Member should not be able to cancel invitation."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="member"
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "You do not have permission to perform this action."
|
||||
)
|
||||
312
src/backend/core/tests/test_models_invitations.py
Normal file
312
src/backend/core/tests/test_models_invitations.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
import time
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core import exceptions, mail
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
from freezegun import freeze_time
|
||||
|
||||
from core import factories, models
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
def test_models_invitations_readonly_after_create():
|
||||
"""Existing invitations should be readonly."""
|
||||
invitation = factories.InvitationFactory()
|
||||
with pytest.raises(exceptions.PermissionDenied):
|
||||
invitation.save()
|
||||
|
||||
|
||||
def test_models_invitations_email_no_empty_mail():
|
||||
"""The "email" field should not be empty."""
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||
factories.InvitationFactory(email="")
|
||||
|
||||
|
||||
def test_models_invitations_email_no_null_mail():
|
||||
"""The "email" field is required."""
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be null"):
|
||||
factories.InvitationFactory(email=None)
|
||||
|
||||
|
||||
def test_models_invitations_document_required():
|
||||
"""The "document" field is required."""
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be null"):
|
||||
factories.InvitationFactory(document=None)
|
||||
|
||||
|
||||
def test_models_invitations_document_should_be_document_instance():
|
||||
"""The "document" field should be a document instance."""
|
||||
with pytest.raises(
|
||||
ValueError, match='Invitation.document" must be a "Document" instance'
|
||||
):
|
||||
factories.InvitationFactory(document="ee")
|
||||
|
||||
|
||||
def test_models_invitations_role_required():
|
||||
"""The "role" field is required."""
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||
factories.InvitationFactory(role="")
|
||||
|
||||
|
||||
def test_models_invitations_role_among_choices():
|
||||
"""The "role" field should be a valid choice."""
|
||||
with pytest.raises(
|
||||
exceptions.ValidationError, match="Value 'boss' is not a valid choice"
|
||||
):
|
||||
factories.InvitationFactory(role="boss")
|
||||
|
||||
|
||||
def test_models_invitations__is_expired(settings):
|
||||
"""
|
||||
The 'is_expired' property should return False until validity duration
|
||||
is exceeded and True afterwards.
|
||||
"""
|
||||
expired_invitation = factories.InvitationFactory()
|
||||
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_invitation__new_user__convert_invitations_to_accesses():
|
||||
"""
|
||||
Upon creating a new user, invitations linked to the email
|
||||
should be converted to accesses and then deleted.
|
||||
"""
|
||||
# Two invitations to the same mail but to different documents
|
||||
invitation_to_document1 = factories.InvitationFactory()
|
||||
invitation_to_document2 = factories.InvitationFactory(
|
||||
email=invitation_to_document1.email
|
||||
)
|
||||
|
||||
other_invitation = factories.InvitationFactory(
|
||||
document=invitation_to_document2.document
|
||||
) # another person invited to document2
|
||||
|
||||
new_user = factories.UserFactory(email=invitation_to_document1.email)
|
||||
|
||||
# The invitation regarding
|
||||
assert models.DocumentAccess.objects.filter(
|
||||
document=invitation_to_document1.document, user=new_user
|
||||
).exists()
|
||||
assert models.DocumentAccess.objects.filter(
|
||||
document=invitation_to_document2.document, user=new_user
|
||||
).exists()
|
||||
assert not models.Invitation.objects.filter(
|
||||
document=invitation_to_document1.document, email=invitation_to_document1.email
|
||||
).exists() # invitation "consumed"
|
||||
assert not models.Invitation.objects.filter(
|
||||
document=invitation_to_document2.document, email=invitation_to_document2.email
|
||||
).exists() # invitation "consumed"
|
||||
assert models.Invitation.objects.filter(
|
||||
document=invitation_to_document2.document, email=other_invitation.email
|
||||
).exists() # the other invitation remains
|
||||
|
||||
|
||||
def test_models_invitation__new_user__filter_expired_invitations():
|
||||
"""
|
||||
Upon creating a new identity, valid invitations should be converted into accesses
|
||||
and expired invitations should remain unchanged.
|
||||
"""
|
||||
document = factories.DocumentFactory()
|
||||
with freeze_time("2020-01-01"):
|
||||
expired_invitation = factories.InvitationFactory(document=document)
|
||||
user_email = expired_invitation.email
|
||||
valid_invitation = factories.InvitationFactory(email=user_email)
|
||||
|
||||
new_user = factories.UserFactory(email=user_email)
|
||||
|
||||
# valid invitation should have granted access to the related document
|
||||
assert models.DocumentAccess.objects.filter(
|
||||
document=valid_invitation.document, user=new_user
|
||||
).exists()
|
||||
assert not models.Invitation.objects.filter(
|
||||
document=valid_invitation.document, email=user_email
|
||||
).exists()
|
||||
|
||||
# expired invitation should not have been consumed
|
||||
assert not models.DocumentAccess.objects.filter(
|
||||
document=expired_invitation.document, user=new_user
|
||||
).exists()
|
||||
assert models.Invitation.objects.filter(
|
||||
document=expired_invitation.document, email=user_email
|
||||
).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_invitations, num_queries", [(0, 3), (1, 6), (20, 6)])
|
||||
def test_models_invitation__new_user__user_creation_constant_num_queries(
|
||||
django_assert_num_queries, num_invitations, num_queries
|
||||
):
|
||||
"""
|
||||
The number of queries executed during user creation should not be proportional
|
||||
to the number of invitations being processed.
|
||||
"""
|
||||
user_email = fake.email()
|
||||
|
||||
if num_invitations != 0:
|
||||
factories.InvitationFactory.create_batch(num_invitations, email=user_email)
|
||||
|
||||
# with no invitation, we skip an "if", resulting in 8 requests
|
||||
# otherwise, we should have 11 queries with any number of invitations
|
||||
with django_assert_num_queries(num_queries):
|
||||
models.User.objects.create(email=user_email, password="!")
|
||||
|
||||
|
||||
def test_models_document_invitations_email():
|
||||
"""Check email invitation during invitation creation."""
|
||||
member_access = factories.UserDocumentAccessFactory(role="member")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
invitation = factories.InvitationFactory(document=document, email="john@people.com")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == [invitation.email]
|
||||
assert email.subject == "Invitation to join Impress!"
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Impress!" in email_content
|
||||
assert "[//example.com]" in email_content
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"django.core.mail.send_mail",
|
||||
side_effect=smtplib.SMTPException("Error SMTPException"),
|
||||
)
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_models_document_invitations_email_failed(mock_logger, _mock_send_mail):
|
||||
"""Check invitation behavior when an SMTP error occurs during invitation creation."""
|
||||
|
||||
member_access = factories.UserDocumentAccessFactory(role="member")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
|
||||
# No error should be raised
|
||||
invitation = factories.InvitationFactory(document=document, email="john@people.com")
|
||||
|
||||
# No email has been sent
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
# Logger should be called
|
||||
mock_logger.assert_called_once()
|
||||
|
||||
(
|
||||
_,
|
||||
email,
|
||||
exception,
|
||||
) = mock_logger.call_args.args
|
||||
|
||||
assert email == invitation.email
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
|
||||
|
||||
# get_abilities
|
||||
|
||||
|
||||
def test_models_document_invitations_get_abilities_anonymous():
|
||||
"""Check abilities returned for an anonymous user."""
|
||||
access = factories.InvitationFactory()
|
||||
abilities = access.get_abilities(AnonymousUser())
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_invitations_get_abilities_authenticated():
|
||||
"""Check abilities returned for an authenticated user."""
|
||||
access = factories.InvitationFactory()
|
||||
user = factories.UserFactory()
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||
def test_models_document_invitations_get_abilities_privileged_member(
|
||||
role, via, mock_user_get_teams
|
||||
):
|
||||
"""Check abilities for a document member with a privileged role."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document) # another one
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
abilities = invitation.get_abilities(user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_models_document_invitations_get_abilities_member(via, mock_user_get_teams):
|
||||
"""Check abilities for a document member with 'member' role."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="member"
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
abilities = invitation.get_abilities(user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
Reference in New Issue
Block a user