From 8db6309389d34ef30e73da9b160e68a45a5e5cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Molina?= Date: Wed, 10 Jun 2026 16:07:03 +0200 Subject: [PATCH] fix: handle already-deleted room in Zoom delete webhook --- vc_zoom/indico_vc_zoom/controllers.py | 13 +++++++++++- vc_zoom/tests/webhook_test.py | 29 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/vc_zoom/indico_vc_zoom/controllers.py b/vc_zoom/indico_vc_zoom/controllers.py index 81fe8d80..79faeacb 100644 --- a/vc_zoom/indico_vc_zoom/controllers.py +++ b/vc_zoom/indico_vc_zoom/controllers.py @@ -12,6 +12,7 @@ from flask_pluginengine import current_plugin from marshmallow import EXCLUDE from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy.orm.exc import StaleDataError from webargs import fields from webargs.flaskparser import use_kwargs from werkzeug.exceptions import Forbidden, ServiceUnavailable @@ -85,6 +86,16 @@ def _handle_validation(self, payload): 'encryptedToken': signed_token }) + def _mark_room_deleted(self, vc_room): + vc_room.status = VCRoomStatus.deleted + try: + db.session.flush() + except StaleDataError: + # The VC room was already removed in Indico (the deletion that triggered this + # webhook), so the row is gone and there is nothing left to update. + db.session.rollback() + current_plugin.logger.info('VC room for the deleted Zoom meeting was already removed in Indico') + def _handle_zoom_event(self, event, payload): # XXX Some Zoom events receive the ID as a string, others as a number meeting_id = int(payload['object']['id']) @@ -99,7 +110,7 @@ def _handle_zoom_event(self, event, payload): current_plugin.refresh_room(vc_room, None) elif event in ('meeting.deleted', 'webinar.deleted'): current_plugin.logger.info('Zoom meeting deleted: %s', meeting_id) - vc_room.status = VCRoomStatus.deleted + self._mark_room_deleted(vc_room) elif event in ('meeting.participant_joined', 'webinar.participant_joined'): if not vc_room.data.get('auto_register') or not vc_room.data.get('auto_checkin'): return diff --git a/vc_zoom/tests/webhook_test.py b/vc_zoom/tests/webhook_test.py index b760a348..ac15392c 100644 --- a/vc_zoom/tests/webhook_test.py +++ b/vc_zoom/tests/webhook_test.py @@ -210,3 +210,32 @@ def test_webinar_participant_joined_checks_in_registration(db, zoom_plugin, reg_ assert resp.status_code == 200 db.session.refresh(reg) assert reg.checked_in + + +# ── meeting.deleted tests ───────────────────────────────────────────────────── + +@pytest.mark.usefixtures('request_context') +def test_meeting_deleted_marks_room_deleted(db, zoom_user, reg_form, webhook_client, create_vc_room_with_assoc): + vc_room, _assoc = create_vc_room_with_assoc(reg_form.event, zoom_user) + payload = {'event': 'meeting.deleted', 'payload': {'object': {'id': str(vc_room.data['zoom_id'])}}} + + resp = webhook_client(payload) + + assert resp.status_code == 200 + db.session.refresh(vc_room) + assert vc_room.status == VCRoomStatus.deleted + + +def test_meeting_deleted_tolerates_already_removed_room(db, zoom_plugin, zoom_user): + from indico_vc_zoom.controllers import RHWebhook + + vc_room = VCRoom(name='Gone', type='zoom', status=VCRoomStatus.created, created_by_user=zoom_user) + vc_room.data = {'zoom_id': 99999} + db.session.add(vc_room) + db.session.flush() + # Simulate Indico having hard-deleted the row in the transaction that triggered this webhook. + db.session.execute(VCRoom.__table__.delete().where(VCRoom.__table__.c.id == vc_room.id)) + + with zoom_plugin.plugin_context(): + # Must not raise StaleDataError even though the row is gone. + RHWebhook()._mark_room_deleted(vc_room)