From c048b2ae9540829593f2d90f40d5491443a68970 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Thu, 16 Oct 2025 17:26:02 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20manage=20invitation=20p?= =?UTF-8?q?artial=20update=20without=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An invitation can be updated to change its role. The front use a PATCH sending only the changed role, so the email is missing in the InivtationSerializer.validate method. We have to check first if an email is present before working on it. --- CHANGELOG.md | 2 ++ src/backend/core/api/serializers.py | 3 +- .../test_api_document_invitations.py | 31 +++++++++++++++++++ .../app-impress/doc-member-create.spec.ts | 10 ++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d410d01..b14ea361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to ### Fixed - ⚡️(backend) improve trashbin endpoint performance +- 🐛(backend) manage invitation partial update without email #1494 + ## [3.8.0] - 2025-10-14 diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 6c09cf18..81b26d5e 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -749,7 +749,8 @@ class InvitationSerializer(serializers.ModelSerializer): if self.instance is None: attrs["issuer"] = user - attrs["email"] = attrs["email"].lower() + if attrs.get("email"): + attrs["email"] = attrs["email"].lower() return attrs diff --git a/src/backend/core/tests/documents/test_api_document_invitations.py b/src/backend/core/tests/documents/test_api_document_invitations.py index 94c5f881..46428c6f 100644 --- a/src/backend/core/tests/documents/test_api_document_invitations.py +++ b/src/backend/core/tests/documents/test_api_document_invitations.py @@ -769,6 +769,37 @@ def test_api_document_invitations_update_authenticated_unprivileged( assert value == old_invitation_values[key] +@pytest.mark.parametrize("via", VIA) +@pytest.mark.parametrize("role", ["administrator", "owner"]) +def test_api_document_invitations_patch(via, role, mock_user_teams): + """Partially updating an invitation should be allowed.""" + + user = factories.UserFactory() + invitation = factories.InvitationFactory(role="editor") + + if via == USER: + factories.UserDocumentAccessFactory( + document=invitation.document, user=user, role=role + ) + elif via == TEAM: + mock_user_teams.return_value = ["lasuite", "unknown"] + factories.TeamDocumentAccessFactory( + document=invitation.document, team="lasuite", role=role + ) + + client = APIClient() + client.force_login(user) + + response = client.patch( + f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/", + {"role": "reader"}, + format="json", + ) + assert response.status_code == 200 + invitation.refresh_from_db() + assert invitation.role == "reader" + + # Delete diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 623b8043..dec14f97 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -202,9 +202,19 @@ test.describe('Document create member', () => { ); await expect(userInvitation).toBeVisible(); + const responsePromisePatchInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && + response.status() === 200 && + response.request().method() === 'PATCH', + ); + await userInvitation.getByLabel('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Reader' }).click(); + const responsePatchInvitation = await responsePromisePatchInvitation; + expect(responsePatchInvitation.ok()).toBeTruthy(); + const moreActions = userInvitation.getByRole('button', { name: 'Open invitation actions menu', });