✨(invitations) refresh invitations
invitations can now be refreshed, de-expiring them or moving their expiration date further in the future.
This commit is contained in:
committed by
Marie
parent
1e02ded7b8
commit
3b58fb7e1e
@@ -8,6 +8,8 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- ✨(invitations) refresh expired invitations
|
||||
|
||||
## [1.23.0] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -36,7 +36,7 @@ from core.plugins.registry import registry as plugin_hooks_registry
|
||||
from core.utils.webhooks import webhooks_synchronizer
|
||||
from core.validators import get_field_validators_from_setting
|
||||
|
||||
from mailbox_manager.exceptions import EmailAlreadyKnownException
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -1001,14 +1001,21 @@ class BaseInvitation(BaseModel):
|
||||
if User.objects.filter(email__iexact=self.email).exists():
|
||||
raise EmailAlreadyKnownException
|
||||
|
||||
def refresh(self):
|
||||
"""A simple way to refresh invitation and move expiration date."""
|
||||
self.clean()
|
||||
self.updated_at = timezone.now()
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
"""Calculate if invitation is still valid or has expired."""
|
||||
if not self.created_at:
|
||||
if not self.updated_at:
|
||||
return None
|
||||
|
||||
validity_duration = timedelta(seconds=settings.INVITATION_VALIDITY_DURATION)
|
||||
return timezone.now() > (self.created_at + validity_duration)
|
||||
return timezone.now() > (self.updated_at + validity_duration)
|
||||
|
||||
def _get_mail_subject(self):
|
||||
"""Get the subject of the invitation."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from django.db.models import Q, Subquery
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import exceptions, filters, mixins, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
@@ -428,6 +429,14 @@ class MailDomainInvitationViewset(
|
||||
)
|
||||
raise exc
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def refresh(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""Enable mailbox. Send a request to dimail and change status in our DB"""
|
||||
invitation = get_object_or_404(models.MailDomainInvitation, id=kwargs["id"])
|
||||
invitation.refresh()
|
||||
invitation.email_invitation()
|
||||
return Response(serializers.MailDomainInvitationSerializer(invitation).data)
|
||||
|
||||
|
||||
class AliasViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.utils.translation import get_language, gettext, override
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import BaseInvitation, BaseModel, Organization, User
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
|
||||
from mailbox_manager.enums import (
|
||||
MailboxStatusChoices,
|
||||
@@ -413,6 +414,23 @@ class MailDomainInvitation(BaseInvitation):
|
||||
)
|
||||
]
|
||||
|
||||
def refresh(self):
|
||||
"""Create domain access if refresh fail because user was created
|
||||
after invitation expiration"""
|
||||
try:
|
||||
super().refresh()
|
||||
except EmailAlreadyKnownException as exc:
|
||||
user = User.objects.get(email__iexact=self.email)
|
||||
|
||||
MailDomainAccess.objects.create(
|
||||
user=user,
|
||||
domain=self.domain,
|
||||
role=self.role,
|
||||
)
|
||||
|
||||
self.delete() # delete related invitation
|
||||
raise exc
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.email} invited to {self.domain}"
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Tests for MailDomainInvitations API endpoint in People's app mailbox_manager.
|
||||
Focus on "refresh" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__anonymous_cannot_refresh():
|
||||
"""Anonymous are not allowed to refresh."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_domain_invitations__no_access_cannot_see_invitation():
|
||||
"""Users with no permission on the domain should be returned a 404 not found."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
otro_access = factories.MailDomainAccessFactory(
|
||||
role=enums.MailDomainRoleChoices.ADMIN
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(otro_access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_api_domain_invitations__viewer_cannot_refresh():
|
||||
"""Viewers cannot refresh invitations."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_api_domain_invitations__admins_can_refresh_expired_invitations():
|
||||
"""Admins can refresh invitations."""
|
||||
with freeze_time("2025-12-16"):
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
assert invitation.is_expired is True # check invitation is correctly expired
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
# can refresh expired invitations
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["is_expired"] is False
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
invitation.refresh_from_db()
|
||||
|
||||
# can also refresh valid
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["is_expired"] is False
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_invitations__refreshing_invitation_can_convert_to_access():
|
||||
"""In the event of someone creating their account after their invitation expired,
|
||||
said invitation has not been converted to access.
|
||||
We don't want to create a new invitation but create an access instead."""
|
||||
with freeze_time("2025-12-16"):
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
core_factories.UserFactory(email=invitation.email)
|
||||
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
assert models.MailDomainAccess.objects.count() == 2
|
||||
assert not models.MailDomainInvitation.objects.exists()
|
||||
Reference in New Issue
Block a user